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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* action_dropdown.js
|
|
3
|
+
*
|
|
4
|
+
* Reusable dropdown component for action buttons (Add Scenario, Add Widget, etc).
|
|
5
|
+
* Handles opening, closing, auto-positioning, keyboard navigation, and outside clicks.
|
|
6
|
+
*
|
|
7
|
+
* Auto-positioning: measures available viewport space around the trigger element
|
|
8
|
+
* and picks the best direction (above/below) and alignment (left/right).
|
|
9
|
+
* Adds scrolling when the menu would exceed available vertical space.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Viewport margin in px — dropdown stays this far from edges. */
|
|
13
|
+
const EDGE_MARGIN = 8;
|
|
14
|
+
/** Gap between trigger and dropdown in px. */
|
|
15
|
+
const GAP = 8;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ActionDropdown - A reusable dropdown component with smart auto-positioning.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* ```javascript
|
|
22
|
+
* const dropdown = new ActionDropdown({
|
|
23
|
+
* trigger: buttonElement,
|
|
24
|
+
* options: [
|
|
25
|
+
* { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },
|
|
26
|
+
* { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }
|
|
27
|
+
* ],
|
|
28
|
+
* onSelect: (option) => console.log('Selected:', option.type),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Control programmatically
|
|
32
|
+
* dropdown.open();
|
|
33
|
+
* dropdown.close();
|
|
34
|
+
* dropdown.toggle();
|
|
35
|
+
* dropdown.destroy();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class ActionDropdown {
|
|
39
|
+
/**
|
|
40
|
+
* @param {Object} config
|
|
41
|
+
* @param {HTMLElement} config.trigger - Button element that triggers the dropdown
|
|
42
|
+
* @param {Array} config.options - Array of option objects with { type, label, icon, description? }
|
|
43
|
+
* @param {Function} config.onSelect - Callback when an option is selected
|
|
44
|
+
* @param {string} [config.className=''] - Additional CSS class for the dropdown
|
|
45
|
+
* @param {string} [config.menuId] - Optional ID for the menu element
|
|
46
|
+
*/
|
|
47
|
+
constructor(config) {
|
|
48
|
+
this.trigger = config.trigger;
|
|
49
|
+
this.options = config.options || [];
|
|
50
|
+
this.onSelect = config.onSelect;
|
|
51
|
+
this.className = config.className || '';
|
|
52
|
+
this.menuId = config.menuId;
|
|
53
|
+
|
|
54
|
+
this.isOpen = false;
|
|
55
|
+
this.menuEl = null;
|
|
56
|
+
|
|
57
|
+
this._boundHandleDocumentClick = this._handleDocumentClick.bind(this);
|
|
58
|
+
this._boundHandleKeydown = this._handleKeydown.bind(this);
|
|
59
|
+
this._boundHandleTriggerClick = this._handleTriggerClick.bind(this);
|
|
60
|
+
|
|
61
|
+
this._init();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize the dropdown.
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
_init() {
|
|
69
|
+
if (!this.trigger) {
|
|
70
|
+
console.warn('[ActionDropdown] No trigger element provided');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._createMenu();
|
|
75
|
+
this.trigger.addEventListener('click', this._boundHandleTriggerClick);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create the dropdown menu element.
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_createMenu() {
|
|
83
|
+
const menu = document.createElement('div');
|
|
84
|
+
menu.className = `twm-action-dropdown-menu ${this.className}`.trim();
|
|
85
|
+
menu.setAttribute('role', 'menu');
|
|
86
|
+
menu.hidden = true;
|
|
87
|
+
|
|
88
|
+
if (this.menuId) {
|
|
89
|
+
menu.id = this.menuId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._renderOptions(menu);
|
|
93
|
+
|
|
94
|
+
// Append to body so position:fixed works without clipping.
|
|
95
|
+
document.body.appendChild(menu);
|
|
96
|
+
|
|
97
|
+
this.menuEl = menu;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render option buttons into a container element.
|
|
102
|
+
* @param {HTMLElement} container
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_renderOptions(container) {
|
|
106
|
+
container.innerHTML = '';
|
|
107
|
+
this.options.forEach((opt, index) => {
|
|
108
|
+
const optionBtn = document.createElement('button');
|
|
109
|
+
optionBtn.type = 'button';
|
|
110
|
+
optionBtn.className = 'twm-action-dropdown-option';
|
|
111
|
+
optionBtn.dataset.index = index;
|
|
112
|
+
optionBtn.dataset.type = opt.type;
|
|
113
|
+
optionBtn.setAttribute('role', 'menuitem');
|
|
114
|
+
|
|
115
|
+
const iconHtml = opt.icon
|
|
116
|
+
? `<span class="material-symbols-outlined twm-option-icon">${opt.icon}</span>`
|
|
117
|
+
: '';
|
|
118
|
+
|
|
119
|
+
const descHtml = opt.description
|
|
120
|
+
? `<span class="twm-option-desc">${opt.description}</span>`
|
|
121
|
+
: '';
|
|
122
|
+
|
|
123
|
+
optionBtn.innerHTML = `
|
|
124
|
+
${iconHtml}
|
|
125
|
+
<span class="option-text">
|
|
126
|
+
<span class="twm-option-label">${opt.label}</span>
|
|
127
|
+
${descHtml}
|
|
128
|
+
</span>
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
optionBtn.addEventListener('click', (e) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
this._selectOption(opt);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
container.appendChild(optionBtn);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle trigger button click.
|
|
143
|
+
* @param {MouseEvent} e
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
_handleTriggerClick(e) {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
this.toggle();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Toggle the dropdown open/closed.
|
|
154
|
+
* @param {boolean} [forceState] - Optional forced state
|
|
155
|
+
*/
|
|
156
|
+
toggle(forceState) {
|
|
157
|
+
const shouldOpen = typeof forceState === 'boolean' ? forceState : !this.isOpen;
|
|
158
|
+
if (shouldOpen) {
|
|
159
|
+
this.open();
|
|
160
|
+
} else {
|
|
161
|
+
this.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Open the dropdown with auto-positioning.
|
|
167
|
+
*/
|
|
168
|
+
open() {
|
|
169
|
+
if (this.isOpen || !this.menuEl) return;
|
|
170
|
+
|
|
171
|
+
this.isOpen = true;
|
|
172
|
+
this.menuEl.hidden = false;
|
|
173
|
+
|
|
174
|
+
// Position against trigger
|
|
175
|
+
ActionDropdown.position(this.trigger, this.menuEl);
|
|
176
|
+
|
|
177
|
+
// Animate in
|
|
178
|
+
requestAnimationFrame(() => {
|
|
179
|
+
this.menuEl.classList.add('visible');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Update trigger state
|
|
183
|
+
this.trigger?.classList.add('twm-is-open');
|
|
184
|
+
|
|
185
|
+
// Add document listeners
|
|
186
|
+
document.addEventListener('click', this._boundHandleDocumentClick, true);
|
|
187
|
+
document.addEventListener('keydown', this._boundHandleKeydown);
|
|
188
|
+
|
|
189
|
+
// Focus first option for keyboard accessibility
|
|
190
|
+
const firstOption = this.menuEl.querySelector('.twm-action-dropdown-option');
|
|
191
|
+
firstOption?.focus();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Close the dropdown.
|
|
196
|
+
*/
|
|
197
|
+
close() {
|
|
198
|
+
if (!this.isOpen) return;
|
|
199
|
+
|
|
200
|
+
this.isOpen = false;
|
|
201
|
+
|
|
202
|
+
if (this.menuEl) {
|
|
203
|
+
this.menuEl.classList.remove('visible');
|
|
204
|
+
this.menuEl.hidden = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update trigger state
|
|
208
|
+
this.trigger?.classList.remove('twm-is-open');
|
|
209
|
+
|
|
210
|
+
// Remove document listeners
|
|
211
|
+
document.removeEventListener('click', this._boundHandleDocumentClick, true);
|
|
212
|
+
document.removeEventListener('keydown', this._boundHandleKeydown);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle document click (close on outside click).
|
|
217
|
+
* @param {MouseEvent} e
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_handleDocumentClick(e) {
|
|
221
|
+
if (!this.isOpen) return;
|
|
222
|
+
|
|
223
|
+
// Don't close if clicking inside dropdown or trigger
|
|
224
|
+
if (this.menuEl?.contains(e.target) || this.trigger?.contains(e.target)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.close();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handle keyboard events.
|
|
233
|
+
* @param {KeyboardEvent} e
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_handleKeydown(e) {
|
|
237
|
+
if (e.key === 'Escape') {
|
|
238
|
+
this.close();
|
|
239
|
+
this.trigger?.focus();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Arrow key navigation
|
|
244
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Navigate options with arrow keys.
|
|
252
|
+
* @param {number} direction - 1 for down, -1 for up
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
_navigateOptions(direction) {
|
|
256
|
+
const options = this.menuEl?.querySelectorAll('.twm-action-dropdown-option');
|
|
257
|
+
if (!options || options.length === 0) return;
|
|
258
|
+
|
|
259
|
+
const currentIndex = Array.from(options).findIndex(opt => opt === document.activeElement);
|
|
260
|
+
let nextIndex = currentIndex + direction;
|
|
261
|
+
|
|
262
|
+
if (nextIndex < 0) nextIndex = options.length - 1;
|
|
263
|
+
if (nextIndex >= options.length) nextIndex = 0;
|
|
264
|
+
|
|
265
|
+
options[nextIndex]?.focus();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Handle option selection.
|
|
270
|
+
* @param {Object} option - The selected option
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
_selectOption(option) {
|
|
274
|
+
this.close();
|
|
275
|
+
|
|
276
|
+
if (this.onSelect) {
|
|
277
|
+
this.onSelect(option);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Update the options dynamically.
|
|
283
|
+
* @param {Array} newOptions - New options array
|
|
284
|
+
*/
|
|
285
|
+
setOptions(newOptions) {
|
|
286
|
+
this.options = newOptions;
|
|
287
|
+
if (this.menuEl) {
|
|
288
|
+
this._renderOptions(this.menuEl);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Destroy the dropdown and clean up.
|
|
294
|
+
*/
|
|
295
|
+
destroy() {
|
|
296
|
+
this.close();
|
|
297
|
+
|
|
298
|
+
// Remove trigger listener
|
|
299
|
+
this.trigger?.removeEventListener('click', this._boundHandleTriggerClick);
|
|
300
|
+
|
|
301
|
+
// Remove menu element
|
|
302
|
+
this.menuEl?.remove();
|
|
303
|
+
this.menuEl = null;
|
|
304
|
+
|
|
305
|
+
this.trigger = null;
|
|
306
|
+
this.options = [];
|
|
307
|
+
this.onSelect = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// =========================================================================
|
|
311
|
+
// STATIC — shared auto-positioning for any trigger + menu pair
|
|
312
|
+
// =========================================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Position a dropdown menu relative to a trigger element.
|
|
316
|
+
* Uses `position: fixed` to avoid overflow clipping.
|
|
317
|
+
* Picks above/below based on available space, aligns left/right edge,
|
|
318
|
+
* and applies `max-height` + `overflow-y: auto` when the menu is taller
|
|
319
|
+
* than the available space.
|
|
320
|
+
*
|
|
321
|
+
* @param {HTMLElement} trigger - The element the dropdown opens from
|
|
322
|
+
* @param {HTMLElement} menu - The dropdown menu element to position
|
|
323
|
+
*/
|
|
324
|
+
static position(trigger, menu) {
|
|
325
|
+
if (!trigger || !menu) return;
|
|
326
|
+
|
|
327
|
+
// Reset any prior inline position so natural size can be measured.
|
|
328
|
+
Object.assign(menu.style, {
|
|
329
|
+
position: 'fixed',
|
|
330
|
+
top: 'auto',
|
|
331
|
+
bottom: 'auto',
|
|
332
|
+
left: 'auto',
|
|
333
|
+
right: 'auto',
|
|
334
|
+
maxHeight: '',
|
|
335
|
+
overflowY: ''
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
339
|
+
const menuRect = menu.getBoundingClientRect();
|
|
340
|
+
const vw = window.innerWidth;
|
|
341
|
+
const vh = window.innerHeight;
|
|
342
|
+
|
|
343
|
+
// ── Vertical: prefer below, flip above if insufficient room ───────
|
|
344
|
+
const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;
|
|
345
|
+
const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;
|
|
346
|
+
|
|
347
|
+
let top;
|
|
348
|
+
let maxHeight;
|
|
349
|
+
|
|
350
|
+
if (menuRect.height <= spaceBelow) {
|
|
351
|
+
// Fits below — use natural height.
|
|
352
|
+
top = triggerRect.bottom + GAP;
|
|
353
|
+
maxHeight = spaceBelow;
|
|
354
|
+
} else if (menuRect.height <= spaceAbove) {
|
|
355
|
+
// Fits above — use natural height.
|
|
356
|
+
top = triggerRect.top - GAP - menuRect.height;
|
|
357
|
+
maxHeight = spaceAbove;
|
|
358
|
+
} else if (spaceBelow >= spaceAbove) {
|
|
359
|
+
// More space below — scroll.
|
|
360
|
+
top = triggerRect.bottom + GAP;
|
|
361
|
+
maxHeight = spaceBelow;
|
|
362
|
+
} else {
|
|
363
|
+
// More space above — scroll.
|
|
364
|
+
maxHeight = spaceAbove;
|
|
365
|
+
top = EDGE_MARGIN;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Horizontal: align left-edge with trigger, flip if off-screen ──
|
|
369
|
+
let left = triggerRect.left;
|
|
370
|
+
|
|
371
|
+
if (left + menuRect.width > vw - EDGE_MARGIN) {
|
|
372
|
+
// Align right edges instead.
|
|
373
|
+
left = triggerRect.right - menuRect.width;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Still off-screen? Clamp to edges.
|
|
377
|
+
left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));
|
|
378
|
+
|
|
379
|
+
Object.assign(menu.style, {
|
|
380
|
+
position: 'fixed',
|
|
381
|
+
top: `${Math.round(top)}px`,
|
|
382
|
+
left: `${Math.round(left)}px`,
|
|
383
|
+
maxHeight: `${Math.round(maxHeight)}px`,
|
|
384
|
+
overflowY: 'auto'
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export default ActionDropdown;
|