flexdesk 0.16.1 → 0.17.1
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 +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- 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 +707 -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-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.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 +3165 -159
- 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 +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -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 +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -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 +696 -71
- 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 +361 -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/tsv.js +74 -0
- 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/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- 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
|
@@ -20,6 +20,7 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
20
20
|
/** @type {number} */ #width;
|
|
21
21
|
/** @type {Function|null} */#onClose;
|
|
22
22
|
/** @type {boolean} */ #open = false;
|
|
23
|
+
/** @type {ResizeObserver|null} */ #hostObserver = null;
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* @param {object} opts
|
|
@@ -131,6 +132,7 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
131
132
|
this.#trackGeometry();
|
|
132
133
|
window.addEventListener('scroll', this.#trackGeometry, true);
|
|
133
134
|
window.addEventListener('resize', this.#trackGeometry);
|
|
135
|
+
this.#observeHost();
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
return this.#body;
|
|
@@ -146,6 +148,8 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
146
148
|
document.removeEventListener('keydown', this.#handleEsc);
|
|
147
149
|
window.removeEventListener('scroll', this.#trackGeometry, true);
|
|
148
150
|
window.removeEventListener('resize', this.#trackGeometry);
|
|
151
|
+
this.#hostObserver?.disconnect();
|
|
152
|
+
this.#hostObserver = null;
|
|
149
153
|
// Leave the inline position:fixed geometry in place. The panel
|
|
150
154
|
// slides off-screen via the transform animation (translateX
|
|
151
155
|
// 100%); the next open() re-runs #trackGeometry which will
|
|
@@ -168,18 +172,51 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
168
172
|
}
|
|
169
173
|
};
|
|
170
174
|
|
|
175
|
+
/** The host moves or resizes without any window event when a tile
|
|
176
|
+
* splitter is dragged or a dock panel toggles, and the fixed geometry
|
|
177
|
+
* would then stay at the old coordinates. Observe the host AND each
|
|
178
|
+
* ancestor: a host can move without changing size of its own (a dock
|
|
179
|
+
* panel toggling beside a fixed-width tile shrinks the content area,
|
|
180
|
+
* an ancestor, and the tile only shifts). The callback runs
|
|
181
|
+
* synchronously, after layout and before paint, so the panel follows
|
|
182
|
+
* a drag without a frame of lag; it only writes the panel's own
|
|
183
|
+
* `position: fixed` geometry, which is out of flow and resizes
|
|
184
|
+
* nothing observed, so it cannot loop. */
|
|
185
|
+
#observeHost() {
|
|
186
|
+
this.#hostObserver = new ResizeObserver(this.#trackGeometry);
|
|
187
|
+
for (let el = this.#container; el; el = el.parentElement) {
|
|
188
|
+
this.#hostObserver.observe(el);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
/** Pin the panel to the right edge of its host's *visible* frame
|
|
172
|
-
* using `position: fixed`. Re-run on every scroll/resize
|
|
173
|
-
*
|
|
174
|
-
* itself sits inside a scrolled
|
|
193
|
+
* using `position: fixed`. Re-run on every window scroll/resize and
|
|
194
|
+
* every host resize (#observeHost) so the panel always overlays the
|
|
195
|
+
* host correctly even when the host itself sits inside a scrolled
|
|
196
|
+
* ancestor.
|
|
197
|
+
*
|
|
198
|
+
* UNDER A CSS `zoom` (the shell's content zoom scales a tile body),
|
|
199
|
+
* `getBoundingClientRect()` answers in screen pixels, which already
|
|
200
|
+
* carry the zoom, while the panel's own `top`/`left`/`height` are
|
|
201
|
+
* CSS pixels that the same zoom multiplies again. Copied across
|
|
202
|
+
* unchanged, every length was scaled twice: at 150% the panel landed
|
|
203
|
+
* right of and below its host, at 50% it was half the host's height.
|
|
204
|
+
* So the rect is divided by the panel's effective zoom
|
|
205
|
+
* (`currentCSSZoom`: 1 when nothing above it zooms, `undefined` in an
|
|
206
|
+
* engine that predates it), and the width subtracted is the panel's
|
|
207
|
+
* laid-out one, which `min-width` may hold above the requested
|
|
208
|
+
* width. Rounding happens in screen pixels, so at 100% the output is
|
|
209
|
+
* exactly what it always was. */
|
|
175
210
|
#trackGeometry = () => {
|
|
176
211
|
if (!this.#open || !this.#panel || !this.#container) return;
|
|
177
212
|
const r = this.#container.getBoundingClientRect();
|
|
213
|
+
const zoom = this.#panel.currentCSSZoom || 1;
|
|
214
|
+
const width = this.#panel.offsetWidth || this.#width;
|
|
178
215
|
const s = this.#panel.style;
|
|
179
216
|
s.position = 'fixed';
|
|
180
|
-
s.top = `${Math.round(r.top)}px`;
|
|
181
|
-
s.left = `${Math.round(r.right -
|
|
217
|
+
s.top = `${Math.round(r.top) / zoom}px`;
|
|
218
|
+
s.left = `${Math.round(r.right - width * zoom) / zoom}px`;
|
|
182
219
|
s.right = 'auto';
|
|
183
|
-
s.height = `${Math.round(r.height)}px`;
|
|
220
|
+
s.height = `${Math.round(r.height) / zoom}px`;
|
|
184
221
|
};
|
|
185
222
|
}
|
|
@@ -1,4 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A range slider with optional companions: a linked number box, a readout, a
|
|
3
|
+
* default with a modified state and a reset button, and an enable toggle.
|
|
4
|
+
*
|
|
5
|
+
* Every companion is opt-in through `field`; with none of them the DOM is the
|
|
6
|
+
* range and the helper line, exactly as before.
|
|
7
|
+
*
|
|
8
|
+
* field.min / max / step / value the range (defaults 0 / 100 / 1 / min)
|
|
9
|
+
* field.numberInput true: an editable number box, two-way synced
|
|
10
|
+
* field.readout true, or (value: number) => string
|
|
11
|
+
* field.defaultValue enables the modified state and a reset button
|
|
12
|
+
* field.toggleable true: an enable checkbox before the slider
|
|
13
|
+
* field.enabled the toggle's initial state (default true)
|
|
14
|
+
* field.disabledValue what onChange reports while toggled off (default null)
|
|
15
|
+
* field.emitOnInput also report every drag frame ({ immediate: true })
|
|
16
|
+
* field.disabled / helperText as before
|
|
17
|
+
*
|
|
18
|
+
* onChange(value, { immediate }) receives the slider's value as a string — the
|
|
19
|
+
* range input's own value, as it always has — or `disabledValue` while the
|
|
20
|
+
* toggle is off. A commit that leaves the reported value where it was reports
|
|
21
|
+
* nothing, so setValue/setRange/setEnabled cannot echo back into the caller
|
|
22
|
+
* that drove them.
|
|
23
|
+
*/
|
|
1
24
|
export class SliderField {
|
|
25
|
+
#min = 0;
|
|
26
|
+
#max = 100;
|
|
27
|
+
#step = 1;
|
|
28
|
+
#value = 0;
|
|
29
|
+
#enabled = true;
|
|
30
|
+
#hasDefault = false;
|
|
31
|
+
#defaultValue = 0;
|
|
32
|
+
|
|
2
33
|
constructor({ fieldId, field = {}, onChange = null, logger = null } = {}) {
|
|
3
34
|
if (!fieldId) {
|
|
4
35
|
throw new Error('SliderField requires a fieldId');
|
|
@@ -10,9 +41,23 @@ export class SliderField {
|
|
|
10
41
|
|
|
11
42
|
this.root = null;
|
|
12
43
|
this.sliderInput = null;
|
|
44
|
+
this.numberInput = null;
|
|
45
|
+
this.readoutEl = null;
|
|
46
|
+
this.resetButton = null;
|
|
47
|
+
this.toggleInput = null;
|
|
13
48
|
this.helperEl = null;
|
|
14
49
|
this.emitOnInput = Boolean(field.emitOnInput ?? field.emitsOnInput);
|
|
15
50
|
this._disposers = [];
|
|
51
|
+
|
|
52
|
+
this.#min = this.#coerceNumber(field.min, 0);
|
|
53
|
+
this.#max = Math.max(this.#min, this.#coerceNumber(field.max, 100));
|
|
54
|
+
this.#step = this.#coercePositiveNumber(field.step, 1);
|
|
55
|
+
this.#value = this.#fit(this.#coerceNumber(field.value, this.#min));
|
|
56
|
+
this.#enabled = field.toggleable ? field.enabled !== false : true;
|
|
57
|
+
this.#hasDefault = this.#isFiniteInput(field.defaultValue);
|
|
58
|
+
if (this.#hasDefault) {
|
|
59
|
+
this.#defaultValue = this.#fit(Number(field.defaultValue));
|
|
60
|
+
}
|
|
16
61
|
}
|
|
17
62
|
|
|
18
63
|
render() {
|
|
@@ -25,13 +70,7 @@ export class SliderField {
|
|
|
25
70
|
this.sliderInput = document.createElement('input');
|
|
26
71
|
this.sliderInput.type = 'range';
|
|
27
72
|
this.sliderInput.className = 'twm-slider-field__input';
|
|
28
|
-
this
|
|
29
|
-
this.sliderInput.max = this.#coerceNumber(this.field.max, 100);
|
|
30
|
-
this.sliderInput.step = this.#coercePositiveNumber(this.field.step, 1);
|
|
31
|
-
this.sliderInput.value = this.#coerceNumber(this.field.value, Number(this.sliderInput.min));
|
|
32
|
-
if (this.field.disabled) {
|
|
33
|
-
this.sliderInput.disabled = true;
|
|
34
|
-
}
|
|
73
|
+
this.#writeRangeAttributes();
|
|
35
74
|
|
|
36
75
|
// Prevent parent interactions from interfering with slider dragging
|
|
37
76
|
// Use stopImmediatePropagation to ensure no other handlers on this element run
|
|
@@ -41,10 +80,8 @@ export class SliderField {
|
|
|
41
80
|
evt.stopImmediatePropagation();
|
|
42
81
|
};
|
|
43
82
|
// Use capture phase to intercept before any parent handlers
|
|
44
|
-
this.sliderInput
|
|
45
|
-
this.sliderInput
|
|
46
|
-
this._disposers.push(() => this.sliderInput.removeEventListener('mousedown', stopDrag, { capture: true }));
|
|
47
|
-
this._disposers.push(() => this.sliderInput.removeEventListener('touchstart', stopDrag, { passive: false, capture: true }));
|
|
83
|
+
this.#listen(this.sliderInput, 'mousedown', stopDrag, { capture: true });
|
|
84
|
+
this.#listen(this.sliderInput, 'touchstart', stopDrag, { passive: false, capture: true });
|
|
48
85
|
|
|
49
86
|
this.helperEl = document.createElement('div');
|
|
50
87
|
this.helperEl.className = 'twm-slider-field__helper';
|
|
@@ -52,7 +89,30 @@ export class SliderField {
|
|
|
52
89
|
this.helperEl.textContent = this.field.helperText;
|
|
53
90
|
}
|
|
54
91
|
|
|
55
|
-
this.
|
|
92
|
+
const companions = this.field.toggleable || this.field.numberInput
|
|
93
|
+
|| this.field.readout || this.#hasDefault;
|
|
94
|
+
if (companions) {
|
|
95
|
+
const row = document.createElement('div');
|
|
96
|
+
row.className = 'twm-slider-field__row';
|
|
97
|
+
if (this.field.toggleable) {
|
|
98
|
+
row.appendChild(this.#buildToggle());
|
|
99
|
+
}
|
|
100
|
+
row.appendChild(this.sliderInput);
|
|
101
|
+
if (this.field.readout) {
|
|
102
|
+
this.readoutEl = document.createElement('span');
|
|
103
|
+
this.readoutEl.className = 'twm-slider-field__readout';
|
|
104
|
+
row.appendChild(this.readoutEl);
|
|
105
|
+
}
|
|
106
|
+
if (this.field.numberInput) {
|
|
107
|
+
row.appendChild(this.#buildNumberInput());
|
|
108
|
+
}
|
|
109
|
+
if (this.#hasDefault) {
|
|
110
|
+
row.appendChild(this.#buildResetButton());
|
|
111
|
+
}
|
|
112
|
+
this.root.appendChild(row);
|
|
113
|
+
} else {
|
|
114
|
+
this.root.appendChild(this.sliderInput);
|
|
115
|
+
}
|
|
56
116
|
this.root.appendChild(this.helperEl);
|
|
57
117
|
|
|
58
118
|
// Throttle onInput to animation frames to avoid re-render stutter while dragging
|
|
@@ -72,6 +132,9 @@ export class SliderField {
|
|
|
72
132
|
};
|
|
73
133
|
|
|
74
134
|
const handleInput = (event) => {
|
|
135
|
+
// The companions follow the thumb on every frame; onChange waits for release.
|
|
136
|
+
this.#value = this.#fit(Number(event.target.value));
|
|
137
|
+
this.#syncCompanions();
|
|
75
138
|
pendingValue = event.target.value;
|
|
76
139
|
if (!rafToken) {
|
|
77
140
|
rafToken = requestAnimationFrame(() => {
|
|
@@ -82,17 +145,71 @@ export class SliderField {
|
|
|
82
145
|
};
|
|
83
146
|
const handleChange = (event) => {
|
|
84
147
|
flushPending();
|
|
85
|
-
|
|
86
|
-
this
|
|
148
|
+
this.#value = this.#fit(Number(event.target.value));
|
|
149
|
+
this.#syncCompanions();
|
|
150
|
+
this.onChange?.(event.target.value, { immediate: false });
|
|
87
151
|
};
|
|
88
|
-
this.sliderInput
|
|
89
|
-
this.sliderInput
|
|
90
|
-
this._disposers.push(() => this.sliderInput.removeEventListener('input', handleInput));
|
|
91
|
-
this._disposers.push(() => this.sliderInput.removeEventListener('change', handleChange));
|
|
152
|
+
this.#listen(this.sliderInput, 'input', handleInput);
|
|
153
|
+
this.#listen(this.sliderInput, 'change', handleChange);
|
|
92
154
|
|
|
155
|
+
this.#syncCompanions();
|
|
93
156
|
return this.root;
|
|
94
157
|
}
|
|
95
158
|
|
|
159
|
+
/** What onChange would report now: the value as a string, or disabledValue while toggled off. */
|
|
160
|
+
getValue() {
|
|
161
|
+
return this.#enabled ? String(this.#value) : this.#disabledValue();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
isEnabled() {
|
|
165
|
+
return this.#enabled;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True when a defaultValue was given and the value differs from it. */
|
|
169
|
+
isModified() {
|
|
170
|
+
return this.#hasDefault && this.#value !== this.#defaultValue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clamp to the range and snap to the step. Reports once, unless `silent` or
|
|
175
|
+
* the reported value did not move. A non-finite value is refused (false).
|
|
176
|
+
*/
|
|
177
|
+
setValue(value, { silent = false } = {}) {
|
|
178
|
+
if (!this.#isFiniteInput(value)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const numeric = Number(value);
|
|
182
|
+
return this.#commit(() => { this.#value = this.#fit(numeric); }, silent);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setEnabled(enabled, { silent = false } = {}) {
|
|
186
|
+
return this.#commit(() => { this.#enabled = Boolean(enabled); }, silent);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Change any of min/max/step; the value and the default are refitted into the new range. */
|
|
190
|
+
setRange({ min, max, step } = {}, { silent = false } = {}) {
|
|
191
|
+
return this.#commit(() => {
|
|
192
|
+
this.#min = this.#isFiniteInput(min) ? Number(min) : this.#min;
|
|
193
|
+
this.#max = Math.max(this.#min, this.#isFiniteInput(max) ? Number(max) : this.#max);
|
|
194
|
+
this.#step = this.#coercePositiveNumber(step, this.#step);
|
|
195
|
+
this.#value = this.#fit(this.#value);
|
|
196
|
+
if (this.#hasDefault) {
|
|
197
|
+
this.#defaultValue = this.#fit(this.#defaultValue);
|
|
198
|
+
}
|
|
199
|
+
if (this.sliderInput) {
|
|
200
|
+
this.#writeRangeAttributes();
|
|
201
|
+
}
|
|
202
|
+
}, silent);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Restore defaultValue and report it, a commit like a drag release. */
|
|
206
|
+
reset({ silent = false } = {}) {
|
|
207
|
+
if (!this.#hasDefault) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return this.#commit(() => { this.#value = this.#defaultValue; }, silent);
|
|
211
|
+
}
|
|
212
|
+
|
|
96
213
|
dispose() {
|
|
97
214
|
this._disposers.forEach((dispose) => {
|
|
98
215
|
try {
|
|
@@ -104,9 +221,149 @@ export class SliderField {
|
|
|
104
221
|
this._disposers = [];
|
|
105
222
|
this.root = null;
|
|
106
223
|
this.sliderInput = null;
|
|
224
|
+
this.numberInput = null;
|
|
225
|
+
this.readoutEl = null;
|
|
226
|
+
this.resetButton = null;
|
|
227
|
+
this.toggleInput = null;
|
|
107
228
|
this.helperEl = null;
|
|
108
229
|
}
|
|
109
230
|
|
|
231
|
+
#buildToggle() {
|
|
232
|
+
this.toggleInput = document.createElement('input');
|
|
233
|
+
this.toggleInput.type = 'checkbox';
|
|
234
|
+
this.toggleInput.className = 'twm-slider-field__toggle';
|
|
235
|
+
this.toggleInput.setAttribute('aria-label', 'Enabled');
|
|
236
|
+
if (this.field.disabled) {
|
|
237
|
+
this.toggleInput.disabled = true;
|
|
238
|
+
}
|
|
239
|
+
this.#listen(this.toggleInput, 'change', () => this.setEnabled(this.toggleInput.checked));
|
|
240
|
+
return this.toggleInput;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#buildNumberInput() {
|
|
244
|
+
this.numberInput = document.createElement('input');
|
|
245
|
+
this.numberInput.type = 'text';
|
|
246
|
+
this.numberInput.inputMode = 'decimal';
|
|
247
|
+
this.numberInput.className = 'twm-slider-field__number';
|
|
248
|
+
this.numberInput.setAttribute('aria-label', 'Value');
|
|
249
|
+
// A typed value that is not a finite number is refused and the box shows the value again.
|
|
250
|
+
const commitTyped = () => {
|
|
251
|
+
if (!this.setValue(this.numberInput.value.trim())) {
|
|
252
|
+
this.#syncCompanions();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
this.#listen(this.numberInput, 'keydown', (event) => {
|
|
256
|
+
if (event.key === 'Enter') {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
commitTyped();
|
|
259
|
+
} else if (event.key === 'Escape') {
|
|
260
|
+
this.#syncCompanions();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
this.#listen(this.numberInput, 'change', commitTyped);
|
|
264
|
+
return this.numberInput;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#buildResetButton() {
|
|
268
|
+
this.resetButton = document.createElement('button');
|
|
269
|
+
this.resetButton.type = 'button';
|
|
270
|
+
this.resetButton.className = 'twm-slider-field__reset';
|
|
271
|
+
this.resetButton.title = 'Reset to default';
|
|
272
|
+
this.resetButton.setAttribute('aria-label', 'Reset to default');
|
|
273
|
+
const icon = document.createElement('span');
|
|
274
|
+
icon.className = 'material-symbols-outlined';
|
|
275
|
+
icon.textContent = 'restart_alt';
|
|
276
|
+
this.resetButton.appendChild(icon);
|
|
277
|
+
this.#listen(this.resetButton, 'click', () => this.reset());
|
|
278
|
+
return this.resetButton;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Apply a change, refresh the DOM, and report once if the reported value moved. */
|
|
282
|
+
#commit(apply, silent) {
|
|
283
|
+
const before = this.getValue();
|
|
284
|
+
apply();
|
|
285
|
+
this.#syncCompanions();
|
|
286
|
+
const after = this.getValue();
|
|
287
|
+
if (after === before) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (!silent) {
|
|
291
|
+
this.onChange?.(after, { immediate: false });
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Write the model into every element: slider, box, readout, toggle, states. */
|
|
297
|
+
#syncCompanions() {
|
|
298
|
+
if (!this.root) return;
|
|
299
|
+
const text = String(this.#value);
|
|
300
|
+
if (this.sliderInput.value !== text) {
|
|
301
|
+
this.sliderInput.value = text;
|
|
302
|
+
}
|
|
303
|
+
const off = !this.#enabled;
|
|
304
|
+
const hardDisabled = Boolean(this.field.disabled);
|
|
305
|
+
this.sliderInput.disabled = hardDisabled || off;
|
|
306
|
+
if (this.numberInput) {
|
|
307
|
+
this.numberInput.value = text;
|
|
308
|
+
this.numberInput.disabled = hardDisabled || off;
|
|
309
|
+
}
|
|
310
|
+
if (this.readoutEl) {
|
|
311
|
+
const format = typeof this.field.readout === 'function' ? this.field.readout : null;
|
|
312
|
+
this.readoutEl.textContent = format ? String(format(this.#value)) : text;
|
|
313
|
+
}
|
|
314
|
+
if (this.toggleInput) {
|
|
315
|
+
this.toggleInput.checked = this.#enabled;
|
|
316
|
+
}
|
|
317
|
+
const modified = this.isModified();
|
|
318
|
+
if (this.resetButton) {
|
|
319
|
+
this.resetButton.disabled = hardDisabled || off || !modified;
|
|
320
|
+
}
|
|
321
|
+
this.root.classList.toggle('twm-slider-field--modified', modified);
|
|
322
|
+
this.root.classList.toggle('twm-slider-field--off', off);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#writeRangeAttributes() {
|
|
326
|
+
this.sliderInput.min = String(this.#min);
|
|
327
|
+
this.sliderInput.max = String(this.#max);
|
|
328
|
+
this.sliderInput.step = String(this.#step);
|
|
329
|
+
this.sliderInput.value = String(this.#value);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Clamp to [min, max] and snap to the step grid anchored at min, as a range input does. */
|
|
333
|
+
#fit(value) {
|
|
334
|
+
const clamped = Math.min(this.#max, Math.max(this.#min, value));
|
|
335
|
+
let snapped = this.#min + Math.round((clamped - this.#min) / this.#step) * this.#step;
|
|
336
|
+
if (snapped > this.#max) snapped -= this.#step;
|
|
337
|
+
const decimals = Math.max(this.#decimals(this.#step), this.#decimals(this.#min));
|
|
338
|
+
return Number(snapped.toFixed(Math.min(decimals, 12)));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#decimals(value) {
|
|
342
|
+
const text = String(value);
|
|
343
|
+
if (text.includes('e-')) return Number(text.split('e-')[1]);
|
|
344
|
+
const dot = text.indexOf('.');
|
|
345
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#isFiniteInput(value) {
|
|
349
|
+
if (value === null || value === undefined || typeof value === 'boolean') {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return Number.isFinite(Number(value));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#disabledValue() {
|
|
359
|
+
return this.field.disabledValue === undefined ? null : this.field.disabledValue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#listen(target, type, handler, options) {
|
|
363
|
+
target.addEventListener(type, handler, options);
|
|
364
|
+
this._disposers.push(() => target.removeEventListener(type, handler, options));
|
|
365
|
+
}
|
|
366
|
+
|
|
110
367
|
#coerceNumber(value, fallback) {
|
|
111
368
|
const numeric = Number(value);
|
|
112
369
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
File without changes
|