goblin-magic 1.0.3
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/.editorconfig +9 -0
- package/.zou-flow +2 -0
- package/eslint.config.js +65 -0
- package/magicNavigation.js +7 -0
- package/package.json +45 -0
- package/widgets/dialog/widget.js +78 -0
- package/widgets/element-helpers/element-has-direct-text.js +9 -0
- package/widgets/element-helpers/is-empty-area-element.js +17 -0
- package/widgets/element-helpers/is-flat-element.js +52 -0
- package/widgets/get-modifiers/get-modifiers.js +34 -0
- package/widgets/input-group/styles.js +74 -0
- package/widgets/input-group/widget.js +24 -0
- package/widgets/magic-action/styles.js +45 -0
- package/widgets/magic-action/widget.js +44 -0
- package/widgets/magic-background/bg-alps.jpg +0 -0
- package/widgets/magic-background/bg-fur.png +0 -0
- package/widgets/magic-background/bg-milkyway.png +0 -0
- package/widgets/magic-background/bg-space.jpg +0 -0
- package/widgets/magic-background/bg-synth.jpg +0 -0
- package/widgets/magic-background/bg-white.png +0 -0
- package/widgets/magic-background/styles.js +81 -0
- package/widgets/magic-background/widget.js +20 -0
- package/widgets/magic-box/styles.js +10 -0
- package/widgets/magic-box/widget.js +28 -0
- package/widgets/magic-box-old/styles.js +111 -0
- package/widgets/magic-box-old/widget.js +30 -0
- package/widgets/magic-button/styles.js +156 -0
- package/widgets/magic-button/widget.js +89 -0
- package/widgets/magic-checkbox/styles.js +116 -0
- package/widgets/magic-checkbox/widget.js +68 -0
- package/widgets/magic-color-field/styles.js +22 -0
- package/widgets/magic-color-field/widget.js +68 -0
- package/widgets/magic-date-field/styles.js +9 -0
- package/widgets/magic-date-field/widget.js +145 -0
- package/widgets/magic-datetime-field/styles.js +11 -0
- package/widgets/magic-datetime-field/widget.js +95 -0
- package/widgets/magic-dialog/styles.js +39 -0
- package/widgets/magic-dialog/widget.js +116 -0
- package/widgets/magic-div/styles.js +22 -0
- package/widgets/magic-div/widget.js +20 -0
- package/widgets/magic-emoji/styles.js +14 -0
- package/widgets/magic-emoji/widget.js +33 -0
- package/widgets/magic-emoji-picker/styles.js +21 -0
- package/widgets/magic-emoji-picker/widget.js +44 -0
- package/widgets/magic-inplace-input/styles.js +55 -0
- package/widgets/magic-inplace-input/widget.js +26 -0
- package/widgets/magic-input/styles.js +50 -0
- package/widgets/magic-input/widget.js +397 -0
- package/widgets/magic-label/styles.js +20 -0
- package/widgets/magic-label/widget.js +24 -0
- package/widgets/magic-navigation/service.js +1306 -0
- package/widgets/magic-navigation/styles.js +103 -0
- package/widgets/magic-navigation/view-context.js +15 -0
- package/widgets/magic-navigation/widget.js +540 -0
- package/widgets/magic-number-field/styles.js +10 -0
- package/widgets/magic-number-field/widget.js +103 -0
- package/widgets/magic-panel/styles.js +61 -0
- package/widgets/magic-panel/widget.js +63 -0
- package/widgets/magic-radio/styles.js +93 -0
- package/widgets/magic-radio/widget.js +74 -0
- package/widgets/magic-scroll/styles.js +22 -0
- package/widgets/magic-scroll/widget.js +20 -0
- package/widgets/magic-select/styles.js +16 -0
- package/widgets/magic-select/widget.js +134 -0
- package/widgets/magic-table/reducer.js +63 -0
- package/widgets/magic-table/styles.js +170 -0
- package/widgets/magic-table/widget.js +627 -0
- package/widgets/magic-tag/styles.js +32 -0
- package/widgets/magic-tag/widget.js +32 -0
- package/widgets/magic-text-field/styles.js +58 -0
- package/widgets/magic-text-field/widget.js +66 -0
- package/widgets/magic-time-field/styles.js +8 -0
- package/widgets/magic-time-field/widget.js +142 -0
- package/widgets/magic-timer/styles.js +30 -0
- package/widgets/magic-timer/widget.js +162 -0
- package/widgets/magic-zen/styles.js +61 -0
- package/widgets/magic-zen/widget.js +42 -0
- package/widgets/main-tabs/styles.js +106 -0
- package/widgets/main-tabs/widget.js +23 -0
- package/widgets/menu/styles.js +156 -0
- package/widgets/menu/test-menu.html +154 -0
- package/widgets/menu/widget.js +575 -0
- package/widgets/movable/widget.js +80 -0
- package/widgets/splitter/styles.js +57 -0
- package/widgets/splitter/widget.js +40 -0
- package/widgets/tab-layout/styles.js +31 -0
- package/widgets/tab-layout/widget.js +59 -0
- package/widgets/with-computed-size/widget.js +52 -0
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const {Elf} = require('xcraft-core-goblin');
|
|
3
|
+
const {
|
|
4
|
+
string,
|
|
5
|
+
object,
|
|
6
|
+
Sculpt,
|
|
7
|
+
array,
|
|
8
|
+
option,
|
|
9
|
+
record,
|
|
10
|
+
boolean,
|
|
11
|
+
} = require('xcraft-core-stones');
|
|
12
|
+
const isEqual = require('lodash/isEqual.js');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {`desktop@${string}@${string}`} DesktopId
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const id = string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {string} id
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
class PanelShape {
|
|
25
|
+
tabIds = array(id);
|
|
26
|
+
currentTabId = option(id);
|
|
27
|
+
lastTabIds = array(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class WindowShape {
|
|
31
|
+
panelIds = array(id);
|
|
32
|
+
dialogIds = array(id);
|
|
33
|
+
activePanelId = id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ViewStateShape {
|
|
37
|
+
serviceId = option(id);
|
|
38
|
+
widget = option(string); // 'serviceId' is required when 'widget' is not defined
|
|
39
|
+
widgetProps = option(object);
|
|
40
|
+
highlighted = boolean;
|
|
41
|
+
parentViewId = option(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ViewState extends Sculpt(ViewStateShape) {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {new (...args: any) => Elf} ElfClass
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @template {(...args: any) => any} T
|
|
52
|
+
* @typedef {T extends (a: any, b: any, ...args: infer P) => any ? P : never} OtherParams
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @template {ElfClass} T
|
|
57
|
+
* @typedef {object} ElfView
|
|
58
|
+
* @property {T} [View.service]
|
|
59
|
+
* @property {`${T["name"]}@${string}`} [View.serviceId]
|
|
60
|
+
* @property {OtherParams<InstanceType<T>["create"]> } [View.serviceArgs]
|
|
61
|
+
* @property {string} [View.widget]
|
|
62
|
+
* @property {object} [View.widgetProps]
|
|
63
|
+
* @property {View} [View.previousView]
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @template {string} T
|
|
68
|
+
* @typedef {object} GoblinView
|
|
69
|
+
* @property {T} [View.service]
|
|
70
|
+
* @property {`${T}@${string}` | T} [View.serviceId]
|
|
71
|
+
* @property {object} [View.serviceArgs]
|
|
72
|
+
* @property {string} [View.widget]
|
|
73
|
+
* @property {object} [View.widgetProps]
|
|
74
|
+
* @property {View} [View.previousView]
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {ElfClass | string} ViewParam
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @template {ViewParam} [T=any]
|
|
83
|
+
* @typedef {T extends string ? GoblinView<T> : T extends ElfClass ? ElfView<T> : never} View
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @typedef {object} Modifiers
|
|
88
|
+
* @property {boolean} shiftKey
|
|
89
|
+
* @property {boolean} ctrlKey
|
|
90
|
+
* @property {boolean} altKey
|
|
91
|
+
* @property {boolean} metaKey
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
class MagicNavigationShape {
|
|
95
|
+
id = id;
|
|
96
|
+
windows = record(id, WindowShape);
|
|
97
|
+
panels = record(id, PanelShape);
|
|
98
|
+
tabs = record(id, ViewStateShape);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {ViewParam} service
|
|
103
|
+
* @returns {string | null}
|
|
104
|
+
*/
|
|
105
|
+
function getServiceName(service) {
|
|
106
|
+
if (!service) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (typeof service === 'string') {
|
|
110
|
+
return service;
|
|
111
|
+
}
|
|
112
|
+
return Elf.goblinName(service);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class MagicNavigationState extends Elf.Sculpt(MagicNavigationShape) {}
|
|
116
|
+
|
|
117
|
+
class MagicNavigationLogic extends Elf.Spirit {
|
|
118
|
+
state = new MagicNavigationState({
|
|
119
|
+
id: undefined,
|
|
120
|
+
windows: {},
|
|
121
|
+
panels: {},
|
|
122
|
+
tabs: {},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
create(id, existingWindowId) {
|
|
126
|
+
let {state} = this;
|
|
127
|
+
state.id = id;
|
|
128
|
+
if (existingWindowId) {
|
|
129
|
+
this.openEmptyWindow(existingWindowId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
change(path, newValue) {
|
|
134
|
+
const {state} = this;
|
|
135
|
+
state._state.set(path, newValue);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {id} windowId
|
|
140
|
+
*/
|
|
141
|
+
handleWindowClosed(windowId) {
|
|
142
|
+
const {state} = this;
|
|
143
|
+
const window = state.windows[windowId];
|
|
144
|
+
for (const panelId of window.panelIds) {
|
|
145
|
+
const panel = state.panels[panelId];
|
|
146
|
+
panel.currentTabId = null;
|
|
147
|
+
for (const tabId of panel.tabIds) {
|
|
148
|
+
delete state.tabs[tabId];
|
|
149
|
+
}
|
|
150
|
+
delete state.panels[panelId];
|
|
151
|
+
}
|
|
152
|
+
delete state.windows[windowId];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {id} windowId
|
|
157
|
+
* @param {id} panelId
|
|
158
|
+
*/
|
|
159
|
+
activatePanel(windowId, panelId) {
|
|
160
|
+
const {state} = this;
|
|
161
|
+
state.windows[windowId].activePanelId = panelId;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {id} windowId
|
|
166
|
+
*/
|
|
167
|
+
openEmptyWindow(windowId) {
|
|
168
|
+
const {state} = this;
|
|
169
|
+
const panelId = `panel@${windowId}@0`;
|
|
170
|
+
state.panels[panelId] = {
|
|
171
|
+
currentTabId: null,
|
|
172
|
+
tabIds: [],
|
|
173
|
+
lastTabIds: [],
|
|
174
|
+
};
|
|
175
|
+
state.windows[windowId] = {
|
|
176
|
+
panelIds: [panelId],
|
|
177
|
+
dialogIds: [],
|
|
178
|
+
activePanelId: panelId,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {typeof this["state"]} state
|
|
184
|
+
* @param {id} windowId
|
|
185
|
+
* @param {id} tabId
|
|
186
|
+
* @param {ViewState} tab
|
|
187
|
+
*/
|
|
188
|
+
_openTabInNewWindow(state, windowId, tabId, tab) {
|
|
189
|
+
state.tabs[tabId] = tab;
|
|
190
|
+
const panelId = `panel@${windowId}@0`;
|
|
191
|
+
state.panels[panelId] = {
|
|
192
|
+
currentTabId: tabId,
|
|
193
|
+
tabIds: [tabId],
|
|
194
|
+
lastTabIds: [],
|
|
195
|
+
};
|
|
196
|
+
state.windows[windowId] = {
|
|
197
|
+
panelIds: [panelId],
|
|
198
|
+
dialogIds: [],
|
|
199
|
+
activePanelId: panelId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {id} windowId
|
|
205
|
+
* @param {id} tabId
|
|
206
|
+
* @param {ViewState} tab
|
|
207
|
+
*/
|
|
208
|
+
openTabInNewWindow(windowId, tabId, tab) {
|
|
209
|
+
const {state} = this;
|
|
210
|
+
this._openTabInNewWindow(state, windowId, tabId, tab);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {id} panelId
|
|
215
|
+
* @param {id} tabId
|
|
216
|
+
* @param {ViewState} tab
|
|
217
|
+
* @param {boolean} activateTab
|
|
218
|
+
*/
|
|
219
|
+
openNewTab(panelId, tabId, tab, activateTab) {
|
|
220
|
+
const {state} = this;
|
|
221
|
+
state.tabs[tabId] = tab;
|
|
222
|
+
state.panels[panelId].tabIds.push(tabId);
|
|
223
|
+
const currentTabId = state.panels[panelId].currentTabId;
|
|
224
|
+
if (activateTab) {
|
|
225
|
+
if (currentTabId) {
|
|
226
|
+
state.panels[panelId].lastTabIds.push(currentTabId);
|
|
227
|
+
}
|
|
228
|
+
state.panels[panelId].currentTabId = tabId;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
highlightTab(tabId) {
|
|
233
|
+
const {state} = this;
|
|
234
|
+
state.tabs[tabId].highlighted = true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {id} panelId
|
|
239
|
+
* @param {id} tabId
|
|
240
|
+
* @param {boolean} keepHistory
|
|
241
|
+
*/
|
|
242
|
+
activateTab(panelId, tabId, keepHistory) {
|
|
243
|
+
const {state} = this;
|
|
244
|
+
const currentTabId = state.panels[panelId].currentTabId;
|
|
245
|
+
if (keepHistory && currentTabId && currentTabId !== tabId) {
|
|
246
|
+
state.panels[panelId].lastTabIds.deleteByValue(currentTabId);
|
|
247
|
+
state.panels[panelId].lastTabIds.push(currentTabId);
|
|
248
|
+
} else {
|
|
249
|
+
state.panels[panelId].lastTabIds = [];
|
|
250
|
+
}
|
|
251
|
+
state.panels[panelId].currentTabId = tabId;
|
|
252
|
+
const tab = state.tabs[tabId];
|
|
253
|
+
if (tab.highlighted) {
|
|
254
|
+
tab.highlighted = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {typeof this["state"]} state
|
|
260
|
+
* @param {id} panelId
|
|
261
|
+
* @param {id} tabId
|
|
262
|
+
*/
|
|
263
|
+
_removeTabAndUpdatePanel(state, panelId, tabId) {
|
|
264
|
+
const panel = state.panels[panelId];
|
|
265
|
+
let panelRemoved = false;
|
|
266
|
+
if (panel.currentTabId === tabId) {
|
|
267
|
+
const newCurrentTabId = (() => {
|
|
268
|
+
if (panel.lastTabIds.length > 0) {
|
|
269
|
+
const lastTabIds = [...panel.lastTabIds];
|
|
270
|
+
const lastTabId = lastTabIds.pop();
|
|
271
|
+
panel.lastTabIds = lastTabIds;
|
|
272
|
+
return lastTabId;
|
|
273
|
+
} else {
|
|
274
|
+
let index = panel.tabIds.indexOf(tabId) + 1;
|
|
275
|
+
if (index >= panel.tabIds.length) {
|
|
276
|
+
index = panel.tabIds.length - 2;
|
|
277
|
+
}
|
|
278
|
+
if (index >= 0) {
|
|
279
|
+
return panel.tabIds[index];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
})();
|
|
284
|
+
|
|
285
|
+
panel.currentTabId = newCurrentTabId;
|
|
286
|
+
|
|
287
|
+
if (!newCurrentTabId) {
|
|
288
|
+
const window = Object.entries(
|
|
289
|
+
state.windows
|
|
290
|
+
).find(([windowId, window]) => window.panelIds.includes(panelId))?.[1];
|
|
291
|
+
if (!window) {
|
|
292
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
293
|
+
}
|
|
294
|
+
if (window.panelIds.length > 1) {
|
|
295
|
+
if (window.activePanelId === panelId) {
|
|
296
|
+
const panelIndex = window.panelIds.indexOf(panelId);
|
|
297
|
+
if (panelIndex < 1) {
|
|
298
|
+
window.activePanelId = window.panelIds[1];
|
|
299
|
+
} else {
|
|
300
|
+
window.activePanelId = window.panelIds[panelIndex - 1];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
window.panelIds.deleteByValue(panelId);
|
|
304
|
+
delete state.panels[panelId];
|
|
305
|
+
panelRemoved = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!panelRemoved) {
|
|
310
|
+
panel.tabIds.deleteByValue(tabId);
|
|
311
|
+
panel.lastTabIds.deleteByValue(tabId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {id} panelId
|
|
317
|
+
* @param {id} tabId
|
|
318
|
+
*/
|
|
319
|
+
_removeTab(panelId, tabId) {
|
|
320
|
+
const {state} = this;
|
|
321
|
+
this._removeTabAndUpdatePanel(state, panelId, tabId);
|
|
322
|
+
delete state.tabs[tabId];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {id} panelId
|
|
327
|
+
* @param {id} tabId
|
|
328
|
+
* @param {id} windowId
|
|
329
|
+
*/
|
|
330
|
+
moveTabToNewWindow(panelId, tabId, windowId) {
|
|
331
|
+
const {state} = this;
|
|
332
|
+
const tab = state.tabs[tabId];
|
|
333
|
+
this._removeTabAndUpdatePanel(state, panelId, tabId);
|
|
334
|
+
this._openTabInNewWindow(state, windowId, tabId, tab);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {id} tabId
|
|
339
|
+
* @param {id} panelId
|
|
340
|
+
* @param {id} windowId
|
|
341
|
+
* @param {boolean} nextPanel
|
|
342
|
+
*/
|
|
343
|
+
moveTabToPanel(tabId, panelId, windowId, nextPanel) {
|
|
344
|
+
const {state} = this;
|
|
345
|
+
const window = state.windows[windowId];
|
|
346
|
+
const panelIds = window.panelIds;
|
|
347
|
+
let index = panelIds.indexOf(panelId);
|
|
348
|
+
let newPanel = false;
|
|
349
|
+
if (!nextPanel) {
|
|
350
|
+
index--;
|
|
351
|
+
if (index < 0) {
|
|
352
|
+
newPanel = true;
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
index++;
|
|
356
|
+
if (index >= panelIds.length) {
|
|
357
|
+
newPanel = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
let newPanelId;
|
|
361
|
+
if (newPanel) {
|
|
362
|
+
let i = 0;
|
|
363
|
+
do {
|
|
364
|
+
newPanelId = `panel@${windowId}@${i++}`;
|
|
365
|
+
} while (panelIds.includes(newPanelId));
|
|
366
|
+
if (nextPanel) {
|
|
367
|
+
panelIds.push(newPanelId);
|
|
368
|
+
} else {
|
|
369
|
+
// panelIds.unshift(newPanelId);
|
|
370
|
+
window.panelIds = [newPanelId, ...panelIds];
|
|
371
|
+
}
|
|
372
|
+
state.panels[newPanelId] = {
|
|
373
|
+
currentTabId: tabId,
|
|
374
|
+
tabIds: [tabId],
|
|
375
|
+
lastTabIds: [],
|
|
376
|
+
};
|
|
377
|
+
} else {
|
|
378
|
+
newPanelId = panelIds[index];
|
|
379
|
+
state.panels[newPanelId].tabIds.push(tabId);
|
|
380
|
+
state.panels[newPanelId].currentTabId = tabId;
|
|
381
|
+
state.panels[newPanelId].lastTabIds = [];
|
|
382
|
+
}
|
|
383
|
+
this._removeTabAndUpdatePanel(state, panelId, tabId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {id} windowId
|
|
388
|
+
* @param {id} dialogId
|
|
389
|
+
* @param {ViewState} dialog
|
|
390
|
+
*/
|
|
391
|
+
openDialog(windowId, dialogId, dialog) {
|
|
392
|
+
const {state} = this;
|
|
393
|
+
state.tabs[dialogId] = dialog;
|
|
394
|
+
state.windows[windowId].dialogIds.push(dialogId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param {id} windowId
|
|
399
|
+
* @param {id} dialogId
|
|
400
|
+
*/
|
|
401
|
+
_removeDialog(windowId, dialogId) {
|
|
402
|
+
const {state} = this;
|
|
403
|
+
const window = state.windows[windowId];
|
|
404
|
+
window.dialogIds.deleteByValue(dialogId);
|
|
405
|
+
delete state.tabs[dialogId];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @param {id} viewId
|
|
410
|
+
* @param {ViewState} view
|
|
411
|
+
*/
|
|
412
|
+
replace(viewId, view) {
|
|
413
|
+
const {state} = this;
|
|
414
|
+
state.tabs[viewId] = view;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
class MagicNavigation extends Elf {
|
|
419
|
+
logic = Elf.getLogic(MagicNavigationLogic);
|
|
420
|
+
state = new MagicNavigationState();
|
|
421
|
+
|
|
422
|
+
/** @type {string} */
|
|
423
|
+
clientSessionId;
|
|
424
|
+
|
|
425
|
+
/** @type {Map<id,View>} */
|
|
426
|
+
views = new Map();
|
|
427
|
+
|
|
428
|
+
/** @type {Map<DesktopId,Function>} */
|
|
429
|
+
unsubs = new Map();
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @param {string} id
|
|
433
|
+
* @param {DesktopId} desktopId
|
|
434
|
+
* @param {string} clientSessionId
|
|
435
|
+
* @param {string} [existingWindowId]
|
|
436
|
+
* @returns {Promise<this>}
|
|
437
|
+
*/
|
|
438
|
+
async create(id, desktopId, clientSessionId, existingWindowId) {
|
|
439
|
+
this.desktopId = desktopId;
|
|
440
|
+
this.clientSessionId = clientSessionId;
|
|
441
|
+
this.logic.create(id, existingWindowId);
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @template {keyof this['state']} K
|
|
447
|
+
* @param {K} path
|
|
448
|
+
* @param {this['state'][K]} newValue
|
|
449
|
+
*/
|
|
450
|
+
async change(path, newValue) {
|
|
451
|
+
this.logic.change(path, newValue);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @returns {Promise<DesktopId>}
|
|
456
|
+
*/
|
|
457
|
+
async _newWindow() {
|
|
458
|
+
const clientAPI = this.quest.getAPI('client');
|
|
459
|
+
const session = this.quest.uuidV4();
|
|
460
|
+
const username = this.quest.user.login;
|
|
461
|
+
const userId = this.quest.user.id;
|
|
462
|
+
const rootWidget = 'yeti-root';
|
|
463
|
+
const configuration = {};
|
|
464
|
+
const mainGoblin = 'yeti';
|
|
465
|
+
const mandate = 'yeti';
|
|
466
|
+
await clientAPI.openSession({
|
|
467
|
+
session,
|
|
468
|
+
username,
|
|
469
|
+
userId,
|
|
470
|
+
rootWidget,
|
|
471
|
+
configuration,
|
|
472
|
+
mainGoblin,
|
|
473
|
+
mandate,
|
|
474
|
+
});
|
|
475
|
+
/** @type {DesktopId} */
|
|
476
|
+
const desktopId = `desktop@${mandate}@${session}`;
|
|
477
|
+
|
|
478
|
+
const labId = (await this.quest.getState('client')).get(
|
|
479
|
+
`private.labByDesktop.${desktopId}`
|
|
480
|
+
);
|
|
481
|
+
const winId = `wm@${labId}`;
|
|
482
|
+
const navigationId = this.id;
|
|
483
|
+
const unsub = this.quest.sub.local(
|
|
484
|
+
`*::${winId}.${this.clientSessionId}.<window-closed>`,
|
|
485
|
+
function* (err, {msg, resp}) {
|
|
486
|
+
console.log('Window closed:', desktopId);
|
|
487
|
+
yield resp.cmd('magicNavigation.handleWindowClosed', {
|
|
488
|
+
id: navigationId,
|
|
489
|
+
windowId: desktopId,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
this.unsubs.set(desktopId, unsub);
|
|
494
|
+
return desktopId;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @param {DesktopId} windowId
|
|
499
|
+
*/
|
|
500
|
+
async handleWindowClosed(windowId) {
|
|
501
|
+
const {state} = this;
|
|
502
|
+
const window = state.windows[windowId];
|
|
503
|
+
for (const panelId of window.panelIds) {
|
|
504
|
+
const panel = state.panels[panelId];
|
|
505
|
+
for (const tabId of panel.tabIds) {
|
|
506
|
+
this.views.delete(tabId);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const tabId of window.dialogIds) {
|
|
510
|
+
this.views.delete(tabId);
|
|
511
|
+
}
|
|
512
|
+
const unsub = this.unsubs.get(windowId);
|
|
513
|
+
if (!unsub) {
|
|
514
|
+
throw new Error('Already unsubscribed');
|
|
515
|
+
}
|
|
516
|
+
unsub();
|
|
517
|
+
this.unsubs.delete(windowId);
|
|
518
|
+
this.logic.handleWindowClosed(windowId);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* @template {ViewParam} T
|
|
523
|
+
* @param {View<T>} view
|
|
524
|
+
* @param {DesktopId} desktopId
|
|
525
|
+
* @returns {Promise<string | undefined>}
|
|
526
|
+
*/
|
|
527
|
+
async _createService(view, desktopId) {
|
|
528
|
+
if (!view.service) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let serviceId = view.serviceId;
|
|
533
|
+
if (!serviceId) {
|
|
534
|
+
if (typeof view.service === 'string') {
|
|
535
|
+
serviceId = `${view.service}@${Elf.uuid()}`;
|
|
536
|
+
await this.quest.create(serviceId, {
|
|
537
|
+
...view.serviceArgs,
|
|
538
|
+
id: serviceId,
|
|
539
|
+
desktopId,
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
const ServiceClass = view.service;
|
|
543
|
+
serviceId = `${Elf.goblinName(ServiceClass)}@${Elf.uuid()}`;
|
|
544
|
+
const serviceArgs = view.serviceArgs || [];
|
|
545
|
+
await new ServiceClass(this).create(
|
|
546
|
+
serviceId,
|
|
547
|
+
this.desktopId,
|
|
548
|
+
...serviceArgs
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return serviceId;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* @param {id} viewId
|
|
557
|
+
* @param {id | null | undefined} serviceId
|
|
558
|
+
* @param {DesktopId} desktopId
|
|
559
|
+
*/
|
|
560
|
+
async _removeView(viewId, serviceId, desktopId) {
|
|
561
|
+
if (!this.views.has(viewId)) {
|
|
562
|
+
throw new Error(`Bad viewId '${viewId}'`);
|
|
563
|
+
}
|
|
564
|
+
this.views.delete(viewId);
|
|
565
|
+
if (serviceId) {
|
|
566
|
+
await this.kill(serviceId, this.id, desktopId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @param {id} parentId
|
|
572
|
+
* @param {string} prompt
|
|
573
|
+
* @param {object} [options]
|
|
574
|
+
* @param {'default' | 'yes-no'} [options.kind]
|
|
575
|
+
* @param {string} [options.advice]
|
|
576
|
+
* @param {string} [options.okLabel]
|
|
577
|
+
* @param {string} [options.noLabel]
|
|
578
|
+
* @param {string} [options.cancelLabel]
|
|
579
|
+
* @param {'cancel' | 'reject' | 'confirm'} [options.main]
|
|
580
|
+
* @returns {Promise<true | false | undefined>}
|
|
581
|
+
*
|
|
582
|
+
* Possible return values:
|
|
583
|
+
* Confirm / Yes -> true
|
|
584
|
+
* No -> false
|
|
585
|
+
* Cancel / ESC / Outside click / Parent closed -> undefined
|
|
586
|
+
*/
|
|
587
|
+
async confirm(
|
|
588
|
+
parentId,
|
|
589
|
+
prompt,
|
|
590
|
+
{kind, advice, okLabel, noLabel, cancelLabel, main} = {}
|
|
591
|
+
) {
|
|
592
|
+
const dialogId = await this.openDialog(
|
|
593
|
+
{
|
|
594
|
+
widget: 'ConfirmDialog',
|
|
595
|
+
widgetProps: {
|
|
596
|
+
prompt,
|
|
597
|
+
kind,
|
|
598
|
+
advice,
|
|
599
|
+
okLabel,
|
|
600
|
+
noLabel,
|
|
601
|
+
cancelLabel,
|
|
602
|
+
main,
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
parentId
|
|
606
|
+
);
|
|
607
|
+
return await this.waitClosed(dialogId);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @param {id} parentId
|
|
612
|
+
* @param {string} prompt
|
|
613
|
+
* @param {string} [advice]
|
|
614
|
+
* @param {string} [okLabel]
|
|
615
|
+
* @param {string} [cancelLabel]
|
|
616
|
+
* @param {string} [initialValue]
|
|
617
|
+
* @returns {Promise<any>}
|
|
618
|
+
*/
|
|
619
|
+
async prompt(parentId, prompt, advice, okLabel, cancelLabel, initialValue) {
|
|
620
|
+
const dialogId = await this.openDialog(
|
|
621
|
+
{
|
|
622
|
+
widget: 'PromptDialog',
|
|
623
|
+
widgetProps: {
|
|
624
|
+
prompt,
|
|
625
|
+
advice,
|
|
626
|
+
okLabel,
|
|
627
|
+
cancelLabel,
|
|
628
|
+
initialValue,
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
parentId
|
|
632
|
+
);
|
|
633
|
+
return await this.waitClosed(dialogId);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @param {id} parentId
|
|
638
|
+
* @param {string} prompt
|
|
639
|
+
* @param {string} [advice]
|
|
640
|
+
* @returns {Promise<any>}
|
|
641
|
+
*/
|
|
642
|
+
async alert(parentId, prompt, advice) {
|
|
643
|
+
const dialogId = await this.openDialog(
|
|
644
|
+
{
|
|
645
|
+
widget: 'AlertDialog',
|
|
646
|
+
widgetProps: {prompt, advice},
|
|
647
|
+
},
|
|
648
|
+
parentId
|
|
649
|
+
);
|
|
650
|
+
return await this.waitClosed(dialogId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async activatePanel(panelId) {
|
|
654
|
+
const windowId = await this.findPanelWindowId(panelId);
|
|
655
|
+
if (!windowId) {
|
|
656
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
657
|
+
}
|
|
658
|
+
this.logic.activatePanel(windowId, panelId);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async openEmptyWindow() {
|
|
662
|
+
const windowId = await this._newWindow();
|
|
663
|
+
this.logic.openEmptyWindow(windowId);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* @template {ViewParam} T
|
|
668
|
+
* @param {View<T>} view
|
|
669
|
+
* @param {DesktopId} desktopId
|
|
670
|
+
* @returns {Promise<id>}
|
|
671
|
+
*/
|
|
672
|
+
async openTabInNewWindow(view, desktopId) {
|
|
673
|
+
const windowId = await this._newWindow();
|
|
674
|
+
|
|
675
|
+
const tabId = `tab@${this.quest.uuidV4()}`;
|
|
676
|
+
const serviceId = await this._createService(view, windowId);
|
|
677
|
+
const tab = {
|
|
678
|
+
serviceId,
|
|
679
|
+
widget: view.widget,
|
|
680
|
+
widgetProps: view.widgetProps,
|
|
681
|
+
highlighted: false,
|
|
682
|
+
};
|
|
683
|
+
this.views.set(tabId, view);
|
|
684
|
+
|
|
685
|
+
this.logic.openTabInNewWindow(windowId, tabId, tab);
|
|
686
|
+
return tabId;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* @template {ViewParam} T
|
|
691
|
+
* @param {View<T>} view
|
|
692
|
+
* @param {DesktopId} desktopId
|
|
693
|
+
* @param {id} [panelId]
|
|
694
|
+
* @param {boolean} [activateTab]
|
|
695
|
+
* @returns {Promise<id>}
|
|
696
|
+
*/
|
|
697
|
+
async openNewTab(view, desktopId, panelId, activateTab = true) {
|
|
698
|
+
if (!panelId) {
|
|
699
|
+
const windowId = desktopId;
|
|
700
|
+
const window = this.state.windows[windowId];
|
|
701
|
+
const panelIndex = window.panelIds.indexOf(window.activePanelId);
|
|
702
|
+
const newPanelIndex = (() => {
|
|
703
|
+
if (panelIndex < window.panelIds.length - 1) {
|
|
704
|
+
return panelIndex + 1;
|
|
705
|
+
}
|
|
706
|
+
if (panelIndex > 0) {
|
|
707
|
+
return panelIndex - 1;
|
|
708
|
+
}
|
|
709
|
+
return panelIndex;
|
|
710
|
+
})();
|
|
711
|
+
panelId = window.panelIds[newPanelIndex];
|
|
712
|
+
}
|
|
713
|
+
const tabId = `tab@${this.quest.uuidV4()}`;
|
|
714
|
+
const serviceId =
|
|
715
|
+
view.serviceId || (await this._createService(view, desktopId));
|
|
716
|
+
const tab = {
|
|
717
|
+
serviceId,
|
|
718
|
+
widget: view.widget,
|
|
719
|
+
widgetProps: view.widgetProps,
|
|
720
|
+
highlighted: false,
|
|
721
|
+
};
|
|
722
|
+
if (!tab.widget && !tab.serviceId) {
|
|
723
|
+
throw new Error(`Cannot open a tab without a widget or a service`);
|
|
724
|
+
}
|
|
725
|
+
this.views.set(tabId, view);
|
|
726
|
+
|
|
727
|
+
this.logic.openNewTab(panelId, tabId, tab, activateTab);
|
|
728
|
+
return tabId;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async _hasEqualViewArgs(viewId, currentView, newView) {
|
|
732
|
+
const serviceId = this.state.tabs[viewId].serviceId;
|
|
733
|
+
if (newView.service && serviceId) {
|
|
734
|
+
if (typeof newView.service === 'string') {
|
|
735
|
+
const serviceAPI = this.quest.getAPI(serviceId);
|
|
736
|
+
if (serviceAPI.hasSameViewArgs) {
|
|
737
|
+
return await serviceAPI.hasSameViewArgs(newView.serviceArgs);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
const ServiceClass = newView.service;
|
|
741
|
+
const serviceAPI = await new ServiceClass(this).api(serviceId);
|
|
742
|
+
if (serviceAPI.hasSameViewArgs) {
|
|
743
|
+
return await serviceAPI.hasSameViewArgs(...newView.serviceArgs);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return isEqual(currentView.serviceArgs, newView.serviceArgs);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async _hasSameWidget(view1, view2) {
|
|
752
|
+
const widget1 = view1.widget || getServiceName(view1.service);
|
|
753
|
+
const widget2 = view2.widget || getServiceName(view2.service);
|
|
754
|
+
return (
|
|
755
|
+
widget1[0].toLowerCase() + widget1.slice(1) ===
|
|
756
|
+
widget2[0].toLowerCase() + widget2.slice(1)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async findExistingView(view, desktopId, kind) {
|
|
761
|
+
for (const [viewId, v] of this.views) {
|
|
762
|
+
if (getServiceName(view.service) !== getServiceName(v.service)) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (view.serviceId && view.serviceId !== v.serviceId) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (!(await this._hasSameWidget(v, view))) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (!(await this._hasEqualViewArgs(viewId, v, view))) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (!isEqual(view.widgetProps, v.widgetProps)) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (kind === 'dialog') {
|
|
778
|
+
if (!this.state.windows[desktopId].dialogIds.includes(viewId)) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
const panelId = await this.findPanelId(viewId);
|
|
783
|
+
if (!panelId) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const windowId = await this.findPanelWindowId(panelId);
|
|
787
|
+
if (windowId !== desktopId) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return viewId;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* @template {ViewParam} T
|
|
797
|
+
* @param {id} viewId
|
|
798
|
+
* @param {View<T>} view
|
|
799
|
+
*/
|
|
800
|
+
async changeView(viewId, view) {
|
|
801
|
+
const currentView = this.views.get(viewId);
|
|
802
|
+
if (!currentView) {
|
|
803
|
+
throw new Error(`Missing view '${viewId}'`);
|
|
804
|
+
}
|
|
805
|
+
const serviceId = this.state.tabs[viewId].serviceId;
|
|
806
|
+
if (!view.service || !serviceId) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// this.views.set(viewId, {...currentView, ...view});
|
|
810
|
+
if (typeof view.service === 'string') {
|
|
811
|
+
const serviceAPI = this.quest.getAPI(serviceId);
|
|
812
|
+
if (serviceAPI.onViewChange) {
|
|
813
|
+
await serviceAPI.onViewChange(view.serviceArgs);
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
const ServiceClass = view.service;
|
|
817
|
+
const serviceAPI = await new ServiceClass(this).api(serviceId);
|
|
818
|
+
if (
|
|
819
|
+
'onViewChange' in serviceAPI &&
|
|
820
|
+
typeof serviceAPI.onViewChange === 'function'
|
|
821
|
+
) {
|
|
822
|
+
await serviceAPI.onViewChange(...view.serviceArgs);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* @template {ViewParam} T
|
|
829
|
+
* @param {View<T>} view
|
|
830
|
+
* @param {DesktopId} desktopId
|
|
831
|
+
* @returns {Promise<id>}
|
|
832
|
+
*/
|
|
833
|
+
async activateOrOpenTab(view, desktopId) {
|
|
834
|
+
const viewId = await this.findExistingView(view, desktopId, 'tab');
|
|
835
|
+
if (!viewId) {
|
|
836
|
+
return await this.openNewTab(view, desktopId);
|
|
837
|
+
}
|
|
838
|
+
await this.changeView(viewId, view);
|
|
839
|
+
await this.activateTab(viewId, true);
|
|
840
|
+
return viewId;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* @param {id} tabId
|
|
845
|
+
*/
|
|
846
|
+
async highlightTab(tabId) {
|
|
847
|
+
this.logic.highlightTab(tabId);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* @template {ViewParam} T
|
|
852
|
+
* @param {View<T>} view
|
|
853
|
+
* @param {DesktopId} desktopId
|
|
854
|
+
* @param {id} [panelId]
|
|
855
|
+
* @returns {Promise<id>}
|
|
856
|
+
*/
|
|
857
|
+
async highlightOrOpenTab(view, desktopId, panelId) {
|
|
858
|
+
let viewId = await this.findExistingView(view, desktopId, 'tab');
|
|
859
|
+
if (!viewId) {
|
|
860
|
+
viewId = await this.openNewTab(view, desktopId, panelId, false);
|
|
861
|
+
}
|
|
862
|
+
const tabPanelId = await this.findPanelId(viewId);
|
|
863
|
+
if (!tabPanelId) {
|
|
864
|
+
throw new Error(`Bad viewId '${viewId}'`);
|
|
865
|
+
}
|
|
866
|
+
const panel = this.state.panels[tabPanelId];
|
|
867
|
+
if (panel.currentTabId === viewId) {
|
|
868
|
+
return viewId;
|
|
869
|
+
}
|
|
870
|
+
await this.changeView(viewId, view);
|
|
871
|
+
if (panel.tabIds.length > 1) {
|
|
872
|
+
await this.highlightTab(viewId);
|
|
873
|
+
} else {
|
|
874
|
+
await this.activateTab(viewId);
|
|
875
|
+
}
|
|
876
|
+
return viewId;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* @template {ViewParam} T
|
|
881
|
+
* @param {View<T>} view
|
|
882
|
+
* @param {DesktopId} desktopId
|
|
883
|
+
* @param {Modifiers} [modifiers]
|
|
884
|
+
* @returns {Promise<id>}
|
|
885
|
+
*/
|
|
886
|
+
async openTab(view, desktopId, modifiers) {
|
|
887
|
+
if (modifiers) {
|
|
888
|
+
if (modifiers.ctrlKey) {
|
|
889
|
+
return await this.highlightOrOpenTab(view, desktopId);
|
|
890
|
+
}
|
|
891
|
+
if (modifiers.altKey || modifiers.metaKey) {
|
|
892
|
+
return await this.openTabInNewWindow(view, desktopId);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return await this.activateOrOpenTab(view, desktopId);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* @param {id} serviceId
|
|
900
|
+
* @returns {Promise<id | undefined >}
|
|
901
|
+
*/
|
|
902
|
+
async findViewId(serviceId) {
|
|
903
|
+
return Object.entries(this.state.tabs).find(
|
|
904
|
+
([viewId, view]) => view.serviceId === serviceId
|
|
905
|
+
)?.[0];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @param {id} viewOrServiceId
|
|
910
|
+
* @returns {Promise<id>}
|
|
911
|
+
*/
|
|
912
|
+
async getViewId(viewOrServiceId) {
|
|
913
|
+
if (this.state._state.get('tabs').has(viewOrServiceId)) {
|
|
914
|
+
return viewOrServiceId;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const id = await this.findViewId(viewOrServiceId);
|
|
918
|
+
if (!id) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Unable to find a navigation view for serviceId '${viewOrServiceId}'`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return id;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* @param {id} tabId
|
|
928
|
+
* @returns {Promise<id | undefined >}
|
|
929
|
+
*/
|
|
930
|
+
async findPanelId(tabId) {
|
|
931
|
+
return Object.entries(this.state.panels).find(([panelId, panel]) =>
|
|
932
|
+
panel.tabIds.includes(tabId)
|
|
933
|
+
)?.[0];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* @param {id} panelId
|
|
938
|
+
* @returns {Promise<DesktopId | undefined >}
|
|
939
|
+
*/
|
|
940
|
+
async findPanelWindowId(panelId) {
|
|
941
|
+
return Object.entries(this.state.windows).find(([windowId, window]) =>
|
|
942
|
+
window.panelIds.includes(panelId)
|
|
943
|
+
)?.[0];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* @param {id} viewId
|
|
948
|
+
* @returns {Promise<DesktopId | undefined >}
|
|
949
|
+
*/
|
|
950
|
+
async findViewWindowId(viewId) {
|
|
951
|
+
return Object.entries(this.state.windows).find(
|
|
952
|
+
([windowId, window]) =>
|
|
953
|
+
window.dialogIds.includes(viewId) ||
|
|
954
|
+
[...window.panelIds].some((panelId) =>
|
|
955
|
+
this.state.panels[panelId].tabIds.includes(viewId)
|
|
956
|
+
)
|
|
957
|
+
)?.[0];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* @param {id} tabId
|
|
962
|
+
* @param {boolean} keepHistory
|
|
963
|
+
*/
|
|
964
|
+
async activateTab(tabId, keepHistory = false) {
|
|
965
|
+
const panelId = await this.findPanelId(tabId);
|
|
966
|
+
if (!panelId) {
|
|
967
|
+
throw new Error(`Unknown tab '${tabId}'`);
|
|
968
|
+
}
|
|
969
|
+
this.logic.activateTab(panelId, tabId, keepHistory);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* @param {DesktopId} desktopId
|
|
974
|
+
* @returns {Promise<id | null | undefined>}
|
|
975
|
+
*/
|
|
976
|
+
async getCurrentTab(desktopId) {
|
|
977
|
+
const windowId = desktopId;
|
|
978
|
+
const window = this.state.windows[windowId];
|
|
979
|
+
const panel = this.state.panels[window.activePanelId];
|
|
980
|
+
return panel.currentTabId;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* @param {DesktopId} desktopId
|
|
985
|
+
*/
|
|
986
|
+
async closeCurrentTab(desktopId) {
|
|
987
|
+
const tabId = await this.getCurrentTab(desktopId);
|
|
988
|
+
if (tabId) {
|
|
989
|
+
await this.closeView(desktopId, tabId);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* @param {id} panelId
|
|
995
|
+
* @param {id} tabId
|
|
996
|
+
*/
|
|
997
|
+
async _removeTab(panelId, tabId) {
|
|
998
|
+
this.logic._removeTab(panelId, tabId);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* @param {id} tabId
|
|
1003
|
+
* @param {any} [result]
|
|
1004
|
+
*/
|
|
1005
|
+
async closeTab(tabId, result) {
|
|
1006
|
+
const panelId = await this.findPanelId(tabId);
|
|
1007
|
+
if (!panelId) {
|
|
1008
|
+
throw new Error(`Unknown tab '${tabId}'`);
|
|
1009
|
+
}
|
|
1010
|
+
const desktopId = await this.findPanelWindowId(panelId);
|
|
1011
|
+
if (!desktopId) {
|
|
1012
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
1013
|
+
}
|
|
1014
|
+
const serviceId = this.state.tabs[tabId].serviceId;
|
|
1015
|
+
await this._removeTab(panelId, tabId);
|
|
1016
|
+
await this._removeView(tabId, serviceId, desktopId);
|
|
1017
|
+
this.quest.evt.send(`${tabId}-closed`, result);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* @param {id} tabId
|
|
1022
|
+
*/
|
|
1023
|
+
async duplicateTab(tabId) {
|
|
1024
|
+
const panelId = await this.findPanelId(tabId);
|
|
1025
|
+
if (!panelId) {
|
|
1026
|
+
throw new Error(`Unknown tab '${tabId}'`);
|
|
1027
|
+
}
|
|
1028
|
+
const windowId = await this.findPanelWindowId(panelId);
|
|
1029
|
+
if (!windowId) {
|
|
1030
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
1031
|
+
}
|
|
1032
|
+
const view = this.views.get(tabId);
|
|
1033
|
+
if (!view) {
|
|
1034
|
+
throw new Error(`Missing view for tab '${tabId}'`);
|
|
1035
|
+
}
|
|
1036
|
+
await this.openNewTab(view, windowId, panelId);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* @param {id} tabId
|
|
1041
|
+
*/
|
|
1042
|
+
async moveTabToNewWindow(tabId) {
|
|
1043
|
+
const panelId = await this.findPanelId(tabId);
|
|
1044
|
+
if (!panelId) {
|
|
1045
|
+
throw new Error(`Unknown tab '${tabId}'`);
|
|
1046
|
+
}
|
|
1047
|
+
const oldWindowId = await this.findPanelWindowId(panelId);
|
|
1048
|
+
if (!oldWindowId) {
|
|
1049
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
1050
|
+
}
|
|
1051
|
+
const windowId = await this._newWindow();
|
|
1052
|
+
const serviceId = this.state.tabs[tabId].serviceId;
|
|
1053
|
+
if (serviceId) {
|
|
1054
|
+
// Attach the service to the new window
|
|
1055
|
+
await this.quest.create(serviceId, {
|
|
1056
|
+
id: serviceId,
|
|
1057
|
+
desktopId: windowId,
|
|
1058
|
+
});
|
|
1059
|
+
await this.kill(serviceId, this.id, oldWindowId);
|
|
1060
|
+
}
|
|
1061
|
+
this.logic.moveTabToNewWindow(panelId, tabId, windowId);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* @param {id} tabId
|
|
1066
|
+
* @param {boolean} [nextPanel]
|
|
1067
|
+
*/
|
|
1068
|
+
async moveTabToPanel(tabId, nextPanel = true) {
|
|
1069
|
+
const panelId = await this.findPanelId(tabId);
|
|
1070
|
+
if (!panelId) {
|
|
1071
|
+
throw new Error(`Unknown tab '${tabId}'`);
|
|
1072
|
+
}
|
|
1073
|
+
const windowId = await this.findPanelWindowId(panelId);
|
|
1074
|
+
if (!windowId) {
|
|
1075
|
+
throw new Error(`Unknown panel '${panelId}'`);
|
|
1076
|
+
}
|
|
1077
|
+
this.logic.moveTabToPanel(tabId, panelId, windowId, nextPanel);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @param {DesktopId} desktopId
|
|
1082
|
+
* @param {boolean} [reverse]
|
|
1083
|
+
*/
|
|
1084
|
+
async switchTab(desktopId, reverse) {
|
|
1085
|
+
const windowId = desktopId;
|
|
1086
|
+
const window = this.state.windows[windowId];
|
|
1087
|
+
const panel = this.state.panels[window.activePanelId];
|
|
1088
|
+
const currentTabId = panel.currentTabId;
|
|
1089
|
+
if (!currentTabId) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
let index = panel.tabIds.indexOf(currentTabId);
|
|
1093
|
+
if (reverse) {
|
|
1094
|
+
index--;
|
|
1095
|
+
if (index < 0) {
|
|
1096
|
+
index = panel.tabIds.length - 1;
|
|
1097
|
+
}
|
|
1098
|
+
} else {
|
|
1099
|
+
index++;
|
|
1100
|
+
if (index >= panel.tabIds.length) {
|
|
1101
|
+
index = 0;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const newTabId = panel.tabIds[index];
|
|
1105
|
+
await this.activateTab(newTabId);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* @param {DesktopId} desktopId
|
|
1110
|
+
* @param {number} index 1-9
|
|
1111
|
+
*/
|
|
1112
|
+
async activateTabIndex(desktopId, index) {
|
|
1113
|
+
const windowId = desktopId;
|
|
1114
|
+
const window = this.state.windows[windowId];
|
|
1115
|
+
const panel = this.state.panels[window.activePanelId];
|
|
1116
|
+
const length = panel.tabIds.length;
|
|
1117
|
+
let arrayIndex = index - 1;
|
|
1118
|
+
if (index === 9) {
|
|
1119
|
+
arrayIndex = length - 1;
|
|
1120
|
+
}
|
|
1121
|
+
if (arrayIndex >= length || arrayIndex < 0) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const tabId = panel.tabIds[arrayIndex];
|
|
1125
|
+
await this.activateTab(tabId);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* @param {id | DesktopId} parentId
|
|
1130
|
+
* @returns {Promise<{windowId: DesktopId, parentViewId: id | null}>}
|
|
1131
|
+
*/
|
|
1132
|
+
async parseParentId(parentId) {
|
|
1133
|
+
if (Object.keys(this.state.windows).includes(parentId)) {
|
|
1134
|
+
return {
|
|
1135
|
+
windowId: parentId,
|
|
1136
|
+
parentViewId: null,
|
|
1137
|
+
};
|
|
1138
|
+
} else {
|
|
1139
|
+
const parentViewId = await this.getViewId(parentId);
|
|
1140
|
+
const windowId = await this.findViewWindowId(parentViewId);
|
|
1141
|
+
if (!windowId) {
|
|
1142
|
+
throw new Error(`Unknown view '${parentViewId}'`);
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
windowId,
|
|
1146
|
+
parentViewId,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* @template {ViewParam} T
|
|
1153
|
+
* @param {View<T>} view
|
|
1154
|
+
* @param {id | DesktopId} parentId
|
|
1155
|
+
* @param {boolean} [modal]
|
|
1156
|
+
* @param {boolean} [openNew]
|
|
1157
|
+
* @returns {Promise<id>}
|
|
1158
|
+
*/
|
|
1159
|
+
async openDialog(view, parentId, modal = true, openNew = false) {
|
|
1160
|
+
const {windowId, parentViewId} = await this.parseParentId(parentId);
|
|
1161
|
+
|
|
1162
|
+
/** @type {View<T>} */
|
|
1163
|
+
const newView = {
|
|
1164
|
+
...view,
|
|
1165
|
+
widgetProps: {...view.widgetProps, modal},
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
if (!openNew) {
|
|
1169
|
+
const viewId = await this.findExistingView(newView, windowId, 'dialog');
|
|
1170
|
+
if (viewId) {
|
|
1171
|
+
return viewId;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const dialogId = `dialog@${this.quest.uuidV4()}`;
|
|
1176
|
+
|
|
1177
|
+
const serviceId =
|
|
1178
|
+
newView.serviceId || (await this._createService(newView, windowId));
|
|
1179
|
+
const dialog = {
|
|
1180
|
+
serviceId,
|
|
1181
|
+
widget: newView.widget,
|
|
1182
|
+
widgetProps: newView.widgetProps,
|
|
1183
|
+
highlighted: false,
|
|
1184
|
+
parentViewId,
|
|
1185
|
+
};
|
|
1186
|
+
this.views.set(dialogId, newView);
|
|
1187
|
+
|
|
1188
|
+
this.logic.openDialog(windowId, dialogId, dialog);
|
|
1189
|
+
return dialogId;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* @param {id} windowId
|
|
1194
|
+
* @param {id} dialogId
|
|
1195
|
+
*/
|
|
1196
|
+
_removeDialog(windowId, dialogId) {
|
|
1197
|
+
this.logic._removeDialog(windowId, dialogId);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* @param {DesktopId} desktopId
|
|
1202
|
+
* @param {id} dialogId
|
|
1203
|
+
* @param {any} [result]
|
|
1204
|
+
*/
|
|
1205
|
+
async closeDialog(desktopId, dialogId, result) {
|
|
1206
|
+
const windowId = desktopId;
|
|
1207
|
+
const serviceId = this.state.tabs[dialogId]?.serviceId;
|
|
1208
|
+
await this._removeDialog(windowId, dialogId);
|
|
1209
|
+
await this._removeView(dialogId, serviceId, desktopId);
|
|
1210
|
+
this.quest.evt.send(`${dialogId}-closed`, result);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* @param {id} viewOrServiceId id of dialog, tab or service
|
|
1215
|
+
* @returns {Promise<any>}
|
|
1216
|
+
*/
|
|
1217
|
+
async waitClosed(viewOrServiceId) {
|
|
1218
|
+
const viewId = await this.getViewId(viewOrServiceId);
|
|
1219
|
+
const result = await this.quest.sub.wait(
|
|
1220
|
+
`*::magicNavigation.${viewId}-closed`
|
|
1221
|
+
);
|
|
1222
|
+
return result;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* @template {ViewParam} T
|
|
1227
|
+
* @param {id} viewOrServiceId
|
|
1228
|
+
* @param {View<T>} view
|
|
1229
|
+
* @param {DesktopId} desktopId
|
|
1230
|
+
* @param {boolean} [back=false]
|
|
1231
|
+
*/
|
|
1232
|
+
async replace(viewOrServiceId, view, desktopId, back = false) {
|
|
1233
|
+
const viewId = await this.getViewId(viewOrServiceId);
|
|
1234
|
+
const currentView = this.views.get(viewId);
|
|
1235
|
+
if (!currentView) {
|
|
1236
|
+
throw new Error(`Missing view '${viewId}'`);
|
|
1237
|
+
}
|
|
1238
|
+
const serviceId =
|
|
1239
|
+
view.serviceId || (await this._createService(view, desktopId));
|
|
1240
|
+
|
|
1241
|
+
const viewState = {
|
|
1242
|
+
serviceId,
|
|
1243
|
+
widget: view.widget,
|
|
1244
|
+
widgetProps: view.widgetProps,
|
|
1245
|
+
highlighted: false,
|
|
1246
|
+
};
|
|
1247
|
+
const newView = back ? view : {...view, previousView: currentView};
|
|
1248
|
+
this.views.set(viewId, newView);
|
|
1249
|
+
this.logic.replace(viewId, viewState);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* @param {DesktopId} desktopId
|
|
1254
|
+
* @param {id} viewId
|
|
1255
|
+
* @param {any} [result]
|
|
1256
|
+
*/
|
|
1257
|
+
async closeView(desktopId, viewId, result) {
|
|
1258
|
+
for (const [id, view] of Object.entries(this.state.tabs)) {
|
|
1259
|
+
if (view.parentViewId === viewId) {
|
|
1260
|
+
await this.closeView(desktopId, id);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (viewId.startsWith('dialog@')) {
|
|
1264
|
+
await this.closeDialog(desktopId, viewId, result);
|
|
1265
|
+
} else {
|
|
1266
|
+
await this.closeTab(viewId, result);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* @param {id} viewOrServiceId
|
|
1272
|
+
* @param {DesktopId} desktopId
|
|
1273
|
+
*/
|
|
1274
|
+
async back(viewOrServiceId, desktopId) {
|
|
1275
|
+
const viewId = await this.getViewId(viewOrServiceId);
|
|
1276
|
+
const currentView = this.views.get(viewId);
|
|
1277
|
+
if (!currentView) {
|
|
1278
|
+
throw new Error(`Missing view '${viewId}'`);
|
|
1279
|
+
}
|
|
1280
|
+
const previousView = currentView.previousView;
|
|
1281
|
+
if (previousView) {
|
|
1282
|
+
await this.replace(viewId, previousView, desktopId, true);
|
|
1283
|
+
} else {
|
|
1284
|
+
await this.closeView(desktopId, viewId);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* @param {DesktopId} desktopId
|
|
1290
|
+
*/
|
|
1291
|
+
async backCurrentTab(desktopId) {
|
|
1292
|
+
const tabId = await this.getCurrentTab(desktopId);
|
|
1293
|
+
if (tabId) {
|
|
1294
|
+
await this.back(tabId, desktopId);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
delete() {
|
|
1299
|
+
for (const unsub of this.unsubs.values()) {
|
|
1300
|
+
unsub();
|
|
1301
|
+
}
|
|
1302
|
+
this.unsubs.clear();
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
module.exports = {MagicNavigation, MagicNavigationLogic};
|