flexdesk 0.4.7 → 0.6.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/README.md +1036 -301
- package/css/base.css +2958 -2
- package/css/flexdesk.css +2962 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +86 -0
- package/dist/canvas.js +390 -0
- package/dist/canvas.js.map +7 -0
- package/dist/charts.js +782 -782
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-ELXVW542.js → chunk-6NG7YVBG.js} +3 -471
- package/dist/chunk-6NG7YVBG.js.map +7 -0
- package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
- package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
- package/dist/chunk-H4LT43RS.js +475 -0
- package/dist/chunk-H4LT43RS.js.map +7 -0
- package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/chunk-SN4LSOAR.js +532 -0
- package/dist/chunk-SN4LSOAR.js.map +7 -0
- package/dist/flexdesk.css +2962 -4
- package/dist/flow.js +10030 -0
- package/dist/flow.js.map +7 -0
- package/dist/tiles.js +2195 -2195
- package/dist/tokens.css +86 -0
- package/dist/widgets.js +5824 -5796
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +8077 -8074
- package/dist/wm.js.map +7 -7
- package/package.json +89 -81
- package/src/canvas/canvas_adapter.js +523 -0
- package/src/canvas/edge_router.js +392 -0
- package/src/canvas/graph/connector_router.js +189 -0
- package/src/canvas/graph/history_service.js +170 -0
- package/src/canvas/graph/node_platform.js +116 -0
- package/src/canvas/index.js +16 -0
- package/src/flow/canvas_editor/editor.js +1081 -0
- package/src/flow/canvas_editor/index.js +16 -0
- package/src/flow/canvas_editor/layout.js +118 -0
- package/src/flow/canvas_editor/strings.js +37 -0
- package/src/flow/kit/always_before.js +325 -0
- package/src/flow/kit/catalogue.js +170 -0
- package/src/flow/kit/chip_input.js +461 -0
- package/src/flow/kit/dom.js +205 -0
- package/src/flow/kit/findings.js +129 -0
- package/src/flow/kit/history.js +173 -0
- package/src/flow/kit/index.js +38 -0
- package/src/flow/kit/keys.js +127 -0
- package/src/flow/kit/logic_graph.js +210 -0
- package/src/flow/kit/references.js +199 -0
- package/src/flow/kit/settings_panel.js +357 -0
- package/src/flow/kit/settings_schema.js +175 -0
- package/src/flow/kit/step_picker.js +209 -0
- package/src/flow/kit/strings.js +102 -0
- package/src/flow/kit/value_picker.js +238 -0
- package/src/flow/kit/widgets.js +763 -0
- package/src/flow/lanes/editor.js +1469 -0
- package/src/flow/lanes/index.js +26 -0
- package/src/flow/lanes/layout.js +525 -0
- package/src/flow/lanes/pipeline.js +327 -0
- package/src/flow/lanes/preview.js +109 -0
- package/src/flow/lanes/strings.js +98 -0
- package/src/flow/outline/editor.js +1511 -0
- package/src/flow/outline/index.js +20 -0
- package/src/flow/outline/mapping.js +286 -0
- package/src/flow/outline/menu.js +154 -0
- package/src/flow/outline/operations.js +738 -0
- package/src/flow/outline/recognise.js +755 -0
- package/src/flow/outline/strings.js +135 -0
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/wm.js +3916 -3916
- package/src/ui/components/data_table.js +4205 -3294
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-ELXVW542.js.map +0 -7
- package/dist/chunk-WMGOJ3I6.js.map +0 -7
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
modalHost
|
|
3
|
+
} from "./chunk-6NG7YVBG.js";
|
|
4
|
+
|
|
5
|
+
// src/help/help_registry.js
|
|
6
|
+
var EMPTY_PROVIDER = Object.freeze({
|
|
7
|
+
getCategories: () => [],
|
|
8
|
+
getTopicsByCategory: () => [],
|
|
9
|
+
loadTopic: async () => null,
|
|
10
|
+
searchTopics: () => []
|
|
11
|
+
});
|
|
12
|
+
var DEFAULT_COPY = Object.freeze({
|
|
13
|
+
title: "Help",
|
|
14
|
+
welcome: "Select a topic from the sidebar or browse the categories below."
|
|
15
|
+
});
|
|
16
|
+
var _provider = EMPTY_PROVIDER;
|
|
17
|
+
var _categories = Object.freeze({});
|
|
18
|
+
var _copy = DEFAULT_COPY;
|
|
19
|
+
function setHelpProvider(provider, categories = {}, copy = {}) {
|
|
20
|
+
_provider = provider ?? EMPTY_PROVIDER;
|
|
21
|
+
_categories = Object.freeze({ ...categories });
|
|
22
|
+
_copy = Object.freeze({ ...DEFAULT_COPY, ...copy });
|
|
23
|
+
}
|
|
24
|
+
function helpProvider() {
|
|
25
|
+
return _provider;
|
|
26
|
+
}
|
|
27
|
+
function helpCategories() {
|
|
28
|
+
return _categories;
|
|
29
|
+
}
|
|
30
|
+
function helpCopy() {
|
|
31
|
+
return _copy;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/help/help_modal.js
|
|
35
|
+
var _activeModal = null;
|
|
36
|
+
var HelpModal = class _HelpModal {
|
|
37
|
+
/** The application's help, resolved at use time. */
|
|
38
|
+
get _service() {
|
|
39
|
+
return helpProvider();
|
|
40
|
+
}
|
|
41
|
+
get _categories() {
|
|
42
|
+
return helpCategories();
|
|
43
|
+
}
|
|
44
|
+
get _copy() {
|
|
45
|
+
return helpCopy();
|
|
46
|
+
}
|
|
47
|
+
constructor() {
|
|
48
|
+
this._overlay = null;
|
|
49
|
+
this._modal = null;
|
|
50
|
+
this._currentTopic = null;
|
|
51
|
+
this._searchQuery = "";
|
|
52
|
+
this._boundKeyHandler = this._handleKeyDown.bind(this);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Open the help modal.
|
|
56
|
+
* @param {string} [topicId] - Initial topic to display
|
|
57
|
+
*/
|
|
58
|
+
async open(topicId = null) {
|
|
59
|
+
if (_activeModal && _activeModal !== this) {
|
|
60
|
+
_activeModal.close();
|
|
61
|
+
}
|
|
62
|
+
_activeModal = this;
|
|
63
|
+
this._createModal();
|
|
64
|
+
document.body.appendChild(this._overlay);
|
|
65
|
+
document.addEventListener("keydown", this._boundKeyHandler);
|
|
66
|
+
const searchInput = this._modal.querySelector(".help-search-input");
|
|
67
|
+
if (searchInput) {
|
|
68
|
+
setTimeout(() => searchInput.focus(), 100);
|
|
69
|
+
}
|
|
70
|
+
if (topicId) {
|
|
71
|
+
await this._loadTopic(topicId);
|
|
72
|
+
} else {
|
|
73
|
+
this._showIndex();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Close the help modal.
|
|
78
|
+
*/
|
|
79
|
+
close() {
|
|
80
|
+
if (this._overlay) {
|
|
81
|
+
document.removeEventListener("keydown", this._boundKeyHandler);
|
|
82
|
+
this._overlay.remove();
|
|
83
|
+
this._overlay = null;
|
|
84
|
+
this._modal = null;
|
|
85
|
+
}
|
|
86
|
+
if (_activeModal === this) {
|
|
87
|
+
_activeModal = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create the modal DOM structure.
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_createModal() {
|
|
95
|
+
this._overlay = document.createElement("div");
|
|
96
|
+
this._overlay.className = "help-modal-overlay";
|
|
97
|
+
this._overlay.addEventListener("click", (e) => {
|
|
98
|
+
if (e.target === this._overlay) this.close();
|
|
99
|
+
});
|
|
100
|
+
this._modal = document.createElement("div");
|
|
101
|
+
this._modal.className = "help-modal";
|
|
102
|
+
this._modal.innerHTML = `
|
|
103
|
+
<div class="help-modal__header">
|
|
104
|
+
<div class="help-modal__title">
|
|
105
|
+
<span class="material-symbols-outlined">help</span>
|
|
106
|
+
<span>Help</span>
|
|
107
|
+
</div>
|
|
108
|
+
<button class="help-modal__close" title="Close (Esc)">
|
|
109
|
+
<span class="material-symbols-outlined">close</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="help-modal__search">
|
|
113
|
+
<span class="material-symbols-outlined">search</span>
|
|
114
|
+
<input type="text" class="help-search-input" placeholder="Search help topics..." autocomplete="off">
|
|
115
|
+
</div>
|
|
116
|
+
<div class="help-modal__body">
|
|
117
|
+
<nav class="help-modal__sidebar">
|
|
118
|
+
<div class="help-sidebar__categories"></div>
|
|
119
|
+
</nav>
|
|
120
|
+
<main class="help-modal__content">
|
|
121
|
+
<div class="help-content__loading">Loading...</div>
|
|
122
|
+
</main>
|
|
123
|
+
</div>
|
|
124
|
+
`;
|
|
125
|
+
const closeBtn = this._modal.querySelector(".help-modal__close");
|
|
126
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
127
|
+
const searchInput = this._modal.querySelector(".help-search-input");
|
|
128
|
+
searchInput.addEventListener("input", (e) => this._handleSearch(e.target.value));
|
|
129
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
130
|
+
if (e.key === "Escape") {
|
|
131
|
+
if (e.target.value) {
|
|
132
|
+
e.target.value = "";
|
|
133
|
+
this._handleSearch("");
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this._buildSidebar();
|
|
139
|
+
this._overlay.appendChild(this._modal);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build the sidebar navigation.
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
_buildSidebar() {
|
|
146
|
+
const container = this._modal.querySelector(".help-sidebar__categories");
|
|
147
|
+
const categories = this._service.getCategories();
|
|
148
|
+
const categoryOrder = Object.keys(this._categories);
|
|
149
|
+
categories.sort((a, b) => {
|
|
150
|
+
const orderA = categoryOrder.indexOf(a);
|
|
151
|
+
const orderB = categoryOrder.indexOf(b);
|
|
152
|
+
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
|
|
153
|
+
});
|
|
154
|
+
container.innerHTML = categories.map((category) => {
|
|
155
|
+
const topics = this._service.getTopicsByCategory(category);
|
|
156
|
+
const label = this._categories[category]?.label || category;
|
|
157
|
+
const icon = this._categories[category]?.icon || "folder";
|
|
158
|
+
return `
|
|
159
|
+
<div class="help-category" data-category="${category}">
|
|
160
|
+
<div class="help-category__header">
|
|
161
|
+
<span class="material-symbols-outlined">${icon}</span>
|
|
162
|
+
<span>${label}</span>
|
|
163
|
+
</div>
|
|
164
|
+
<ul class="help-category__topics">
|
|
165
|
+
${topics.map((topic) => `
|
|
166
|
+
<li class="help-topic-item" data-topic="${topic.id}">
|
|
167
|
+
${topic.title}
|
|
168
|
+
</li>
|
|
169
|
+
`).join("")}
|
|
170
|
+
</ul>
|
|
171
|
+
</div>
|
|
172
|
+
`;
|
|
173
|
+
}).join("");
|
|
174
|
+
container.querySelectorAll(".help-topic-item").forEach((item) => {
|
|
175
|
+
item.addEventListener("click", () => {
|
|
176
|
+
this._loadTopic(item.dataset.topic);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
container.querySelectorAll(".help-category__header").forEach((header) => {
|
|
180
|
+
header.addEventListener("click", () => {
|
|
181
|
+
header.parentElement.classList.toggle("collapsed");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Show the help index page.
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_showIndex() {
|
|
190
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
191
|
+
const categories = this._service.getCategories();
|
|
192
|
+
const categoryOrder = Object.keys(this._categories);
|
|
193
|
+
categories.sort((a, b) => {
|
|
194
|
+
const orderA = categoryOrder.indexOf(a);
|
|
195
|
+
const orderB = categoryOrder.indexOf(b);
|
|
196
|
+
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
|
|
197
|
+
});
|
|
198
|
+
content.innerHTML = `
|
|
199
|
+
<div class="help-index">
|
|
200
|
+
<h1>${this._copy.title}</h1>
|
|
201
|
+
<p>${this._copy.welcome}</p>
|
|
202
|
+
|
|
203
|
+
<div class="help-index__grid">
|
|
204
|
+
${categories.map((category) => {
|
|
205
|
+
const topics = this._service.getTopicsByCategory(category);
|
|
206
|
+
const label = this._categories[category]?.label || category;
|
|
207
|
+
const icon = this._categories[category]?.icon || "folder";
|
|
208
|
+
return `
|
|
209
|
+
<div class="help-index__category" data-category="${category}">
|
|
210
|
+
<div class="help-index__category-header">
|
|
211
|
+
<span class="material-symbols-outlined">${icon}</span>
|
|
212
|
+
<span>${label}</span>
|
|
213
|
+
</div>
|
|
214
|
+
<ul class="help-index__topics">
|
|
215
|
+
${topics.slice(0, 3).map((topic) => `
|
|
216
|
+
<li class="help-index__topic" data-topic="${topic.id}">
|
|
217
|
+
${topic.title}
|
|
218
|
+
</li>
|
|
219
|
+
`).join("")}
|
|
220
|
+
${topics.length > 3 ? `<li class="help-index__more">+${topics.length - 3} more</li>` : ""}
|
|
221
|
+
</ul>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
}).join("")}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
content.querySelectorAll(".help-index__topic").forEach((item) => {
|
|
229
|
+
item.addEventListener("click", () => {
|
|
230
|
+
this._loadTopic(item.dataset.topic);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Load and display a topic.
|
|
236
|
+
* @param {string} topicId - Topic ID
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
async _loadTopic(topicId) {
|
|
240
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
241
|
+
content.innerHTML = '<div class="help-content__loading">Loading...</div>';
|
|
242
|
+
this._modal.querySelectorAll(".help-topic-item").forEach((item) => {
|
|
243
|
+
item.classList.toggle("active", item.dataset.topic === topicId);
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
const topic = await this._service.loadTopic(topicId);
|
|
247
|
+
this._currentTopic = topicId;
|
|
248
|
+
content.innerHTML = `
|
|
249
|
+
<div class="help-content__article">
|
|
250
|
+
<div class="help-content__breadcrumb">
|
|
251
|
+
<a href="#" class="help-breadcrumb__home" title="Help Index">
|
|
252
|
+
<span class="material-symbols-outlined">home</span>
|
|
253
|
+
</a>
|
|
254
|
+
<span class="help-breadcrumb__separator">/</span>
|
|
255
|
+
<span class="help-breadcrumb__title">${topic.title}</span>
|
|
256
|
+
</div>
|
|
257
|
+
<article class="help-article">
|
|
258
|
+
${topic.html}
|
|
259
|
+
</article>
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
content.querySelector(".help-breadcrumb__home").addEventListener("click", (e) => {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
this._showIndex();
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
content.innerHTML = `
|
|
268
|
+
<div class="help-content__error">
|
|
269
|
+
<span class="material-symbols-outlined">error</span>
|
|
270
|
+
<p>Failed to load help topic.</p>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Handle search input.
|
|
277
|
+
* @param {string} query - Search query
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
_handleSearch(query) {
|
|
281
|
+
this._searchQuery = query;
|
|
282
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
283
|
+
if (!query.trim()) {
|
|
284
|
+
this._showIndex();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const results = this._service.searchTopics(query);
|
|
288
|
+
if (results.length === 0) {
|
|
289
|
+
content.innerHTML = `
|
|
290
|
+
<div class="help-search-results">
|
|
291
|
+
<h2>Search Results</h2>
|
|
292
|
+
<p class="help-search-empty">No results found for "${query}"</p>
|
|
293
|
+
</div>
|
|
294
|
+
`;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
content.innerHTML = `
|
|
298
|
+
<div class="help-search-results">
|
|
299
|
+
<h2>Search Results</h2>
|
|
300
|
+
<p class="help-search-count">${results.length} result${results.length !== 1 ? "s" : ""} for "${query}"</p>
|
|
301
|
+
<ul class="help-search-list">
|
|
302
|
+
${results.map((topic) => `
|
|
303
|
+
<li class="help-search-item" data-topic="${topic.id}">
|
|
304
|
+
<span class="help-search-item__title">${topic.title}</span>
|
|
305
|
+
<span class="help-search-item__category">${this._categories[topic.category]?.label || topic.category}</span>
|
|
306
|
+
</li>
|
|
307
|
+
`).join("")}
|
|
308
|
+
</ul>
|
|
309
|
+
</div>
|
|
310
|
+
`;
|
|
311
|
+
content.querySelectorAll(".help-search-item").forEach((item) => {
|
|
312
|
+
item.addEventListener("click", () => {
|
|
313
|
+
this._loadTopic(item.dataset.topic);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Handle keyboard events.
|
|
319
|
+
* @param {KeyboardEvent} event
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
_handleKeyDown(event) {
|
|
323
|
+
if (event.key === "Escape") {
|
|
324
|
+
this.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Static method to open help modal.
|
|
329
|
+
* @param {string} [topicId] - Initial topic
|
|
330
|
+
* @returns {HelpModal}
|
|
331
|
+
*/
|
|
332
|
+
static open(topicId = null) {
|
|
333
|
+
const modal = new _HelpModal();
|
|
334
|
+
modal.open(topicId);
|
|
335
|
+
return modal;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get the currently active modal.
|
|
339
|
+
* @returns {HelpModal|null}
|
|
340
|
+
*/
|
|
341
|
+
static getActive() {
|
|
342
|
+
return _activeModal;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function setupGlobalHelpShortcut() {
|
|
346
|
+
document.addEventListener("keydown", (event) => {
|
|
347
|
+
if (event.key !== "?") return;
|
|
348
|
+
const t = event.target;
|
|
349
|
+
if (t?.closest?.(
|
|
350
|
+
'input, textarea, select, [contenteditable="true"]'
|
|
351
|
+
)) return;
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
if (_activeModal) {
|
|
354
|
+
_activeModal.close();
|
|
355
|
+
} else {
|
|
356
|
+
HelpModal.open();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (typeof document !== "undefined") {
|
|
361
|
+
setupGlobalHelpShortcut();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/ui/components/context_menu.js
|
|
365
|
+
var _activeMenu = null;
|
|
366
|
+
var _returnFocusTo = null;
|
|
367
|
+
function showContextMenu(x, y, items, onAction) {
|
|
368
|
+
hideContextMenu();
|
|
369
|
+
_returnFocusTo = document.activeElement;
|
|
370
|
+
const menu = document.createElement("div");
|
|
371
|
+
menu.className = "twm-context-menu ea-context-menu";
|
|
372
|
+
menu.setAttribute("role", "menu");
|
|
373
|
+
for (const it of items) {
|
|
374
|
+
if (it.separator) {
|
|
375
|
+
const sep = document.createElement("div");
|
|
376
|
+
sep.className = "twm-context-menu__separator";
|
|
377
|
+
sep.setAttribute("role", "separator");
|
|
378
|
+
menu.appendChild(sep);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const row = document.createElement("button");
|
|
382
|
+
row.type = "button";
|
|
383
|
+
row.setAttribute("role", "menuitem");
|
|
384
|
+
let cls = "twm-context-menu-item";
|
|
385
|
+
if (it.danger) cls += " twm-delete-node";
|
|
386
|
+
if (it.disabled) cls += " disabled";
|
|
387
|
+
row.className = cls;
|
|
388
|
+
if (it.disabled) {
|
|
389
|
+
row.disabled = true;
|
|
390
|
+
row.setAttribute("aria-disabled", "true");
|
|
391
|
+
}
|
|
392
|
+
if (it.title) row.title = it.title;
|
|
393
|
+
row.innerHTML = `
|
|
394
|
+
<span class="material-symbols-outlined">${it.icon || ""}</span>
|
|
395
|
+
<span>${escapeHtml(it.label)}</span>
|
|
396
|
+
`;
|
|
397
|
+
if (!it.disabled) {
|
|
398
|
+
row.addEventListener("click", (e) => {
|
|
399
|
+
e.stopPropagation();
|
|
400
|
+
hideContextMenu();
|
|
401
|
+
onAction?.(it.action);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
menu.appendChild(row);
|
|
405
|
+
}
|
|
406
|
+
(modalHost() || document.body).appendChild(menu);
|
|
407
|
+
menu.style.display = "block";
|
|
408
|
+
_activeMenu = menu;
|
|
409
|
+
const rect = menu.getBoundingClientRect();
|
|
410
|
+
const left = Math.min(x, window.innerWidth - rect.width - 8);
|
|
411
|
+
const top = Math.min(y, window.innerHeight - rect.height - 8);
|
|
412
|
+
menu.style.left = `${Math.max(0, left)}px`;
|
|
413
|
+
menu.style.top = `${Math.max(0, top)}px`;
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
document.addEventListener("mousedown", _outsideHandler, { once: true, capture: true });
|
|
416
|
+
}, 0);
|
|
417
|
+
document.addEventListener("keydown", _keyHandler);
|
|
418
|
+
window.addEventListener("scroll", hideContextMenu, { once: true, capture: true });
|
|
419
|
+
_enabledItems(menu)[0]?.focus({ preventScroll: true });
|
|
420
|
+
}
|
|
421
|
+
function hideContextMenu() {
|
|
422
|
+
if (!_activeMenu) return;
|
|
423
|
+
const returnTo = _returnFocusTo;
|
|
424
|
+
const held = _activeMenu.contains(document.activeElement);
|
|
425
|
+
_activeMenu.remove();
|
|
426
|
+
_activeMenu = null;
|
|
427
|
+
_returnFocusTo = null;
|
|
428
|
+
document.removeEventListener("keydown", _keyHandler);
|
|
429
|
+
if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
|
|
430
|
+
}
|
|
431
|
+
function _enabledItems(menu) {
|
|
432
|
+
return [...menu.querySelectorAll(".twm-context-menu-item:not(.disabled)")];
|
|
433
|
+
}
|
|
434
|
+
function _keyHandler(e) {
|
|
435
|
+
if (!_activeMenu) return;
|
|
436
|
+
if (e.key === "Escape") {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
hideContextMenu();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const items = _enabledItems(_activeMenu);
|
|
442
|
+
if (items.length === 0) return;
|
|
443
|
+
const at = items.indexOf(document.activeElement);
|
|
444
|
+
let next = null;
|
|
445
|
+
if (e.key === "ArrowDown") next = items[(at + 1 + items.length) % items.length];
|
|
446
|
+
else if (e.key === "ArrowUp") next = items[(at - 1 + items.length) % items.length];
|
|
447
|
+
else if (e.key === "Home") next = items[0];
|
|
448
|
+
else if (e.key === "End") next = items[items.length - 1];
|
|
449
|
+
if (!next) return;
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
next.focus({ preventScroll: true });
|
|
452
|
+
}
|
|
453
|
+
function _outsideHandler(e) {
|
|
454
|
+
if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();
|
|
455
|
+
}
|
|
456
|
+
function escapeHtml(s) {
|
|
457
|
+
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
458
|
+
"&": "&",
|
|
459
|
+
"<": "<",
|
|
460
|
+
">": ">",
|
|
461
|
+
'"': """,
|
|
462
|
+
"'": "'"
|
|
463
|
+
})[c]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export {
|
|
467
|
+
setHelpProvider,
|
|
468
|
+
helpProvider,
|
|
469
|
+
helpCategories,
|
|
470
|
+
helpCopy,
|
|
471
|
+
HelpModal,
|
|
472
|
+
showContextMenu,
|
|
473
|
+
hideContextMenu
|
|
474
|
+
};
|
|
475
|
+
//# sourceMappingURL=chunk-H4LT43RS.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/help/help_registry.js", "../src/help/help_modal.js", "../src/ui/components/context_menu.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * Where the help modal gets its help.\n *\n * HelpModal used to `import { getHelpService, HELP_TOPICS } from './help_service.js'` \u2014\n * and help_service.js is 249 domain words of EcoAgent prose about sectors,\n * currencies, agents and markets. That import was the last framework -> domain\n * edge in the core: a generic modal reaching into one application's manual.\n *\n * The modal now asks this registry, and the application fills it. If nobody\n * fills it, the modal opens and says there is no help \u2014 which is the correct\n * behaviour for a library that ships no documentation about your app.\n *\n * @typedef {Object} HelpProvider\n * @property {() => string[]} getCategories\n * @property {(category: string) => Array<{id: string, title: string}>} getTopicsByCategory\n * @property {(id: string) => Promise<Object|null>} loadTopic\n * @property {(query: string) => Array<Object>} searchTopics\n *\n * @typedef {Object.<string, {label: string, icon: string}>} HelpCategories\n * Keyed by category id; ITERATION ORDER IS DISPLAY ORDER.\n *\n * @typedef {{title: string, welcome: string}} HelpCopy\n * The landing heading. The modal used to hardcode \"EcoAgent Help\" and\n * \"Welcome to EcoAgent!\".\n */\n\n/** A provider with nothing in it. Not an error \u2014 just an app with no help. */\nconst EMPTY_PROVIDER = Object.freeze({\n getCategories: () => [],\n getTopicsByCategory: () => [],\n loadTopic: async () => null,\n searchTopics: () => [],\n});\n\nconst DEFAULT_COPY = Object.freeze({\n title: 'Help',\n welcome: 'Select a topic from the sidebar or browse the categories below.',\n});\n\nlet _provider = EMPTY_PROVIDER;\nlet _categories = Object.freeze({});\nlet _copy = DEFAULT_COPY;\n\n/**\n * @param {HelpProvider} provider\n * @param {HelpCategories} [categories] Display labels + icons, in display order.\n * @param {Partial<HelpCopy>} [copy] Landing heading and welcome line.\n */\nexport function setHelpProvider(provider, categories = {}, copy = {}) {\n _provider = provider ?? EMPTY_PROVIDER;\n _categories = Object.freeze({ ...categories });\n _copy = Object.freeze({ ...DEFAULT_COPY, ...copy });\n}\n\nexport function helpProvider() {\n return _provider;\n}\n\nexport function helpCategories() {\n return _categories;\n}\n\nexport function helpCopy() {\n return _copy;\n}\n", "/**\n * HelpModal - Modal dialog for displaying help content\n *\n * Features:\n * - Markdown rendering with syntax highlighting\n * - Table of contents navigation\n * - Topic search\n * - Category browsing\n * - Keyboard navigation (Escape to close)\n */\n\nimport { helpProvider, helpCategories, helpCopy } from './help_registry.js';\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// HelpModal Class\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nlet _activeModal = null;\n\nexport class HelpModal {\n /** The application's help, resolved at use time. */\n get _service() { return helpProvider(); }\n\n get _categories() { return helpCategories(); }\n\n get _copy() { return helpCopy(); }\n\n constructor() {\n // Read LAZILY, not here. A HelpModal can be constructed at boot (the\n // command palette holds one) before the application has registered its\n // help, and a constructor read would freeze the empty default forever.\n this._overlay = null;\n this._modal = null;\n this._currentTopic = null;\n this._searchQuery = '';\n this._boundKeyHandler = this._handleKeyDown.bind(this);\n }\n\n /**\n * Open the help modal.\n * @param {string} [topicId] - Initial topic to display\n */\n async open(topicId = null) {\n // Close any existing modal\n if (_activeModal && _activeModal !== this) {\n _activeModal.close();\n }\n _activeModal = this;\n\n this._createModal();\n document.body.appendChild(this._overlay);\n\n // Add keyboard handler\n document.addEventListener('keydown', this._boundKeyHandler);\n\n // Focus search input\n const searchInput = this._modal.querySelector('.help-search-input');\n if (searchInput) {\n setTimeout(() => searchInput.focus(), 100);\n }\n\n // Load initial topic or show index\n if (topicId) {\n await this._loadTopic(topicId);\n } else {\n this._showIndex();\n }\n }\n\n /**\n * Close the help modal.\n */\n close() {\n if (this._overlay) {\n document.removeEventListener('keydown', this._boundKeyHandler);\n this._overlay.remove();\n this._overlay = null;\n this._modal = null;\n }\n if (_activeModal === this) {\n _activeModal = null;\n }\n }\n\n /**\n * Create the modal DOM structure.\n * @private\n */\n _createModal() {\n // Overlay\n this._overlay = document.createElement('div');\n this._overlay.className = 'help-modal-overlay';\n this._overlay.addEventListener('click', (e) => {\n if (e.target === this._overlay) this.close();\n });\n\n // Modal container\n this._modal = document.createElement('div');\n this._modal.className = 'help-modal';\n this._modal.innerHTML = `\n <div class=\"help-modal__header\">\n <div class=\"help-modal__title\">\n <span class=\"material-symbols-outlined\">help</span>\n <span>Help</span>\n </div>\n <button class=\"help-modal__close\" title=\"Close (Esc)\">\n <span class=\"material-symbols-outlined\">close</span>\n </button>\n </div>\n <div class=\"help-modal__search\">\n <span class=\"material-symbols-outlined\">search</span>\n <input type=\"text\" class=\"help-search-input\" placeholder=\"Search help topics...\" autocomplete=\"off\">\n </div>\n <div class=\"help-modal__body\">\n <nav class=\"help-modal__sidebar\">\n <div class=\"help-sidebar__categories\"></div>\n </nav>\n <main class=\"help-modal__content\">\n <div class=\"help-content__loading\">Loading...</div>\n </main>\n </div>\n `;\n\n // Event handlers\n const closeBtn = this._modal.querySelector('.help-modal__close');\n closeBtn.addEventListener('click', () => this.close());\n\n const searchInput = this._modal.querySelector('.help-search-input');\n searchInput.addEventListener('input', (e) => this._handleSearch(e.target.value));\n searchInput.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n if (e.target.value) {\n e.target.value = '';\n this._handleSearch('');\n e.stopPropagation();\n }\n }\n });\n\n // Build sidebar\n this._buildSidebar();\n\n this._overlay.appendChild(this._modal);\n }\n\n /**\n * Build the sidebar navigation.\n * @private\n */\n _buildSidebar() {\n const container = this._modal.querySelector('.help-sidebar__categories');\n const categories = this._service.getCategories();\n\n // Sort categories by order\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n container.innerHTML = categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-category\" data-category=\"${category}\">\n <div class=\"help-category__header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-category__topics\">\n ${topics.map(topic => `\n <li class=\"help-topic-item\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n }).join('');\n\n // Add click handlers\n container.querySelectorAll('.help-topic-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n\n container.querySelectorAll('.help-category__header').forEach(header => {\n header.addEventListener('click', () => {\n header.parentElement.classList.toggle('collapsed');\n });\n });\n }\n\n /**\n * Show the help index page.\n * @private\n */\n _showIndex() {\n const content = this._modal.querySelector('.help-modal__content');\n const categories = this._service.getCategories();\n\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n content.innerHTML = `\n <div class=\"help-index\">\n <h1>${this._copy.title}</h1>\n <p>${this._copy.welcome}</p>\n\n <div class=\"help-index__grid\">\n ${categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-index__category\" data-category=\"${category}\">\n <div class=\"help-index__category-header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-index__topics\">\n ${topics.slice(0, 3).map(topic => `\n <li class=\"help-index__topic\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n ${topics.length > 3 ? `<li class=\"help-index__more\">+${topics.length - 3} more</li>` : ''}\n </ul>\n </div>\n `;\n }).join('')}\n </div>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-index__topic').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Load and display a topic.\n * @param {string} topicId - Topic ID\n * @private\n */\n async _loadTopic(topicId) {\n const content = this._modal.querySelector('.help-modal__content');\n content.innerHTML = '<div class=\"help-content__loading\">Loading...</div>';\n\n // Update sidebar selection\n this._modal.querySelectorAll('.help-topic-item').forEach(item => {\n item.classList.toggle('active', item.dataset.topic === topicId);\n });\n\n try {\n const topic = await this._service.loadTopic(topicId);\n this._currentTopic = topicId;\n\n content.innerHTML = `\n <div class=\"help-content__article\">\n <div class=\"help-content__breadcrumb\">\n <a href=\"#\" class=\"help-breadcrumb__home\" title=\"Help Index\">\n <span class=\"material-symbols-outlined\">home</span>\n </a>\n <span class=\"help-breadcrumb__separator\">/</span>\n <span class=\"help-breadcrumb__title\">${topic.title}</span>\n </div>\n <article class=\"help-article\">\n ${topic.html}\n </article>\n </div>\n `;\n\n // Add breadcrumb handler\n content.querySelector('.help-breadcrumb__home').addEventListener('click', (e) => {\n e.preventDefault();\n this._showIndex();\n });\n } catch (error) {\n content.innerHTML = `\n <div class=\"help-content__error\">\n <span class=\"material-symbols-outlined\">error</span>\n <p>Failed to load help topic.</p>\n </div>\n `;\n }\n }\n\n /**\n * Handle search input.\n * @param {string} query - Search query\n * @private\n */\n _handleSearch(query) {\n this._searchQuery = query;\n const content = this._modal.querySelector('.help-modal__content');\n\n if (!query.trim()) {\n this._showIndex();\n return;\n }\n\n const results = this._service.searchTopics(query);\n\n if (results.length === 0) {\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-empty\">No results found for \"${query}\"</p>\n </div>\n `;\n return;\n }\n\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-count\">${results.length} result${results.length !== 1 ? 's' : ''} for \"${query}\"</p>\n <ul class=\"help-search-list\">\n ${results.map(topic => `\n <li class=\"help-search-item\" data-topic=\"${topic.id}\">\n <span class=\"help-search-item__title\">${topic.title}</span>\n <span class=\"help-search-item__category\">${this._categories[topic.category]?.label || topic.category}</span>\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-search-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Handle keyboard events.\n * @param {KeyboardEvent} event\n * @private\n */\n _handleKeyDown(event) {\n if (event.key === 'Escape') {\n this.close();\n }\n }\n\n /**\n * Static method to open help modal.\n * @param {string} [topicId] - Initial topic\n * @returns {HelpModal}\n */\n static open(topicId = null) {\n const modal = new HelpModal();\n modal.open(topicId);\n return modal;\n }\n\n /**\n * Get the currently active modal.\n * @returns {HelpModal|null}\n */\n static getActive() {\n return _activeModal;\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global Help Shortcut\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Open help modal with the `?` key. (F1 is reserved for top-nav Home.)\n */\nfunction setupGlobalHelpShortcut() {\n document.addEventListener('keydown', (event) => {\n if (event.key !== '?') return;\n // Don't hijack `?` typed into a field (search boxes, editors).\n const t = event.target;\n if (t?.closest?.(\n 'input, textarea, select, [contenteditable=\"true\"]')) return;\n event.preventDefault();\n if (_activeModal) {\n _activeModal.close();\n } else {\n HelpModal.open();\n }\n });\n}\n\n// Initialize global shortcut\nif (typeof document !== 'undefined') {\n setupGlobalHelpShortcut();\n}\n\nexport default HelpModal;\n", "/**\n * context_menu.js \u2014 small popup menu that matches Ecosim's `.context-menu`\n * idiom (see main_new.css line 9713 and ui/js/ui/components/data_table.js).\n *\n * Usage:\n * showContextMenu(x, y, [\n * { label: 'Open', icon: 'open_in_new', action: 'open' },\n * { separator: true },\n * { label: 'Delete', icon: 'delete', action: 'delete', danger: true },\n * ], (action) => { ... });\n *\n * The menu auto-closes on outside click / scroll / Escape.\n *\n * \u2500\u2500 It is operable by keyboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Items were bare `<div>`s with a click listener: no role, no tab stop, no\n * arrow-key movement, and `showContextMenu` never moved focus into the menu.\n * A consumer that opened this from Shift+F10 \u2014 the rail in Tables does \u2014 put a\n * menu on screen that the keyboard could only dismiss. Every command in it was\n * unreachable without a pointer, and nothing said so.\n *\n * So an item is a `<button role=\"menuitem\">`, the first enabled one takes focus\n * when the menu opens, Up/Down/Home/End move between them and wrap, Enter and\n * Space activate, Escape closes, and focus returns to whatever had it before.\n * Disabled items are skipped by the arrows rather than focusable-but-inert.\n * `<button>` rather than a div with `tabindex`: the browser then gives Enter and\n * Space for free and screen readers announce it without further help.\n */\n\nimport { modalHost } from './modal.js';\n\nlet _activeMenu = null;\nlet _returnFocusTo = null;\n\nexport function showContextMenu(x, y, items, onAction) {\n hideContextMenu();\n\n // Whatever had focus when the menu opened gets it back when the menu\n // closes. Without this a keyboard user who presses Escape is returned to\n // `document.body` and has to tab back to where they were.\n _returnFocusTo = document.activeElement;\n\n const menu = document.createElement('div');\n menu.className = 'twm-context-menu ea-context-menu';\n menu.setAttribute('role', 'menu');\n\n for (const it of items) {\n if (it.separator) {\n const sep = document.createElement('div');\n sep.className = 'twm-context-menu__separator';\n sep.setAttribute('role', 'separator');\n menu.appendChild(sep);\n continue;\n }\n const row = document.createElement('button');\n row.type = 'button';\n row.setAttribute('role', 'menuitem');\n let cls = 'twm-context-menu-item';\n if (it.danger) cls += ' twm-delete-node';\n if (it.disabled) cls += ' disabled';\n row.className = cls;\n if (it.disabled) {\n row.disabled = true;\n row.setAttribute('aria-disabled', 'true');\n }\n // A DISABLED ROW WITH NO EXPLANATION IS A DEAD CONTROL. `title` was\n // accepted by callers and rendered by nothing \u2014 the item was built with\n // one, the row silently dropped it, and the user got a greyed line with\n // no way to learn why. It is the same shape as the classes this\n // repository keeps finding: no error, no throw, and invisible to every\n // test that checks the row is disabled.\n if (it.title) row.title = it.title;\n row.innerHTML = `\n <span class=\"material-symbols-outlined\">${it.icon || ''}</span>\n <span>${escapeHtml(it.label)}</span>\n `;\n if (!it.disabled) {\n row.addEventListener('click', (e) => {\n e.stopPropagation();\n hideContextMenu();\n onAction?.(it.action);\n });\n }\n menu.appendChild(row);\n }\n\n // C25. THE SAME QUESTION A MODAL ASKS: which window is the user in? A\n // consumer spanning two browser windows sets the host when its focus moves;\n // null \u2014 every consumer today \u2014 is `document.body`, exactly as before. A\n // menu in the wrong window is worse than a modal in the wrong window,\n // because it is positioned at coordinates from the OTHER one.\n (modalHost() || document.body).appendChild(menu);\n menu.style.display = 'block';\n _activeMenu = menu;\n\n // Position with viewport clamping.\n const rect = menu.getBoundingClientRect();\n const left = Math.min(x, window.innerWidth - rect.width - 8);\n const top = Math.min(y, window.innerHeight - rect.height - 8);\n menu.style.left = `${Math.max(0, left)}px`;\n menu.style.top = `${Math.max(0, top)}px`;\n\n // Close on outside click / scroll / Escape.\n setTimeout(() => {\n document.addEventListener('mousedown', _outsideHandler, { once: true, capture: true });\n }, 0);\n document.addEventListener('keydown', _keyHandler);\n window.addEventListener('scroll', hideContextMenu, { once: true, capture: true });\n\n // Focus the first item the keyboard can actually use. `preventScroll` so a\n // menu opened near the bottom of a long page does not jump it.\n _enabledItems(menu)[0]?.focus({ preventScroll: true });\n}\n\nexport function hideContextMenu() {\n if (!_activeMenu) return;\n const returnTo = _returnFocusTo;\n const held = _activeMenu.contains(document.activeElement);\n _activeMenu.remove();\n _activeMenu = null;\n _returnFocusTo = null;\n document.removeEventListener('keydown', _keyHandler);\n // Only take focus back if the menu still had it. A click elsewhere has\n // already moved focus deliberately and must not be undone.\n if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });\n}\n\nfunction _enabledItems(menu) {\n return [...menu.querySelectorAll('.twm-context-menu-item:not(.disabled)')];\n}\n\n/** Up/Down/Home/End move; the list WRAPS, which is what a menu of four items\n * wants and what every desktop menu does. Enter and Space are the button's\n * own, so they are not bound here. */\nfunction _keyHandler(e) {\n if (!_activeMenu) return;\n if (e.key === 'Escape') { e.preventDefault(); hideContextMenu(); return; }\n const items = _enabledItems(_activeMenu);\n if (items.length === 0) return;\n const at = items.indexOf(document.activeElement);\n let next = null;\n if (e.key === 'ArrowDown') next = items[(at + 1 + items.length) % items.length];\n else if (e.key === 'ArrowUp') next = items[(at - 1 + items.length) % items.length];\n else if (e.key === 'Home') next = items[0];\n else if (e.key === 'End') next = items[items.length - 1];\n if (!next) return;\n e.preventDefault();\n next.focus({ preventScroll: true });\n}\n\nfunction _outsideHandler(e) {\n if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();\n}\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, (c) => ({\n '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''',\n }[c]));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|