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.
Files changed (88) hide show
  1. package/.editorconfig +9 -0
  2. package/.zou-flow +2 -0
  3. package/eslint.config.js +65 -0
  4. package/magicNavigation.js +7 -0
  5. package/package.json +45 -0
  6. package/widgets/dialog/widget.js +78 -0
  7. package/widgets/element-helpers/element-has-direct-text.js +9 -0
  8. package/widgets/element-helpers/is-empty-area-element.js +17 -0
  9. package/widgets/element-helpers/is-flat-element.js +52 -0
  10. package/widgets/get-modifiers/get-modifiers.js +34 -0
  11. package/widgets/input-group/styles.js +74 -0
  12. package/widgets/input-group/widget.js +24 -0
  13. package/widgets/magic-action/styles.js +45 -0
  14. package/widgets/magic-action/widget.js +44 -0
  15. package/widgets/magic-background/bg-alps.jpg +0 -0
  16. package/widgets/magic-background/bg-fur.png +0 -0
  17. package/widgets/magic-background/bg-milkyway.png +0 -0
  18. package/widgets/magic-background/bg-space.jpg +0 -0
  19. package/widgets/magic-background/bg-synth.jpg +0 -0
  20. package/widgets/magic-background/bg-white.png +0 -0
  21. package/widgets/magic-background/styles.js +81 -0
  22. package/widgets/magic-background/widget.js +20 -0
  23. package/widgets/magic-box/styles.js +10 -0
  24. package/widgets/magic-box/widget.js +28 -0
  25. package/widgets/magic-box-old/styles.js +111 -0
  26. package/widgets/magic-box-old/widget.js +30 -0
  27. package/widgets/magic-button/styles.js +156 -0
  28. package/widgets/magic-button/widget.js +89 -0
  29. package/widgets/magic-checkbox/styles.js +116 -0
  30. package/widgets/magic-checkbox/widget.js +68 -0
  31. package/widgets/magic-color-field/styles.js +22 -0
  32. package/widgets/magic-color-field/widget.js +68 -0
  33. package/widgets/magic-date-field/styles.js +9 -0
  34. package/widgets/magic-date-field/widget.js +145 -0
  35. package/widgets/magic-datetime-field/styles.js +11 -0
  36. package/widgets/magic-datetime-field/widget.js +95 -0
  37. package/widgets/magic-dialog/styles.js +39 -0
  38. package/widgets/magic-dialog/widget.js +116 -0
  39. package/widgets/magic-div/styles.js +22 -0
  40. package/widgets/magic-div/widget.js +20 -0
  41. package/widgets/magic-emoji/styles.js +14 -0
  42. package/widgets/magic-emoji/widget.js +33 -0
  43. package/widgets/magic-emoji-picker/styles.js +21 -0
  44. package/widgets/magic-emoji-picker/widget.js +44 -0
  45. package/widgets/magic-inplace-input/styles.js +55 -0
  46. package/widgets/magic-inplace-input/widget.js +26 -0
  47. package/widgets/magic-input/styles.js +50 -0
  48. package/widgets/magic-input/widget.js +397 -0
  49. package/widgets/magic-label/styles.js +20 -0
  50. package/widgets/magic-label/widget.js +24 -0
  51. package/widgets/magic-navigation/service.js +1306 -0
  52. package/widgets/magic-navigation/styles.js +103 -0
  53. package/widgets/magic-navigation/view-context.js +15 -0
  54. package/widgets/magic-navigation/widget.js +540 -0
  55. package/widgets/magic-number-field/styles.js +10 -0
  56. package/widgets/magic-number-field/widget.js +103 -0
  57. package/widgets/magic-panel/styles.js +61 -0
  58. package/widgets/magic-panel/widget.js +63 -0
  59. package/widgets/magic-radio/styles.js +93 -0
  60. package/widgets/magic-radio/widget.js +74 -0
  61. package/widgets/magic-scroll/styles.js +22 -0
  62. package/widgets/magic-scroll/widget.js +20 -0
  63. package/widgets/magic-select/styles.js +16 -0
  64. package/widgets/magic-select/widget.js +134 -0
  65. package/widgets/magic-table/reducer.js +63 -0
  66. package/widgets/magic-table/styles.js +170 -0
  67. package/widgets/magic-table/widget.js +627 -0
  68. package/widgets/magic-tag/styles.js +32 -0
  69. package/widgets/magic-tag/widget.js +32 -0
  70. package/widgets/magic-text-field/styles.js +58 -0
  71. package/widgets/magic-text-field/widget.js +66 -0
  72. package/widgets/magic-time-field/styles.js +8 -0
  73. package/widgets/magic-time-field/widget.js +142 -0
  74. package/widgets/magic-timer/styles.js +30 -0
  75. package/widgets/magic-timer/widget.js +162 -0
  76. package/widgets/magic-zen/styles.js +61 -0
  77. package/widgets/magic-zen/widget.js +42 -0
  78. package/widgets/main-tabs/styles.js +106 -0
  79. package/widgets/main-tabs/widget.js +23 -0
  80. package/widgets/menu/styles.js +156 -0
  81. package/widgets/menu/test-menu.html +154 -0
  82. package/widgets/menu/widget.js +575 -0
  83. package/widgets/movable/widget.js +80 -0
  84. package/widgets/splitter/styles.js +57 -0
  85. package/widgets/splitter/widget.js +40 -0
  86. package/widgets/tab-layout/styles.js +31 -0
  87. package/widgets/tab-layout/widget.js +59 -0
  88. 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};