Windows 窗口拖拽缩放工具

GB.Windows 为页面中的窗口元素提供拖拽移动、原生尺寸调整和焦点通知能力。它基于 Pointer Events、CSS resize 和 MutationObserver,新加入页面且匹配选择器的窗口也会自动初始化。

安装与引入

浏览器直接引入

<script src="https://cloud.m775.net/js/windows.js"></script>

npm 项目

npm i @gentbo/windows

import '@gentbo/windows';

准备窗口结构

目标窗口应使用可定位布局,并包含一个 header 作为拖拽标题栏。调整尺寸依赖浏览器原生 CSS resize,因此窗口的 overflow 不能是 visible。

<section class="app-window">
  <header>
    <strong>窗口标题</strong>
    <button type="button">关闭</button>
  </header>
  <main>窗口内容</main>
</section>
.app-window {
  position: fixed;
  left: 80px;
  top: 60px;
  width: 720px;
  height: 480px;
  min-width: 280px;
  min-height: 200px;
  overflow: hidden;
}

.app-window > header {
  cursor: move;
  user-select: none;
  touch-action: none;
}

快速开始

传入窗口选择器,按需启用缩放、移动和焦点回调,最后调用 start()。

GB.Windows('.app-window')
  .resize()
  .move()
  .focus(windowElement => {
    windowElement.classList.add('active');
  })
  .start();

未调用 resize() 或 move() 时,对应能力不会启用。焦点监听始终会注册,但只有设置有效回调后才执行具体业务。

API 速查

GB.Windows(selector)创建窗口管理器,selector 用于匹配一个或多个窗口元素。
.resize()启用浏览器原生双向尺寸调整,并返回当前实例。
.move(allowOutside?, margin?)启用标题栏拖拽;margin 按 [上, 右, 下, 左] 设置安全边距,仅在 allowOutside 为 false 时生效。
.focus(callback)窗口获得交互焦点时执行 callback(windowElement),并返回当前实例。
.start()初始化现有窗口并监听后续 DOM 变化,返回当前实例。
.run()start() 的兼容入口,返回当前实例。

窗口移动与边界

move() 默认将窗口限制在可用视口内。第二个参数使用 [上, 右, 下, 左] 顺序设置安全边距,四个方向的默认值均为 0。

// 限制在视口内
GB.Windows('.dialog')
  .move()
  .start();

// 上、右、下、左分别预留 16、24、32、24px
GB.Windows('.workspace-window')
  .move(false, [16, 24, 32, 24])
  .start();

// 允许拖到视口之外
GB.Windows('.floating-panel')
  .move(true) // 第二个参数在此模式下无效
  .start();

拖拽从窗口内部的 header 开始。点击标题栏中的 button 不会触发拖拽,因此最小化、最大化和关闭按钮仍可正常操作。

工具不识别任务栏或其他页面组件,可视区域的四向预留空间完全由第二个参数控制。例如需要避开底部栏时,应把底部栏占用的空间作为第三个数组值传入。

调整窗口尺寸

resize() 会为目标窗口设置 resize: both。未允许移出视口时,还会根据窗口当前位置和四向安全边距设置 max-width 与 max-height。

GB.Windows('.resizable-window')
  .resize()
  .move()
  .start();

最小尺寸由窗口自身 CSS 的 min-width 和 min-height 决定。使用原生 resize 时必须设置 overflow: auto、hidden 或 scroll,否则部分浏览器不会显示缩放手柄。

焦点与层级管理

focus() 只通知当前交互的窗口,不会自动修改 z-index。可在回调中维护活动类和窗口层级。

let zIndex = 10;

GB.Windows('.app-window')
  .resize()
  .move()
  .focus(current => {
    document.querySelectorAll('.app-window').forEach(item => {
      item.classList.toggle('active', item === current);
    });
    current.style.zIndex = ++zIndex;
  })
  .start();

窗口内已有的 iframe 也会参与焦点判断:同源 iframe 监听其内部 pointerdown;跨域 iframe 无法访问内部文档时,通过窗口失焦和 activeElement 提供兜底判断。

动态创建窗口

start() 会通过 MutationObserver 监听 body。后续插入并匹配目标选择器的窗口会自动绑定,不需要再次调用。

const windows = GB.Windows('.app-window')
  .resize()
  .move()
  .focus(handleFocus)
  .start();

const newWindow = document.createElement('section');
newWindow.className = 'app-window';
newWindow.innerHTML = `
  <header>动态窗口</header>
  <main>窗口内容</main>
`;
document.body.appendChild(newWindow);

使用边界

启动次数同一管理器不应重复调用 start();重复调用会创建额外的 MutationObserver。
销毁能力当前 API 没有 disconnect 或 destroy 方法,适合与页面生命周期一致的窗口系统。
标题栏约定移动功能固定查找窗口内部的第一个 header,不支持自定义标题栏选择器。
定位方式工具写入 left 和 top;窗口应使用 fixed、absolute 等可定位布局。
动态 iframe窗口首次绑定后才插入的 iframe 不会单独补绑内部焦点监听,建议创建好窗口内容后再加入页面。
浏览器环境依赖 Pointer Events、MutationObserver、requestAnimationFrame、原生 CSS resize 和 WeakSet。