flexdesk 0.16.1 → 0.17.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/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard — a multi-step flow: a step indicator, a body slot and a footer with
|
|
3
|
+
* Cancel, Back and Next/Finish, every step gated by its own `canAdvance`.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const wizard = new Wizard({
|
|
7
|
+
* data: { file: null }, // yours; every hook sees it as ctx.data
|
|
8
|
+
* steps: [
|
|
9
|
+
* { id: 'source', label: 'Source', icon: 'upload_file',
|
|
10
|
+
* render(bodyEl, ctx) { … ; return () => { … } }, // optional cleanup function
|
|
11
|
+
* canAdvance: (ctx) => ctx.data.file
|
|
12
|
+
* ? { ok: true } : { ok: false, hint: 'Choose a file first' } },
|
|
13
|
+
* { id: 'review', label: 'Review', render(bodyEl, ctx) { … } },
|
|
14
|
+
* ],
|
|
15
|
+
* onFinish: async (ctx) => { … }, // rejects → the step shows the error
|
|
16
|
+
* onCancel: (ctx) => { … },
|
|
17
|
+
* });
|
|
18
|
+
* wizard.mount(hostEl); // inline …
|
|
19
|
+
* wizard.openInWindow({ id: 'import', title: 'Import' }); // … or in a window
|
|
20
|
+
*
|
|
21
|
+
* Steps are drawn done / current / upcoming / error and joined by a connector.
|
|
22
|
+
* A done (or errored) step BEFORE the current one is a button that jumps back;
|
|
23
|
+
* a step after it is inert, so the only way forward is Next, through the gate.
|
|
24
|
+
*
|
|
25
|
+
* `canAdvance(ctx)` is asked whenever the footer is drawn, and `revalidate()`
|
|
26
|
+
* draws it again — the body is rendered once per visit, never by a revalidate,
|
|
27
|
+
* so an input the user is typing into keeps its focus and its value.
|
|
28
|
+
*
|
|
29
|
+
* Every move goes through a core StateMachine:
|
|
30
|
+
*
|
|
31
|
+
* step ──move──▶ step (guard: back to a visited step, or forward
|
|
32
|
+
* │ one step through the gate)
|
|
33
|
+
* ├──finish──▶ finishing ──resolve──▶ finished (closes)
|
|
34
|
+
* │ │
|
|
35
|
+
* │ └──reject──▶ step (the step shows the error)
|
|
36
|
+
* └──cancel──▶ cancelled (closes, calls onCancel)
|
|
37
|
+
*
|
|
38
|
+
* While `finishing`, every button is disabled and no move is accepted, so an
|
|
39
|
+
* async finish cannot be raced by a click. `finished` and `cancelled` are final:
|
|
40
|
+
* onCancel runs once however many times Cancel is pressed.
|
|
41
|
+
*
|
|
42
|
+
* State lives in this instance only; the DOM carries classes and ARIA, never a
|
|
43
|
+
* data attribute.
|
|
44
|
+
*/
|
|
45
|
+
import { ComponentBase } from '../base/component_base.js';
|
|
46
|
+
import { StateMachine } from '../../core/state_machine.js';
|
|
47
|
+
import { ManagedWindow } from './managed_window.js';
|
|
48
|
+
|
|
49
|
+
const DEFAULT_LABELS = Object.freeze({
|
|
50
|
+
back: 'Back',
|
|
51
|
+
next: 'Next',
|
|
52
|
+
finish: 'Finish',
|
|
53
|
+
cancel: 'Cancel',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const OPEN = 'step';
|
|
57
|
+
const FINISHING = 'finishing';
|
|
58
|
+
const FINISHED = 'finished';
|
|
59
|
+
const CANCELLED = 'cancelled';
|
|
60
|
+
|
|
61
|
+
/** Steps own one override: an error and its message. Everything else about a
|
|
62
|
+
* step's look follows from where it is relative to the current one. */
|
|
63
|
+
const STEP_STATES = Object.freeze(['error']);
|
|
64
|
+
|
|
65
|
+
const isFinal = (state) => state === FINISHED || state === CANCELLED;
|
|
66
|
+
|
|
67
|
+
/** A step's render may hand back a cleanup function, run when the step is
|
|
68
|
+
* left and on dispose(). */
|
|
69
|
+
function runCleanup(cleanup) {
|
|
70
|
+
if (typeof cleanup === 'function') cleanup();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function messageOf(err) {
|
|
74
|
+
if (err && typeof err.message === 'string' && err.message) return err.message;
|
|
75
|
+
return err == null ? 'Failed' : String(err);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class Wizard extends ComponentBase {
|
|
79
|
+
|
|
80
|
+
/** @type {Array<object>} */ #steps;
|
|
81
|
+
/** @type {object} */ #labels;
|
|
82
|
+
/** @type {Function|null} */ #onFinish;
|
|
83
|
+
/** @type {Function|null} */ #onCancel;
|
|
84
|
+
/** @type {object} */ #ctx;
|
|
85
|
+
/** @type {StateMachine} */ #machine;
|
|
86
|
+
/** @type {number} */ #index = 0;
|
|
87
|
+
/** @type {Map<string,{state:string,message:string}>} */ #overrides = new Map();
|
|
88
|
+
/** @type {*} */ #cleanup = null;
|
|
89
|
+
/** @type {Promise} */ #queue = Promise.resolve();
|
|
90
|
+
/** @type {ManagedWindow|null} */ #window = null;
|
|
91
|
+
/** @type {boolean} */ #disposed = false;
|
|
92
|
+
|
|
93
|
+
/** @type {HTMLElement|null} */ #root = null;
|
|
94
|
+
/** @type {HTMLElement[]} */ #stepItems = [];
|
|
95
|
+
/** @type {HTMLButtonElement[]} */#stepButtons = [];
|
|
96
|
+
/** @type {HTMLElement[]} */ #connectors = [];
|
|
97
|
+
/** @type {HTMLElement|null} */ #body = null;
|
|
98
|
+
/** @type {HTMLElement|null} */ #message = null;
|
|
99
|
+
/** @type {HTMLButtonElement|null} */ #cancelBtn = null;
|
|
100
|
+
/** @type {HTMLButtonElement|null} */ #backBtn = null;
|
|
101
|
+
/** @type {HTMLButtonElement|null} */ #nextBtn = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {object} opts
|
|
105
|
+
* @param {Array<{id:string,label:string,icon?:string,
|
|
106
|
+
* render:(bodyEl:HTMLElement, ctx:object)=>(Function|void),
|
|
107
|
+
* canAdvance?:(ctx:object)=>{ok:boolean,hint?:string}}>} opts.steps
|
|
108
|
+
* @param {object} [opts.data={}] Your state; every hook reads it as `ctx.data`.
|
|
109
|
+
* @param {Function} [opts.onFinish] `(ctx) => void|Promise`. A rejection marks the
|
|
110
|
+
* current step as error with the rejection's message.
|
|
111
|
+
* @param {Function} [opts.onCancel] `(ctx) => void`, called once when the wizard is cancelled.
|
|
112
|
+
* @param {object} [opts.labels] `{back, next, finish, cancel}` button labels.
|
|
113
|
+
* @param {object} [opts.eventBus] Passed to the StateMachine (its `fsm:*` events).
|
|
114
|
+
* @param {object} [opts.logger] Passed to the StateMachine.
|
|
115
|
+
*/
|
|
116
|
+
constructor({ steps, data = {}, onFinish = null, onCancel = null, labels = {},
|
|
117
|
+
eventBus = null, logger = null } = {}) {
|
|
118
|
+
super({ eventBus, logger });
|
|
119
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
120
|
+
throw new Error('Wizard requires at least one step');
|
|
121
|
+
}
|
|
122
|
+
const ids = new Set();
|
|
123
|
+
for (const step of steps) {
|
|
124
|
+
if (!step || typeof step.id !== 'string' || !step.id) {
|
|
125
|
+
throw new Error('Wizard: every step needs a non-empty string id');
|
|
126
|
+
}
|
|
127
|
+
if (ids.has(step.id)) throw new Error(`Wizard: duplicate step id "${step.id}"`);
|
|
128
|
+
if (typeof step.render !== 'function') {
|
|
129
|
+
throw new Error(`Wizard: step "${step.id}" needs a render(bodyEl, ctx) function`);
|
|
130
|
+
}
|
|
131
|
+
ids.add(step.id);
|
|
132
|
+
}
|
|
133
|
+
this.#steps = steps.slice();
|
|
134
|
+
this.#labels = { ...DEFAULT_LABELS, ...labels };
|
|
135
|
+
this.#onFinish = onFinish;
|
|
136
|
+
this.#onCancel = onCancel;
|
|
137
|
+
|
|
138
|
+
const wizard = this;
|
|
139
|
+
this.#ctx = Object.freeze({
|
|
140
|
+
data,
|
|
141
|
+
get step() { return wizard.currentStep; },
|
|
142
|
+
get index() { return wizard.currentIndex; },
|
|
143
|
+
revalidate: () => this.revalidate(),
|
|
144
|
+
next: () => this.next(),
|
|
145
|
+
back: () => this.back(),
|
|
146
|
+
goTo: (target) => this.goTo(target),
|
|
147
|
+
setStepState: (id, state, message) => this.setStepState(id, state, message),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
this.#machine = new StateMachine({
|
|
151
|
+
name: 'wizard',
|
|
152
|
+
initialState: OPEN,
|
|
153
|
+
eventBus,
|
|
154
|
+
logger,
|
|
155
|
+
states: {
|
|
156
|
+
[OPEN]: {
|
|
157
|
+
transitions: {
|
|
158
|
+
move: {
|
|
159
|
+
target: OPEN,
|
|
160
|
+
guard: ({ payload }) => this.#canMoveTo(payload.to),
|
|
161
|
+
action: ({ payload }) => this.#enterStep(payload.to),
|
|
162
|
+
},
|
|
163
|
+
finish: {
|
|
164
|
+
target: FINISHING,
|
|
165
|
+
guard: () => this.#isLast() && this.#gate().ok,
|
|
166
|
+
action: () => { this.#overrides.delete(this.currentStep); },
|
|
167
|
+
},
|
|
168
|
+
cancel: { target: CANCELLED },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
[FINISHING]: {
|
|
172
|
+
transitions: {
|
|
173
|
+
resolve: { target: FINISHED },
|
|
174
|
+
reject: {
|
|
175
|
+
target: OPEN,
|
|
176
|
+
action: ({ payload }) => {
|
|
177
|
+
this.#overrides.set(this.currentStep,
|
|
178
|
+
{ state: 'error', message: messageOf(payload.error) });
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
[FINISHED]: { onEnter: () => this.#close() },
|
|
184
|
+
[CANCELLED]: {
|
|
185
|
+
onEnter: () => {
|
|
186
|
+
this.#close();
|
|
187
|
+
this.#onCancel?.(this.#ctx);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Mount inline into `container` and render the first step.
|
|
198
|
+
* @param {HTMLElement} container
|
|
199
|
+
* @returns {Wizard}
|
|
200
|
+
*/
|
|
201
|
+
mount(container) {
|
|
202
|
+
if (this._mounted) return this;
|
|
203
|
+
this.#build();
|
|
204
|
+
container.appendChild(this.#root);
|
|
205
|
+
this._mounted = true;
|
|
206
|
+
this.#renderBody();
|
|
207
|
+
this.#paint();
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Mount in a ManagedWindow and show it. The window's close button and
|
|
213
|
+
* Escape cancel the wizard (and are refused while a finish is running);
|
|
214
|
+
* finishing or cancelling closes the window.
|
|
215
|
+
* @param {object} [opts] ManagedWindow options: id, title, icon, defaultWidth,
|
|
216
|
+
* defaultHeight, minWidth, minHeight, container, modal (default true).
|
|
217
|
+
* @returns {ManagedWindow}
|
|
218
|
+
*/
|
|
219
|
+
openInWindow({ id = 'twm-wizard', title = 'Wizard', icon, defaultWidth = 720,
|
|
220
|
+
defaultHeight = 520, minWidth = 420, minHeight = 320,
|
|
221
|
+
container, modal = true } = {}) {
|
|
222
|
+
if (this._mounted) throw new Error('Wizard is already mounted');
|
|
223
|
+
this.#build();
|
|
224
|
+
this._mounted = true;
|
|
225
|
+
this.#window = new ManagedWindow({
|
|
226
|
+
id, title, icon, defaultWidth, defaultHeight, minWidth, minHeight,
|
|
227
|
+
container, modal,
|
|
228
|
+
content: this.#root,
|
|
229
|
+
canMinimize: false,
|
|
230
|
+
beforeClose: () => {
|
|
231
|
+
if (isFinal(this.state)) return true;
|
|
232
|
+
void this.cancel();
|
|
233
|
+
return false;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
this.#window.show();
|
|
237
|
+
this.#renderBody();
|
|
238
|
+
this.#paint();
|
|
239
|
+
return this.#window;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Tear down (and close the window, if any) without calling any callback.
|
|
243
|
+
* Every later move, finish or cancel is refused. */
|
|
244
|
+
dispose() {
|
|
245
|
+
if (this.#disposed) return;
|
|
246
|
+
this.#disposed = true;
|
|
247
|
+
const win = this.#window;
|
|
248
|
+
this.#teardown();
|
|
249
|
+
win?.close({ force: true });
|
|
250
|
+
super.dispose();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/** The wizard's root element (null before mount). */
|
|
256
|
+
get element() { return this.#root; }
|
|
257
|
+
|
|
258
|
+
/** The body slot the current step renders into. */
|
|
259
|
+
get bodyElement() { return this.#body; }
|
|
260
|
+
|
|
261
|
+
/** The ManagedWindow, when opened with `openInWindow`. */
|
|
262
|
+
get window() { return this.#window; }
|
|
263
|
+
|
|
264
|
+
/** The object every hook receives: `{data, step, index, revalidate, next, back, goTo, setStepState}`. */
|
|
265
|
+
get context() { return this.#ctx; }
|
|
266
|
+
|
|
267
|
+
/** The current step's id. */
|
|
268
|
+
get currentStep() { return this.#steps[this.#index].id; }
|
|
269
|
+
|
|
270
|
+
/** The current step's index. */
|
|
271
|
+
get currentIndex() { return this.#index; }
|
|
272
|
+
|
|
273
|
+
/** 'step' | 'finishing' | 'finished' | 'cancelled' (null for a moment after construction). */
|
|
274
|
+
get state() { return this.#machine.getState(); }
|
|
275
|
+
|
|
276
|
+
/** True while an async finish is running. */
|
|
277
|
+
get busy() { return this.state === FINISHING; }
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Ask the current step's `canAdvance` again and redraw the indicator and
|
|
281
|
+
* the footer. The body is NOT re-rendered.
|
|
282
|
+
*/
|
|
283
|
+
revalidate() {
|
|
284
|
+
this.#paint();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Forward one step, if the current step's gate allows it. Resolves true when it moved. */
|
|
288
|
+
next() {
|
|
289
|
+
return this.#move(() => this.#index + 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Back one step. Resolves true when it moved. */
|
|
293
|
+
back() {
|
|
294
|
+
return this.#move(() => this.#index - 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Move to a step by id or index: any step before the current one, or the
|
|
299
|
+
* next one through the gate. Resolves true when it moved.
|
|
300
|
+
* @param {string|number} target
|
|
301
|
+
*/
|
|
302
|
+
goTo(target) {
|
|
303
|
+
return this.#move(() => this.#resolveIndex(target));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Mark a step as errored (`'error'`, with a message shown while it is the
|
|
308
|
+
* current step), or clear the mark (`null`).
|
|
309
|
+
* @param {string} id
|
|
310
|
+
* @param {'error'|null} state
|
|
311
|
+
* @param {string} [message]
|
|
312
|
+
*/
|
|
313
|
+
setStepState(id, state, message = '') {
|
|
314
|
+
const i = this.#resolveIndex(id);
|
|
315
|
+
if (i < 0 || i >= this.#steps.length) throw new Error(`Wizard: no step "${id}"`);
|
|
316
|
+
const key = this.#steps[i].id;
|
|
317
|
+
if (state == null) this.#overrides.delete(key);
|
|
318
|
+
else if (STEP_STATES.includes(state)) this.#overrides.set(key, { state, message: String(message ?? '') });
|
|
319
|
+
else throw new Error(`Wizard: unknown step state "${state}" (expected 'error' or null)`);
|
|
320
|
+
this.#paint();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Run `onFinish` from the last step, if its gate allows. The buttons are
|
|
325
|
+
* disabled until it settles; resolved, the wizard closes; rejected, the
|
|
326
|
+
* step is marked as error with the message. Resolves true when it finished.
|
|
327
|
+
*/
|
|
328
|
+
finish() {
|
|
329
|
+
return this.#enqueue(async () => {
|
|
330
|
+
if (this.state !== OPEN) return false;
|
|
331
|
+
const { transitioned } = await this.#machine.transition('finish');
|
|
332
|
+
if (!transitioned) return false;
|
|
333
|
+
this.#paint();
|
|
334
|
+
try {
|
|
335
|
+
await this.#onFinish?.(this.#ctx);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
await this.#machine.transition('reject', { error });
|
|
338
|
+
this.#paint();
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
await this.#machine.transition('resolve');
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Close the wizard and call `onCancel` — once. Refused while a finish is
|
|
348
|
+
* running. Resolves true when it cancelled.
|
|
349
|
+
*/
|
|
350
|
+
cancel() {
|
|
351
|
+
return this.#enqueue(async () => {
|
|
352
|
+
if (this.state !== OPEN) return false;
|
|
353
|
+
const { transitioned } = await this.#machine.transition('cancel');
|
|
354
|
+
return transitioned;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Moves ──────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/** Moves run one at a time, so a double click cannot start a transition
|
|
361
|
+
* inside another. The target is computed when the move runs, not when it
|
|
362
|
+
* was asked for. A request made while a finish is running (or after the
|
|
363
|
+
* wizard closed) is refused there and then — it is not queued behind the
|
|
364
|
+
* finish to run once it settles. */
|
|
365
|
+
#enqueue(fn) {
|
|
366
|
+
if (this.#disposed || this.busy || isFinal(this.state)) return Promise.resolve(false);
|
|
367
|
+
const run = this.#queue.then(() => (this.#disposed ? false : fn()));
|
|
368
|
+
this.#queue = run.catch(() => {});
|
|
369
|
+
return run;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#move(targetOf) {
|
|
373
|
+
return this.#enqueue(async () => {
|
|
374
|
+
if (this.state !== OPEN) return false;
|
|
375
|
+
const to = targetOf();
|
|
376
|
+
if (!this.#canMoveTo(to)) return false;
|
|
377
|
+
const { transitioned } = await this.#machine.transition('move', { to });
|
|
378
|
+
this.#paint();
|
|
379
|
+
return transitioned;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#canMoveTo(to) {
|
|
384
|
+
if (!Number.isInteger(to) || to < 0 || to >= this.#steps.length) return false;
|
|
385
|
+
if (to < this.#index) return true;
|
|
386
|
+
return to === this.#index + 1 && this.#gate().ok;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#enterStep(to) {
|
|
390
|
+
this.#index = to;
|
|
391
|
+
this.#renderBody();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#resolveIndex(target) {
|
|
395
|
+
if (typeof target === 'number') return Number.isInteger(target) ? target : -1;
|
|
396
|
+
return this.#steps.findIndex((s) => s.id === target);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#isLast() {
|
|
400
|
+
return this.#index === this.#steps.length - 1;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The current step's gate, normalised to `{ok, hint}`. */
|
|
404
|
+
#gate() {
|
|
405
|
+
const step = this.#steps[this.#index];
|
|
406
|
+
if (typeof step.canAdvance !== 'function') return { ok: true, hint: '' };
|
|
407
|
+
const r = step.canAdvance(this.#ctx) ?? {};
|
|
408
|
+
return { ok: r.ok === true, hint: typeof r.hint === 'string' ? r.hint : '' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── DOM ────────────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
#build() {
|
|
414
|
+
const root = document.createElement('div');
|
|
415
|
+
root.className = 'twm-wizard';
|
|
416
|
+
|
|
417
|
+
const steps = document.createElement('ol');
|
|
418
|
+
steps.className = 'twm-wizard__steps';
|
|
419
|
+
this.#steps.forEach((step, i) => {
|
|
420
|
+
if (i > 0) {
|
|
421
|
+
const connector = document.createElement('li');
|
|
422
|
+
connector.className = 'twm-wizard__connector';
|
|
423
|
+
connector.setAttribute('role', 'presentation');
|
|
424
|
+
connector.setAttribute('aria-hidden', 'true');
|
|
425
|
+
steps.appendChild(connector);
|
|
426
|
+
this.#connectors.push(connector);
|
|
427
|
+
}
|
|
428
|
+
const item = document.createElement('li');
|
|
429
|
+
item.className = 'twm-wizard__step';
|
|
430
|
+
const btn = document.createElement('button');
|
|
431
|
+
btn.type = 'button';
|
|
432
|
+
btn.className = 'twm-wizard__step-btn';
|
|
433
|
+
const marker = document.createElement('span');
|
|
434
|
+
marker.className = 'twm-wizard__step-marker';
|
|
435
|
+
const label = document.createElement('span');
|
|
436
|
+
label.className = 'twm-wizard__step-label';
|
|
437
|
+
label.textContent = step.label ?? step.id;
|
|
438
|
+
btn.append(marker, label);
|
|
439
|
+
btn.addEventListener('click', () => this.#onStepClick(i));
|
|
440
|
+
item.appendChild(btn);
|
|
441
|
+
steps.appendChild(item);
|
|
442
|
+
this.#stepItems.push(item);
|
|
443
|
+
this.#stepButtons.push(btn);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
this.#body = document.createElement('div');
|
|
447
|
+
this.#body.className = 'twm-wizard__body';
|
|
448
|
+
|
|
449
|
+
const footer = document.createElement('div');
|
|
450
|
+
footer.className = 'twm-wizard__footer';
|
|
451
|
+
this.#message = document.createElement('div');
|
|
452
|
+
this.#message.className = 'twm-wizard__message';
|
|
453
|
+
this.#message.setAttribute('aria-live', 'polite');
|
|
454
|
+
|
|
455
|
+
const actions = document.createElement('div');
|
|
456
|
+
actions.className = 'twm-wizard__actions';
|
|
457
|
+
this.#cancelBtn = this.#button('twm-wizard__cancel', this.#labels.cancel, () => { void this.cancel(); });
|
|
458
|
+
this.#backBtn = this.#button('twm-wizard__back', this.#labels.back, () => { void this.back(); });
|
|
459
|
+
this.#nextBtn = this.#button('twm-wizard__next twm-btn--primary', this.#labels.next, () => {
|
|
460
|
+
void (this.#isLast() ? this.finish() : this.next());
|
|
461
|
+
});
|
|
462
|
+
actions.append(this.#cancelBtn, this.#backBtn, this.#nextBtn);
|
|
463
|
+
footer.append(this.#message, actions);
|
|
464
|
+
|
|
465
|
+
root.append(steps, this.#body, footer);
|
|
466
|
+
this.#root = root;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#button(cls, text, onClick) {
|
|
470
|
+
const btn = document.createElement('button');
|
|
471
|
+
btn.type = 'button';
|
|
472
|
+
btn.className = `twm-btn ${cls}`;
|
|
473
|
+
btn.textContent = text;
|
|
474
|
+
btn.addEventListener('click', onClick);
|
|
475
|
+
return btn;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#onStepClick(i) {
|
|
479
|
+
if (i < this.#index) void this.goTo(i);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#renderBody() {
|
|
483
|
+
runCleanup(this.#cleanup);
|
|
484
|
+
this.#cleanup = null;
|
|
485
|
+
this.#body.replaceChildren();
|
|
486
|
+
const step = this.#steps[this.#index];
|
|
487
|
+
this.#cleanup = step.render(this.#body, this.#ctx) ?? null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The visual state of step `i`: an override, else its position. */
|
|
491
|
+
#stepState(i) {
|
|
492
|
+
const override = this.#overrides.get(this.#steps[i].id);
|
|
493
|
+
if (override) return override.state;
|
|
494
|
+
if (i < this.#index) return 'done';
|
|
495
|
+
return i === this.#index ? 'current' : 'upcoming';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#paint() {
|
|
499
|
+
if (!this.#root) return;
|
|
500
|
+
const busy = this.busy;
|
|
501
|
+
this.#root.classList.toggle('twm-wizard--busy', busy);
|
|
502
|
+
this.#root.setAttribute('aria-busy', busy ? 'true' : 'false');
|
|
503
|
+
|
|
504
|
+
this.#steps.forEach((step, i) => {
|
|
505
|
+
const state = this.#stepState(i);
|
|
506
|
+
const item = this.#stepItems[i];
|
|
507
|
+
for (const s of ['done', 'current', 'upcoming', 'error']) {
|
|
508
|
+
item.classList.toggle(`twm-wizard__step--${s}`, s === state);
|
|
509
|
+
}
|
|
510
|
+
item.classList.toggle('twm-wizard__step--active', i === this.#index);
|
|
511
|
+
const btn = this.#stepButtons[i];
|
|
512
|
+
btn.disabled = busy || i >= this.#index;
|
|
513
|
+
if (i === this.#index) btn.setAttribute('aria-current', 'step');
|
|
514
|
+
else btn.removeAttribute('aria-current');
|
|
515
|
+
const override = this.#overrides.get(step.id);
|
|
516
|
+
btn.title = override?.message || step.label || step.id;
|
|
517
|
+
|
|
518
|
+
const marker = btn.firstChild;
|
|
519
|
+
const glyph = state === 'done' ? 'check' : state === 'error' ? 'error' : (step.icon || '');
|
|
520
|
+
marker.classList.toggle('material-symbols-outlined', Boolean(glyph));
|
|
521
|
+
marker.textContent = glyph || String(i + 1);
|
|
522
|
+
if (i > 0) {
|
|
523
|
+
this.#connectors[i - 1].classList.toggle('twm-wizard__connector--done', i <= this.#index);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const gate = this.#gate();
|
|
528
|
+
const last = this.#isLast();
|
|
529
|
+
this.#nextBtn.textContent = last ? this.#labels.finish : this.#labels.next;
|
|
530
|
+
this.#nextBtn.disabled = busy || !gate.ok;
|
|
531
|
+
this.#nextBtn.title = gate.ok ? '' : gate.hint;
|
|
532
|
+
this.#backBtn.disabled = busy || this.#index === 0;
|
|
533
|
+
this.#cancelBtn.disabled = busy;
|
|
534
|
+
|
|
535
|
+
const error = this.#overrides.get(this.currentStep);
|
|
536
|
+
const text = error ? error.message : (gate.ok ? '' : gate.hint);
|
|
537
|
+
this.#message.textContent = text;
|
|
538
|
+
this.#message.classList.toggle('twm-wizard__message--error', Boolean(error));
|
|
539
|
+
this.#message.classList.toggle('twm-wizard__message--hint', !error && Boolean(text));
|
|
540
|
+
if (error) this.#message.setAttribute('role', 'alert');
|
|
541
|
+
else this.#message.removeAttribute('role');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#close() {
|
|
545
|
+
const win = this.#window;
|
|
546
|
+
this.#teardown();
|
|
547
|
+
this._mounted = false;
|
|
548
|
+
win?.close({ force: true });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#teardown() {
|
|
552
|
+
runCleanup(this.#cleanup);
|
|
553
|
+
this.#cleanup = null;
|
|
554
|
+
this.#root?.remove();
|
|
555
|
+
this.#root = null;
|
|
556
|
+
this.#body = null;
|
|
557
|
+
this.#message = null;
|
|
558
|
+
this.#stepItems = [];
|
|
559
|
+
this.#stepButtons = [];
|
|
560
|
+
this.#connectors = [];
|
|
561
|
+
this.#cancelBtn = this.#backBtn = this.#nextBtn = null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
@@ -99,7 +99,7 @@ export class WindowChromeController {
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Draw minimise, maximise/restore and close, unless the page has its own
|
|
102
|
-
* `#win-*` buttons (
|
|
102
|
+
* `#win-*` buttons (an embedder's own markup), which are then wired instead.
|
|
103
103
|
*/
|
|
104
104
|
#createWindowControls() {
|
|
105
105
|
if (document.getElementById('win-minimize') || document.getElementById('win-close')) return;
|
|
@@ -403,7 +403,7 @@ export class WindowChromeController {
|
|
|
403
403
|
|
|
404
404
|
// What never starts a drag: anything you can click or type into. An
|
|
405
405
|
// embedder adds its own with `noDrag`. (`.window-controls` and
|
|
406
|
-
// `.panel-toggles` are kept for
|
|
406
|
+
// `.panel-toggles` are kept for embedders' own markup of that shape.)
|
|
407
407
|
const NO_DRAG_SELECTOR = [
|
|
408
408
|
'.no-drag', 'button', 'a', 'input', 'select', 'textarea', 'label',
|
|
409
409
|
'[role="button"]', '[role="menuitem"]', '[role="tab"]', '[contenteditable="true"]',
|