flexdesk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ManagedWindow - A reusable window component with drag, resize, minimize, and persistence.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const win = new ManagedWindow({
|
|
6
|
+
* id: 'my-window',
|
|
7
|
+
* title: 'My Window',
|
|
8
|
+
* icon: 'folder_open', // Material Symbols icon name (optional)
|
|
9
|
+
* content: myDomElement,
|
|
10
|
+
* minWidth: 400,
|
|
11
|
+
* minHeight: 300,
|
|
12
|
+
* defaultWidth: 600,
|
|
13
|
+
* defaultHeight: 400,
|
|
14
|
+
* canMaximize: true,
|
|
15
|
+
* onClose: () => { ... }
|
|
16
|
+
* });
|
|
17
|
+
* win.show();
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getSetting } from '../../core/settings.js';
|
|
21
|
+
|
|
22
|
+
const STORAGE_KEY = 'ecosim.managedWindows.v1';
|
|
23
|
+
const BASE_Z_INDEX = 6000;
|
|
24
|
+
const MAX_Z_INDEX = 6999;
|
|
25
|
+
|
|
26
|
+
// Bar heights for maximize bounds. Must match the tiling shell's
|
|
27
|
+
// own constants (host.css locks `.global-bottom-bar` to 22 px) so a
|
|
28
|
+
// maximized window stops above the bottom bar instead of slipping
|
|
29
|
+
// under it.
|
|
30
|
+
const TOP_BAR_HEIGHT = 35;
|
|
31
|
+
const BOTTOM_BAR_HEIGHT = 22;
|
|
32
|
+
|
|
33
|
+
// Default icon for windows
|
|
34
|
+
const DEFAULT_ICON = 'web_asset';
|
|
35
|
+
|
|
36
|
+
// Global state for z-order management
|
|
37
|
+
let _zIndexCounter = 0;
|
|
38
|
+
let _activeWindows = new Map(); // id -> ManagedWindow instance
|
|
39
|
+
|
|
40
|
+
/** Collect the focusable elements inside `root`, in DOM order. Used
|
|
41
|
+
* by the modal focus trap so Tab cycles within the modal frame and
|
|
42
|
+
* can't escape into the underlying tiles. */
|
|
43
|
+
function _collectFocusable(root) {
|
|
44
|
+
if (!root) return [];
|
|
45
|
+
const sel = [
|
|
46
|
+
'a[href]',
|
|
47
|
+
'button:not([disabled])',
|
|
48
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
49
|
+
'select:not([disabled])',
|
|
50
|
+
'textarea:not([disabled])',
|
|
51
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
52
|
+
].join(',');
|
|
53
|
+
return Array.from(root.querySelectorAll(sel)).filter((el) => {
|
|
54
|
+
// Skip hidden / zero-size elements — Tab visits only what the
|
|
55
|
+
// user can actually see.
|
|
56
|
+
if (el.hidden) return false;
|
|
57
|
+
if (el.closest('[hidden]')) return false;
|
|
58
|
+
const r = el.getBoundingClientRect();
|
|
59
|
+
return r.width > 0 || r.height > 0;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Global state cache
|
|
64
|
+
let _stateCache = null;
|
|
65
|
+
|
|
66
|
+
function _loadState() {
|
|
67
|
+
if (_stateCache) return _stateCache;
|
|
68
|
+
try {
|
|
69
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
70
|
+
_stateCache = raw ? JSON.parse(raw) : {};
|
|
71
|
+
} catch {
|
|
72
|
+
_stateCache = {};
|
|
73
|
+
}
|
|
74
|
+
return _stateCache;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _saveState(state) {
|
|
78
|
+
_stateCache = state;
|
|
79
|
+
try {
|
|
80
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('[ManagedWindow] Failed to save state:', err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _getWindowState(id) {
|
|
87
|
+
const state = _loadState();
|
|
88
|
+
return state[id] || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _setWindowState(id, windowState) {
|
|
92
|
+
const state = _loadState();
|
|
93
|
+
state[id] = windowState;
|
|
94
|
+
_saveState(state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class ManagedWindow {
|
|
98
|
+
/**
|
|
99
|
+
* @param {Object} options
|
|
100
|
+
* @param {string} options.id - Unique window ID for persistence
|
|
101
|
+
* @param {string} options.title - Window title
|
|
102
|
+
* @param {string} [options.icon] - Material Symbols icon name (default: 'web_asset')
|
|
103
|
+
* @param {HTMLElement|Function} options.content - Content element or render function
|
|
104
|
+
* @param {number} [options.minWidth=400] - Minimum width
|
|
105
|
+
* @param {number} [options.minHeight=300] - Minimum height
|
|
106
|
+
* @param {number} [options.defaultWidth=600] - Default width
|
|
107
|
+
* @param {number} [options.defaultHeight=400] - Default height
|
|
108
|
+
* @param {boolean} [options.canMinimize=true] - Whether minimize button is shown
|
|
109
|
+
* @param {boolean} [options.canMaximize=true] - Whether maximize button is shown
|
|
110
|
+
* @param {boolean} [options.canResize=true] - Whether window can be resized
|
|
111
|
+
* @param {boolean} [options.canDrag=true] - Whether window can be dragged
|
|
112
|
+
* @param {boolean} [options.modal=false] - Whether to show backdrop
|
|
113
|
+
* @param {Function} [options.onClose] - Callback when window is closed
|
|
114
|
+
* @param {Function} [options.beforeClose] - Guard called before close. Return false (or a Promise resolving to false) to prevent closing.
|
|
115
|
+
* @param {Function} [options.onMinimize] - Callback when window is minimized
|
|
116
|
+
*/
|
|
117
|
+
constructor(options) {
|
|
118
|
+
this.id = options.id;
|
|
119
|
+
this.title = options.title || 'Window';
|
|
120
|
+
this.icon = options.icon || DEFAULT_ICON;
|
|
121
|
+
this.content = options.content;
|
|
122
|
+
this.minWidth = options.minWidth || 400;
|
|
123
|
+
this.minHeight = options.minHeight || 300;
|
|
124
|
+
this.defaultWidth = options.defaultWidth || 600;
|
|
125
|
+
this.defaultHeight = options.defaultHeight || 400;
|
|
126
|
+
this.canMinimize = options.canMinimize ?? true;
|
|
127
|
+
this.canMaximize = options.canMaximize ?? true;
|
|
128
|
+
this.canResize = options.canResize ?? true;
|
|
129
|
+
this.modal = options.modal ?? false;
|
|
130
|
+
// Backdrop appearance (modal only). `undefined` → inherit the CSS
|
|
131
|
+
// default (blur 2px over a 50% dim). Override per-modal when the
|
|
132
|
+
// background should stay legible — e.g. a live-applied editor where
|
|
133
|
+
// you want to watch the content behind update.
|
|
134
|
+
// backdropBlur: blur radius in px; 0 disables the blur.
|
|
135
|
+
// backdropOpacity: dim darkness, 0 (clear) … 1 (opaque black).
|
|
136
|
+
this.backdropBlur = options.backdropBlur;
|
|
137
|
+
this.backdropOpacity = options.backdropOpacity;
|
|
138
|
+
// Modals are non-draggable by default
|
|
139
|
+
this.canDrag = options.canDrag ?? !this.modal;
|
|
140
|
+
this.onClose = options.onClose;
|
|
141
|
+
this.beforeClose = options.beforeClose || null;
|
|
142
|
+
this.onMinimize = options.onMinimize;
|
|
143
|
+
|
|
144
|
+
this.element = null;
|
|
145
|
+
this.backdropElement = null;
|
|
146
|
+
this.contentContainer = null;
|
|
147
|
+
this.isVisible = false;
|
|
148
|
+
this.isMinimized = false;
|
|
149
|
+
this.isMaximized = false;
|
|
150
|
+
this.zIndex = BASE_Z_INDEX;
|
|
151
|
+
|
|
152
|
+
// Position/size state - clamp to viewport bounds
|
|
153
|
+
this.x = 0;
|
|
154
|
+
this.y = 0;
|
|
155
|
+
const maxWidth = window.innerWidth;
|
|
156
|
+
const maxHeight = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
|
|
157
|
+
this.width = Math.min(this.defaultWidth, maxWidth);
|
|
158
|
+
this.height = Math.min(this.defaultHeight, maxHeight);
|
|
159
|
+
|
|
160
|
+
// State before maximize (for restore)
|
|
161
|
+
this._preMaximizeState = null;
|
|
162
|
+
|
|
163
|
+
// Drag state
|
|
164
|
+
this._dragState = null;
|
|
165
|
+
this._resizeState = null;
|
|
166
|
+
|
|
167
|
+
// Bound handlers for cleanup
|
|
168
|
+
this._boundOnPointerMove = this._onPointerMove.bind(this);
|
|
169
|
+
this._boundOnPointerUp = this._onPointerUp.bind(this);
|
|
170
|
+
this._boundOnKeyDown = this._onKeyDown.bind(this);
|
|
171
|
+
|
|
172
|
+
// Register globally
|
|
173
|
+
_activeWindows.set(this.id, this);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Show the window.
|
|
178
|
+
*/
|
|
179
|
+
show() {
|
|
180
|
+
if (this.isVisible && !this.isMinimized) {
|
|
181
|
+
this.bringToFront();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!this.element) {
|
|
186
|
+
this._build();
|
|
187
|
+
this._restoreState();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.isMinimized) {
|
|
191
|
+
this._restore();
|
|
192
|
+
} else {
|
|
193
|
+
this._applyPosition();
|
|
194
|
+
document.body.appendChild(this.element);
|
|
195
|
+
if (this.modal && this.backdropElement) {
|
|
196
|
+
document.body.appendChild(this.backdropElement);
|
|
197
|
+
}
|
|
198
|
+
this.isVisible = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.bringToFront();
|
|
202
|
+
document.addEventListener('keydown', this._boundOnKeyDown);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Hide/close the window.
|
|
207
|
+
* @param {{ force?: boolean }} [options] - Pass force:true to bypass the beforeClose guard.
|
|
208
|
+
*/
|
|
209
|
+
close({ force = false } = {}) {
|
|
210
|
+
if (!this.isVisible) return;
|
|
211
|
+
|
|
212
|
+
if (!force && this.beforeClose) {
|
|
213
|
+
const result = this.beforeClose();
|
|
214
|
+
if (result && typeof result.then === 'function') {
|
|
215
|
+
result.then(allowed => { if (allowed !== false) this._doClose(); });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (result === false) return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this._doClose();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Internal close — always executes, no guard. */
|
|
225
|
+
_doClose() {
|
|
226
|
+
if (!this.isVisible) return;
|
|
227
|
+
|
|
228
|
+
this._saveCurrentState();
|
|
229
|
+
|
|
230
|
+
// Call onClose BEFORE removing elements so content can still read its state
|
|
231
|
+
// (e.g., expression modal needs to read the editor value before DOM is detached)
|
|
232
|
+
if (this.onClose) {
|
|
233
|
+
this.onClose();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (this.element && this.element.parentNode) {
|
|
237
|
+
this.element.parentNode.removeChild(this.element);
|
|
238
|
+
}
|
|
239
|
+
if (this.backdropElement && this.backdropElement.parentNode) {
|
|
240
|
+
this.backdropElement.parentNode.removeChild(this.backdropElement);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.isVisible = false;
|
|
244
|
+
this.isMinimized = false;
|
|
245
|
+
document.removeEventListener('keydown', this._boundOnKeyDown);
|
|
246
|
+
|
|
247
|
+
// Notify taskbar
|
|
248
|
+
window.dispatchEvent(new CustomEvent('managed-window-closed', { detail: { id: this.id } }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Minimize to taskbar with animation.
|
|
253
|
+
*/
|
|
254
|
+
minimize() {
|
|
255
|
+
if (!this.isVisible || this.isMinimized) return;
|
|
256
|
+
|
|
257
|
+
this._saveCurrentState();
|
|
258
|
+
this.isMinimized = true;
|
|
259
|
+
|
|
260
|
+
if (this.onMinimize) {
|
|
261
|
+
this.onMinimize();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (this.backdropElement) {
|
|
265
|
+
this.backdropElement.style.display = 'none';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Dispatch event first so the taskbar button is created synchronously
|
|
269
|
+
window.dispatchEvent(new CustomEvent('managed-window-minimized', {
|
|
270
|
+
detail: { id: this.id, title: this.title, icon: this.icon }
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
if (!this.element) return;
|
|
274
|
+
|
|
275
|
+
if (!getSetting('window.animateMinimize', true)) {
|
|
276
|
+
this.element.style.display = 'none';
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Calculate animation target toward the taskbar button
|
|
281
|
+
this._setMinimizeTargetProperties();
|
|
282
|
+
this.element.classList.add('twm-managed-window--minimizing');
|
|
283
|
+
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
if (this.element) {
|
|
286
|
+
this.element.style.display = 'none';
|
|
287
|
+
this.element.classList.remove('twm-managed-window--minimizing');
|
|
288
|
+
this._clearTargetProperties();
|
|
289
|
+
}
|
|
290
|
+
}, 200);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Restore from minimized state with animation.
|
|
295
|
+
*/
|
|
296
|
+
_restore() {
|
|
297
|
+
if (!this.isMinimized) return;
|
|
298
|
+
|
|
299
|
+
// Capture taskbar item rect BEFORE dispatching the event (which removes it)
|
|
300
|
+
const taskbarRect = this._getTaskbarItemRect();
|
|
301
|
+
|
|
302
|
+
if (this.element) {
|
|
303
|
+
this.element.style.display = '';
|
|
304
|
+
|
|
305
|
+
const animate = getSetting('window.animateMinimize', true);
|
|
306
|
+
if (animate) {
|
|
307
|
+
this._setRestoreTargetProperties(taskbarRect);
|
|
308
|
+
this.element.classList.add('twm-managed-window--restoring');
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
if (this.element) {
|
|
311
|
+
this.element.classList.remove('twm-managed-window--restoring');
|
|
312
|
+
this._clearTargetProperties();
|
|
313
|
+
}
|
|
314
|
+
}, 250);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (this.modal && this.backdropElement) {
|
|
318
|
+
this.backdropElement.style.display = '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.isMinimized = false;
|
|
322
|
+
this.bringToFront();
|
|
323
|
+
|
|
324
|
+
// Notify taskbar (removes the taskbar button)
|
|
325
|
+
window.dispatchEvent(new CustomEvent('managed-window-restored', { detail: { id: this.id } }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Toggle maximize state.
|
|
330
|
+
*/
|
|
331
|
+
toggleMaximize() {
|
|
332
|
+
if (!this.canMaximize) return;
|
|
333
|
+
|
|
334
|
+
if (this.isMaximized) {
|
|
335
|
+
// Restore
|
|
336
|
+
if (this._preMaximizeState) {
|
|
337
|
+
this.x = this._preMaximizeState.x;
|
|
338
|
+
this.y = this._preMaximizeState.y;
|
|
339
|
+
this.width = this._preMaximizeState.width;
|
|
340
|
+
this.height = this._preMaximizeState.height;
|
|
341
|
+
this._preMaximizeState = null;
|
|
342
|
+
}
|
|
343
|
+
this.isMaximized = false;
|
|
344
|
+
} else {
|
|
345
|
+
// Maximize - respect top and bottom bars
|
|
346
|
+
this._preMaximizeState = { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
347
|
+
this.x = 0;
|
|
348
|
+
this.y = TOP_BAR_HEIGHT;
|
|
349
|
+
this.width = window.innerWidth;
|
|
350
|
+
this.height = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
|
|
351
|
+
this.isMaximized = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this._applyPosition();
|
|
355
|
+
this._saveCurrentState();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Find the taskbar button for this window.
|
|
360
|
+
* @returns {DOMRect|null}
|
|
361
|
+
*/
|
|
362
|
+
_getTaskbarItemRect() {
|
|
363
|
+
const btn = document.querySelector(`.twm-bar-windows__item[data-window-id="${this.id}"]`);
|
|
364
|
+
return btn ? btn.getBoundingClientRect() : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Set CSS custom properties to animate minimize toward the taskbar item.
|
|
369
|
+
*/
|
|
370
|
+
_setMinimizeTargetProperties() {
|
|
371
|
+
const targetRect = this._getTaskbarItemRect();
|
|
372
|
+
if (!targetRect || !this.element) return;
|
|
373
|
+
|
|
374
|
+
const winRect = this.element.getBoundingClientRect();
|
|
375
|
+
const winCenterX = winRect.left + winRect.width / 2;
|
|
376
|
+
const winCenterY = winRect.top + winRect.height / 2;
|
|
377
|
+
const targetCenterX = targetRect.left + targetRect.width / 2;
|
|
378
|
+
const targetCenterY = targetRect.top + targetRect.height / 2;
|
|
379
|
+
|
|
380
|
+
const dx = targetCenterX - winCenterX;
|
|
381
|
+
const dy = targetCenterY - winCenterY;
|
|
382
|
+
const scale = Math.min(targetRect.width / winRect.width, targetRect.height / winRect.height, 0.15);
|
|
383
|
+
|
|
384
|
+
this.element.style.setProperty('--mw-target-x', `${dx}px`);
|
|
385
|
+
this.element.style.setProperty('--mw-target-y', `${dy}px`);
|
|
386
|
+
this.element.style.setProperty('--mw-target-scale', scale);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set CSS custom properties to animate restore from the taskbar item position.
|
|
391
|
+
* @param {DOMRect|null} taskbarRect
|
|
392
|
+
*/
|
|
393
|
+
_setRestoreTargetProperties(taskbarRect) {
|
|
394
|
+
if (!taskbarRect || !this.element) return;
|
|
395
|
+
|
|
396
|
+
const winRect = this.element.getBoundingClientRect();
|
|
397
|
+
const winCenterX = winRect.left + winRect.width / 2;
|
|
398
|
+
const winCenterY = winRect.top + winRect.height / 2;
|
|
399
|
+
const targetCenterX = taskbarRect.left + taskbarRect.width / 2;
|
|
400
|
+
const targetCenterY = taskbarRect.top + taskbarRect.height / 2;
|
|
401
|
+
|
|
402
|
+
const dx = targetCenterX - winCenterX;
|
|
403
|
+
const dy = targetCenterY - winCenterY;
|
|
404
|
+
const scale = Math.min(taskbarRect.width / winRect.width, taskbarRect.height / winRect.height, 0.15);
|
|
405
|
+
|
|
406
|
+
this.element.style.setProperty('--mw-target-x', `${dx}px`);
|
|
407
|
+
this.element.style.setProperty('--mw-target-y', `${dy}px`);
|
|
408
|
+
this.element.style.setProperty('--mw-target-scale', scale);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Clear animation CSS custom properties.
|
|
413
|
+
*/
|
|
414
|
+
_clearTargetProperties() {
|
|
415
|
+
if (!this.element) return;
|
|
416
|
+
this.element.style.removeProperty('--mw-target-x');
|
|
417
|
+
this.element.style.removeProperty('--mw-target-y');
|
|
418
|
+
this.element.style.removeProperty('--mw-target-scale');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Bring window to front of z-order.
|
|
423
|
+
*/
|
|
424
|
+
bringToFront() {
|
|
425
|
+
_zIndexCounter++;
|
|
426
|
+
this.zIndex = Math.min(BASE_Z_INDEX + _zIndexCounter, MAX_Z_INDEX);
|
|
427
|
+
if (this.element) {
|
|
428
|
+
this.element.style.zIndex = this.zIndex.toString();
|
|
429
|
+
}
|
|
430
|
+
if (this.backdropElement) {
|
|
431
|
+
this.backdropElement.style.zIndex = (this.zIndex - 1).toString();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Update window title.
|
|
437
|
+
*/
|
|
438
|
+
setTitle(title) {
|
|
439
|
+
this.title = title;
|
|
440
|
+
if (this.element) {
|
|
441
|
+
const titleEl = this.element.querySelector('.twm-managed-window__title');
|
|
442
|
+
if (titleEl) titleEl.textContent = title;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ========== Private Methods ==========
|
|
447
|
+
|
|
448
|
+
_build() {
|
|
449
|
+
// Create backdrop for modal windows
|
|
450
|
+
if (this.modal) {
|
|
451
|
+
this.backdropElement = document.createElement('div');
|
|
452
|
+
this.backdropElement.className = 'twm-managed-window__backdrop';
|
|
453
|
+
// Apply backdrop overrides as inline styles only when the caller
|
|
454
|
+
// set them; otherwise the CSS default governs (single source of
|
|
455
|
+
// truth for the default look).
|
|
456
|
+
if (this.backdropBlur !== undefined && this.backdropBlur !== null) {
|
|
457
|
+
this.backdropElement.style.backdropFilter =
|
|
458
|
+
this.backdropBlur > 0 ? `blur(${this.backdropBlur}px)` : 'none';
|
|
459
|
+
}
|
|
460
|
+
if (this.backdropOpacity !== undefined && this.backdropOpacity !== null) {
|
|
461
|
+
this.backdropElement.style.background =
|
|
462
|
+
`rgba(0, 0, 0, ${this.backdropOpacity})`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Create window element
|
|
467
|
+
this.element = document.createElement('div');
|
|
468
|
+
this.element.className = 'twm-managed-window';
|
|
469
|
+
if (!this.canDrag) {
|
|
470
|
+
this.element.classList.add('twm-managed-window--no-drag');
|
|
471
|
+
}
|
|
472
|
+
if (this.modal) {
|
|
473
|
+
this.element.classList.add('twm-managed-window--modal');
|
|
474
|
+
}
|
|
475
|
+
this.element.setAttribute('data-window-id', this.id);
|
|
476
|
+
|
|
477
|
+
// Top bar
|
|
478
|
+
const topbar = document.createElement('div');
|
|
479
|
+
topbar.className = 'twm-managed-window__topbar';
|
|
480
|
+
|
|
481
|
+
// Icon
|
|
482
|
+
const icon = document.createElement('span');
|
|
483
|
+
icon.className = 'twm-managed-window__icon material-symbols-outlined';
|
|
484
|
+
icon.textContent = this.icon;
|
|
485
|
+
|
|
486
|
+
const title = document.createElement('div');
|
|
487
|
+
title.className = 'twm-managed-window__title';
|
|
488
|
+
title.textContent = this.title;
|
|
489
|
+
|
|
490
|
+
const buttons = document.createElement('div');
|
|
491
|
+
buttons.className = 'twm-managed-window__buttons';
|
|
492
|
+
|
|
493
|
+
// Minimize button (optional)
|
|
494
|
+
if (this.canMinimize) {
|
|
495
|
+
const minBtn = document.createElement('button');
|
|
496
|
+
minBtn.className = 'twm-managed-window__btn managed-window__btn--minimize';
|
|
497
|
+
minBtn.type = 'button';
|
|
498
|
+
minBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10"><path d="M1 5h8" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
|
|
499
|
+
minBtn.title = 'Minimize';
|
|
500
|
+
minBtn.addEventListener('click', (e) => { e.stopPropagation(); this.minimize(); });
|
|
501
|
+
buttons.appendChild(minBtn);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Maximize button (optional)
|
|
505
|
+
if (this.canMaximize) {
|
|
506
|
+
const maxBtn = document.createElement('button');
|
|
507
|
+
maxBtn.className = 'twm-managed-window__btn managed-window__btn--maximize';
|
|
508
|
+
maxBtn.type = 'button';
|
|
509
|
+
maxBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10"><rect x="1" y="1" width="8" height="8" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
|
|
510
|
+
maxBtn.title = 'Maximize';
|
|
511
|
+
maxBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleMaximize(); });
|
|
512
|
+
buttons.appendChild(maxBtn);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Close button
|
|
516
|
+
const closeBtn = document.createElement('button');
|
|
517
|
+
closeBtn.className = 'twm-managed-window__btn twm-managed-window__btn--close';
|
|
518
|
+
closeBtn.type = 'button';
|
|
519
|
+
closeBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10"><path d="M1 1l8 8M9 1l-8 8" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
|
|
520
|
+
closeBtn.title = 'Close';
|
|
521
|
+
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); });
|
|
522
|
+
buttons.appendChild(closeBtn);
|
|
523
|
+
|
|
524
|
+
topbar.appendChild(icon);
|
|
525
|
+
topbar.appendChild(title);
|
|
526
|
+
topbar.appendChild(buttons);
|
|
527
|
+
|
|
528
|
+
// Content container
|
|
529
|
+
this.contentContainer = document.createElement('div');
|
|
530
|
+
this.contentContainer.className = 'twm-managed-window__content';
|
|
531
|
+
|
|
532
|
+
if (this.content instanceof HTMLElement) {
|
|
533
|
+
this.contentContainer.appendChild(this.content);
|
|
534
|
+
} else if (typeof this.content === 'function') {
|
|
535
|
+
const rendered = this.content();
|
|
536
|
+
if (rendered instanceof HTMLElement) {
|
|
537
|
+
this.contentContainer.appendChild(rendered);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.element.appendChild(topbar);
|
|
542
|
+
this.element.appendChild(this.contentContainer);
|
|
543
|
+
|
|
544
|
+
// Resize handles (only if resizable)
|
|
545
|
+
if (this.canResize) {
|
|
546
|
+
this._addResizeHandles();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Event listeners
|
|
550
|
+
topbar.addEventListener('pointerdown', (e) => this._onTopbarPointerDown(e));
|
|
551
|
+
if (this.canMaximize) {
|
|
552
|
+
topbar.addEventListener('dblclick', () => this.toggleMaximize());
|
|
553
|
+
}
|
|
554
|
+
this.element.addEventListener('pointerdown', () => this.bringToFront());
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_addResizeHandles() {
|
|
558
|
+
const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
|
|
559
|
+
for (const dir of directions) {
|
|
560
|
+
const handle = document.createElement('div');
|
|
561
|
+
handle.className = `twm-managed-window__resize managed-window__resize--${dir}`;
|
|
562
|
+
handle.addEventListener('pointerdown', (e) => this._onResizePointerDown(e, dir));
|
|
563
|
+
this.element.appendChild(handle);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
_applyPosition() {
|
|
568
|
+
if (!this.element) return;
|
|
569
|
+
|
|
570
|
+
// Calculate max available dimensions
|
|
571
|
+
const maxWidth = window.innerWidth;
|
|
572
|
+
const maxHeight = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
|
|
573
|
+
|
|
574
|
+
// Clamp width and height to viewport (but respect minWidth/minHeight)
|
|
575
|
+
this.width = Math.max(this.minWidth, Math.min(this.width, maxWidth));
|
|
576
|
+
this.height = Math.max(this.minHeight, Math.min(this.height, maxHeight));
|
|
577
|
+
|
|
578
|
+
// Ensure window is within viewport (respect top and bottom bars)
|
|
579
|
+
const maxX = Math.max(0, maxWidth - this.width);
|
|
580
|
+
const maxY = Math.max(TOP_BAR_HEIGHT, window.innerHeight - this.height - BOTTOM_BAR_HEIGHT);
|
|
581
|
+
this.x = Math.max(0, Math.min(this.x, maxX));
|
|
582
|
+
this.y = Math.max(TOP_BAR_HEIGHT, Math.min(this.y, maxY));
|
|
583
|
+
|
|
584
|
+
this.element.style.left = `${this.x}px`;
|
|
585
|
+
this.element.style.top = `${this.y}px`;
|
|
586
|
+
this.element.style.width = `${this.width}px`;
|
|
587
|
+
this.element.style.height = `${this.height}px`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_restoreState() {
|
|
591
|
+
// Calculate max available dimensions
|
|
592
|
+
const maxWidth = window.innerWidth;
|
|
593
|
+
const maxHeight = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
|
|
594
|
+
|
|
595
|
+
// Modal windows always center on screen - never restore saved position
|
|
596
|
+
const saved = this.modal ? null : _getWindowState(this.id);
|
|
597
|
+
if (saved) {
|
|
598
|
+
this.x = saved.x ?? this.x;
|
|
599
|
+
this.y = saved.y ?? this.y;
|
|
600
|
+
// Clamp restored dimensions to current viewport
|
|
601
|
+
this.width = Math.min(saved.width ?? this.width, maxWidth);
|
|
602
|
+
this.height = Math.min(saved.height ?? this.height, maxHeight);
|
|
603
|
+
this.isMaximized = saved.maximized ?? false;
|
|
604
|
+
|
|
605
|
+
if (this.isMaximized && this.canMaximize) {
|
|
606
|
+
this._preMaximizeState = { x: saved.x, y: saved.y, width: saved.width, height: saved.height };
|
|
607
|
+
this.x = 0;
|
|
608
|
+
this.y = TOP_BAR_HEIGHT;
|
|
609
|
+
this.width = maxWidth;
|
|
610
|
+
this.height = maxHeight;
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
// Center on screen (between top and bottom bars)
|
|
614
|
+
this.x = Math.max(0, (maxWidth - this.width) / 2);
|
|
615
|
+
this.y = Math.max(TOP_BAR_HEIGHT, TOP_BAR_HEIGHT + (maxHeight - this.height) / 2);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
_saveCurrentState() {
|
|
620
|
+
// Modal windows always center - no need to persist position
|
|
621
|
+
if (this.modal) return;
|
|
622
|
+
|
|
623
|
+
_setWindowState(this.id, {
|
|
624
|
+
x: this._preMaximizeState?.x ?? this.x,
|
|
625
|
+
y: this._preMaximizeState?.y ?? this.y,
|
|
626
|
+
width: this._preMaximizeState?.width ?? this.width,
|
|
627
|
+
height: this._preMaximizeState?.height ?? this.height,
|
|
628
|
+
maximized: this.isMaximized,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ========== Drag Handling ==========
|
|
633
|
+
|
|
634
|
+
_onTopbarPointerDown(e) {
|
|
635
|
+
// Don't start drag if clicking on buttons
|
|
636
|
+
if (e.target.closest('.twm-managed-window__buttons')) return;
|
|
637
|
+
if (this.isMaximized) return;
|
|
638
|
+
if (!this.canDrag) return;
|
|
639
|
+
|
|
640
|
+
e.preventDefault();
|
|
641
|
+
this._dragState = {
|
|
642
|
+
startX: e.clientX,
|
|
643
|
+
startY: e.clientY,
|
|
644
|
+
startWinX: this.x,
|
|
645
|
+
startWinY: this.y,
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
document.addEventListener('pointermove', this._boundOnPointerMove);
|
|
649
|
+
document.addEventListener('pointerup', this._boundOnPointerUp);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
_onPointerMove(e) {
|
|
653
|
+
if (this._dragState) {
|
|
654
|
+
const dx = e.clientX - this._dragState.startX;
|
|
655
|
+
const dy = e.clientY - this._dragState.startY;
|
|
656
|
+
this.x = this._dragState.startWinX + dx;
|
|
657
|
+
this.y = this._dragState.startWinY + dy;
|
|
658
|
+
this._applyPosition();
|
|
659
|
+
} else if (this._resizeState) {
|
|
660
|
+
this._handleResize(e);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
_onPointerUp() {
|
|
665
|
+
if (this._dragState || this._resizeState) {
|
|
666
|
+
this._saveCurrentState();
|
|
667
|
+
}
|
|
668
|
+
this._dragState = null;
|
|
669
|
+
this._resizeState = null;
|
|
670
|
+
document.removeEventListener('pointermove', this._boundOnPointerMove);
|
|
671
|
+
document.removeEventListener('pointerup', this._boundOnPointerUp);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ========== Resize Handling ==========
|
|
675
|
+
|
|
676
|
+
_onResizePointerDown(e, direction) {
|
|
677
|
+
if (this.isMaximized) return;
|
|
678
|
+
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
e.stopPropagation();
|
|
681
|
+
|
|
682
|
+
this._resizeState = {
|
|
683
|
+
direction,
|
|
684
|
+
startX: e.clientX,
|
|
685
|
+
startY: e.clientY,
|
|
686
|
+
startWinX: this.x,
|
|
687
|
+
startWinY: this.y,
|
|
688
|
+
startWidth: this.width,
|
|
689
|
+
startHeight: this.height,
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
document.addEventListener('pointermove', this._boundOnPointerMove);
|
|
693
|
+
document.addEventListener('pointerup', this._boundOnPointerUp);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_handleResize(e) {
|
|
697
|
+
const state = this._resizeState;
|
|
698
|
+
if (!state) return;
|
|
699
|
+
|
|
700
|
+
const dx = e.clientX - state.startX;
|
|
701
|
+
const dy = e.clientY - state.startY;
|
|
702
|
+
const dir = state.direction;
|
|
703
|
+
|
|
704
|
+
// Calculate max available dimensions
|
|
705
|
+
const maxWidth = window.innerWidth;
|
|
706
|
+
const maxHeight = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
|
|
707
|
+
|
|
708
|
+
let newX = state.startWinX;
|
|
709
|
+
let newY = state.startWinY;
|
|
710
|
+
let newW = state.startWidth;
|
|
711
|
+
let newH = state.startHeight;
|
|
712
|
+
|
|
713
|
+
// Handle horizontal resize
|
|
714
|
+
if (dir.includes('e')) {
|
|
715
|
+
newW = Math.max(this.minWidth, Math.min(state.startWidth + dx, maxWidth - newX));
|
|
716
|
+
}
|
|
717
|
+
if (dir.includes('w')) {
|
|
718
|
+
const maxDx = state.startWidth - this.minWidth;
|
|
719
|
+
const actualDx = Math.min(dx, maxDx);
|
|
720
|
+
newX = Math.max(0, state.startWinX + actualDx);
|
|
721
|
+
newW = state.startWidth - (newX - state.startWinX);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Handle vertical resize
|
|
725
|
+
if (dir.includes('s')) {
|
|
726
|
+
newH = Math.max(this.minHeight, Math.min(state.startHeight + dy, maxHeight - (newY - TOP_BAR_HEIGHT)));
|
|
727
|
+
}
|
|
728
|
+
if (dir.includes('n')) {
|
|
729
|
+
const maxDy = state.startHeight - this.minHeight;
|
|
730
|
+
const actualDy = Math.min(dy, maxDy);
|
|
731
|
+
newY = Math.max(TOP_BAR_HEIGHT, state.startWinY + actualDy);
|
|
732
|
+
newH = state.startHeight - (newY - state.startWinY);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
this.x = newX;
|
|
736
|
+
this.y = newY;
|
|
737
|
+
this.width = newW;
|
|
738
|
+
this.height = newH;
|
|
739
|
+
this._applyPosition();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ========== Keyboard Handling ==========
|
|
743
|
+
|
|
744
|
+
_onKeyDown(e) {
|
|
745
|
+
if (!this.isVisible || this.isMinimized) return;
|
|
746
|
+
// Only the top-most window owns global keydowns — otherwise a
|
|
747
|
+
// background window would steal focus from an open modal.
|
|
748
|
+
const topWindow = Array.from(_activeWindows.values())
|
|
749
|
+
.filter(w => w.isVisible && !w.isMinimized)
|
|
750
|
+
.sort((a, b) => b.zIndex - a.zIndex)[0];
|
|
751
|
+
if (topWindow !== this) return;
|
|
752
|
+
|
|
753
|
+
if (e.key === 'Escape') {
|
|
754
|
+
this.close();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
// Focus trap for modals: Tab and Shift+Tab cycle through the
|
|
758
|
+
// focusable elements inside the modal's frame instead of
|
|
759
|
+
// escaping to underlying tiles. Without this, Tab moves the
|
|
760
|
+
// active element into the background — bad UX, and a
|
|
761
|
+
// genuine WCAG focus-trap violation for accessibility tools.
|
|
762
|
+
if (e.key === 'Tab' && this.modal && this.element) {
|
|
763
|
+
const focusables = _collectFocusable(this.element);
|
|
764
|
+
if (focusables.length === 0) {
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const first = focusables[0];
|
|
769
|
+
const last = focusables[focusables.length - 1];
|
|
770
|
+
const active = document.activeElement;
|
|
771
|
+
if (e.shiftKey) {
|
|
772
|
+
if (active === first || !this.element.contains(active)) {
|
|
773
|
+
e.preventDefault();
|
|
774
|
+
last.focus();
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
if (active === last || !this.element.contains(active)) {
|
|
778
|
+
e.preventDefault();
|
|
779
|
+
first.focus();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ========== Static Methods ==========
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Get a window by ID.
|
|
789
|
+
*/
|
|
790
|
+
static get(id) {
|
|
791
|
+
return _activeWindows.get(id) || null;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Restore a minimized window by ID.
|
|
796
|
+
*/
|
|
797
|
+
static restore(id) {
|
|
798
|
+
const win = _activeWindows.get(id);
|
|
799
|
+
if (win && win.isMinimized) {
|
|
800
|
+
win._restore();
|
|
801
|
+
win.bringToFront();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|