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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorField — one colour: a swatch that opens the native picker, a hex text
|
|
3
|
+
* box kept in step with it, and an optional row of preset swatches.
|
|
4
|
+
*
|
|
5
|
+
* const field = new ColorField({
|
|
6
|
+
* value: 'rgb(31, 119, 180)', // shown as #1f77b4
|
|
7
|
+
* presets: ['#1f77b4', '#ff7f0e', '#2ca02c'],
|
|
8
|
+
* onChange: (hex, { commit }) => { ... },
|
|
9
|
+
* });
|
|
10
|
+
* host.appendChild(field.render());
|
|
11
|
+
* field.getValue(); // '#1f77b4' | '' | null
|
|
12
|
+
*
|
|
13
|
+
* THE HEX BOX IS THE CONTROL. Picking in the swatch writes the box and fires
|
|
14
|
+
* the box's own `input` (and, when the picker commits, `change`); a preset
|
|
15
|
+
* does the same. So there is one path from any gesture to the value, and a
|
|
16
|
+
* host listening on the box — `openForm` does, by its `name` — hears every
|
|
17
|
+
* change whatever made it. Typing a valid hex moves the swatch; typing an
|
|
18
|
+
* invalid one leaves the swatch on the last good colour and marks the field
|
|
19
|
+
* `.twm-color-field--invalid`.
|
|
20
|
+
*
|
|
21
|
+
* VALUES. `getValue()` is the colour as lowercase `#rrggbb`, `''` when the box
|
|
22
|
+
* is empty, and `null` when what is typed is not a hex colour (`#rgb` and
|
|
23
|
+
* `#rrggbb` are). An `rgb()` / `rgba()` value handed IN — a default, a preset,
|
|
24
|
+
* `setValue` — is converted to its hex equivalent first; alpha is dropped,
|
|
25
|
+
* because the native picker has no alpha channel.
|
|
26
|
+
*
|
|
27
|
+
* `onChange(value, { commit })` runs on every edit with `commit: false` and on
|
|
28
|
+
* the box's `change` (blur, Enter, a preset, the picker closing) with
|
|
29
|
+
* `commit: true`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
33
|
+
const RGB_RE = /^rgba?\(\s*([^)]*)\)$/i;
|
|
34
|
+
|
|
35
|
+
/** True for `#rgb` and `#rrggbb` (any case), and nothing else. */
|
|
36
|
+
export function isHexColor(value) {
|
|
37
|
+
return typeof value === 'string' && HEX_RE.test(value.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A colour as lowercase `#rrggbb`, or null when `value` is not one.
|
|
42
|
+
* Accepts `#rgb`, `#rrggbb` and `rgb()` / `rgba()` in comma or space syntax,
|
|
43
|
+
* with channels as 0–255 or percentages; an alpha channel is dropped.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeHex(value) {
|
|
46
|
+
if (typeof value !== 'string') return null;
|
|
47
|
+
const s = value.trim();
|
|
48
|
+
if (HEX_RE.test(s)) {
|
|
49
|
+
const h = s.slice(1).toLowerCase();
|
|
50
|
+
return h.length === 3 ? `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}` : `#${h}`;
|
|
51
|
+
}
|
|
52
|
+
const m = RGB_RE.exec(s);
|
|
53
|
+
if (!m) return null;
|
|
54
|
+
const parts = m[1].split(/\s*[,/]\s*|\s+/).filter(Boolean);
|
|
55
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
56
|
+
const channels = [];
|
|
57
|
+
for (const p of parts.slice(0, 3)) {
|
|
58
|
+
const pct = p.endsWith('%');
|
|
59
|
+
const n = Number(pct ? p.slice(0, -1) : p);
|
|
60
|
+
if (!Number.isFinite(n)) return null;
|
|
61
|
+
const v = pct ? (n / 100) * 255 : n;
|
|
62
|
+
if (v < 0 || v > 255) return null;
|
|
63
|
+
channels.push(Math.round(v));
|
|
64
|
+
}
|
|
65
|
+
return `#${channels.map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What the box shows for a value handed in: its hex if it is a colour, else as given. */
|
|
69
|
+
function displayText(value) {
|
|
70
|
+
if (value == null) return '';
|
|
71
|
+
return normalizeHex(String(value)) ?? String(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ColorField {
|
|
75
|
+
/**
|
|
76
|
+
* @param {Object} [o]
|
|
77
|
+
* @param {string} [o.value] initial colour (hex or rgb()); '' for none
|
|
78
|
+
* @param {string[]} [o.presets] preset colours drawn as buttons; ones that
|
|
79
|
+
* are not colours are left out
|
|
80
|
+
* @param {string} [o.name] `name` of the hex box, for a host form
|
|
81
|
+
* @param {string} [o.label] accessible name of the swatch and the box
|
|
82
|
+
* @param {Function} [o.onChange] `(value, { commit })`, value as `getValue()`
|
|
83
|
+
*/
|
|
84
|
+
constructor({ value = '', presets = [], name = null, label = null, onChange = null } = {}) {
|
|
85
|
+
this.initial = displayText(value);
|
|
86
|
+
this.presets = (Array.isArray(presets) ? presets : []).map(normalizeHex).filter(Boolean);
|
|
87
|
+
this.name = name;
|
|
88
|
+
this.label = label;
|
|
89
|
+
this.onChange = typeof onChange === 'function' ? onChange : null;
|
|
90
|
+
|
|
91
|
+
this.root = null;
|
|
92
|
+
this.swatch = null;
|
|
93
|
+
this.hex = null;
|
|
94
|
+
this._disposers = [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
if (this.root) return this.root;
|
|
99
|
+
const root = document.createElement('div');
|
|
100
|
+
root.className = 'twm-color-field';
|
|
101
|
+
|
|
102
|
+
const swatch = document.createElement('input');
|
|
103
|
+
swatch.type = 'color';
|
|
104
|
+
swatch.className = 'twm-color-field__swatch';
|
|
105
|
+
swatch.value = normalizeHex(this.initial) ?? '#000000';
|
|
106
|
+
if (this.label) swatch.title = this.label;
|
|
107
|
+
|
|
108
|
+
const hex = document.createElement('input');
|
|
109
|
+
hex.type = 'text';
|
|
110
|
+
hex.className = 'twm-color-field__hex';
|
|
111
|
+
hex.value = this.initial;
|
|
112
|
+
hex.placeholder = '#rrggbb';
|
|
113
|
+
hex.spellcheck = false;
|
|
114
|
+
hex.autocomplete = 'off';
|
|
115
|
+
hex.maxLength = 7;
|
|
116
|
+
if (this.name) hex.name = this.name;
|
|
117
|
+
if (this.label) hex.setAttribute('aria-label', this.label);
|
|
118
|
+
|
|
119
|
+
root.append(swatch, hex);
|
|
120
|
+
this.root = root;
|
|
121
|
+
this.swatch = swatch;
|
|
122
|
+
this.hex = hex;
|
|
123
|
+
|
|
124
|
+
if (this.presets.length) {
|
|
125
|
+
const strip = document.createElement('div');
|
|
126
|
+
strip.className = 'twm-color-field__presets';
|
|
127
|
+
for (const color of this.presets) {
|
|
128
|
+
const btn = document.createElement('button');
|
|
129
|
+
btn.type = 'button';
|
|
130
|
+
btn.className = 'twm-color-field__preset';
|
|
131
|
+
btn.style.backgroundColor = color;
|
|
132
|
+
btn.title = color;
|
|
133
|
+
btn.setAttribute('aria-label', color);
|
|
134
|
+
this.#listen(btn, 'click', () => this.#writeBox(color, true));
|
|
135
|
+
strip.appendChild(btn);
|
|
136
|
+
}
|
|
137
|
+
root.appendChild(strip);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Every gesture lands on the box; the box's listeners do the rest.
|
|
141
|
+
this.#listen(swatch, 'input', () => this.#writeBox(swatch.value, false));
|
|
142
|
+
this.#listen(swatch, 'change', () => this.#writeBox(swatch.value, true));
|
|
143
|
+
this.#listen(hex, 'input', () => this.#sync(false));
|
|
144
|
+
this.#listen(hex, 'change', () => {
|
|
145
|
+
const v = this.getValue();
|
|
146
|
+
if (v) hex.value = v; // canonical form once committed
|
|
147
|
+
this.#sync(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
this.#sync(null);
|
|
151
|
+
return root;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `#rrggbb`, `''` when empty, or `null` when the box holds no hex colour. */
|
|
155
|
+
getValue() {
|
|
156
|
+
const text = this.getText().trim();
|
|
157
|
+
if (text === '') return '';
|
|
158
|
+
return isHexColor(text) ? normalizeHex(text) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The box as it stands. */
|
|
162
|
+
getText() {
|
|
163
|
+
return this.hex ? this.hex.value : this.initial;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
isValid() {
|
|
167
|
+
return this.getValue() !== null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Write a colour (hex or rgb()) without firing `onChange`. */
|
|
171
|
+
setValue(value) {
|
|
172
|
+
const text = displayText(value);
|
|
173
|
+
if (!this.hex) { this.initial = text; return; }
|
|
174
|
+
this.hex.value = text;
|
|
175
|
+
this.#sync(null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
focus() {
|
|
179
|
+
this.hex?.focus();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
dispose() {
|
|
183
|
+
for (const d of this._disposers) {
|
|
184
|
+
try { d(); } catch { /* listener already gone */ }
|
|
185
|
+
}
|
|
186
|
+
this._disposers = [];
|
|
187
|
+
this.root = null;
|
|
188
|
+
this.swatch = null;
|
|
189
|
+
this.hex = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#listen(el, type, fn) {
|
|
193
|
+
el.addEventListener(type, fn);
|
|
194
|
+
this._disposers.push(() => el.removeEventListener(type, fn));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Put `text` in the box and fire the box's own events, as typing would. */
|
|
198
|
+
#writeBox(text, commit) {
|
|
199
|
+
this.hex.value = text;
|
|
200
|
+
this.hex.dispatchEvent(new Event('input', { bubbles: true }));
|
|
201
|
+
if (commit) this.hex.dispatchEvent(new Event('change', { bubbles: true }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Swatch and invalid mark follow the box; `commit === null` is silent. */
|
|
205
|
+
#sync(commit) {
|
|
206
|
+
const v = this.getValue();
|
|
207
|
+
if (v) this.swatch.value = v;
|
|
208
|
+
this.root.classList.toggle('twm-color-field--invalid', v === null);
|
|
209
|
+
if (commit !== null) this.onChange?.(v, { commit });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorListField — an ordered list of colours: add, remove, and reorder by
|
|
3
|
+
* the grip (drag) or the up / down buttons, each row a `ColorField`.
|
|
4
|
+
*
|
|
5
|
+
* const list = new ColorListField({
|
|
6
|
+
* value: ['#1f77b4', 'rgb(255, 127, 14)'],
|
|
7
|
+
* onChange: (colors, { commit }) => { ... },
|
|
8
|
+
* });
|
|
9
|
+
* host.appendChild(list.render());
|
|
10
|
+
* list.getValue(); // ['#1f77b4', '#ff7f0e'] — or null, see below
|
|
11
|
+
*
|
|
12
|
+
* The list is a `SortableList` (the same add / remove / drag-reorder every
|
|
13
|
+
* list in the library uses) whose rows render a `ColorField` and a pair of
|
|
14
|
+
* move buttons, so the order can be changed without a pointer.
|
|
15
|
+
*
|
|
16
|
+
* `getValue()` is the colours in their current order as lowercase `#rrggbb`,
|
|
17
|
+
* or `null` while any row holds something that is not a hex colour —
|
|
18
|
+
* `invalidIndexes()` says which. Empty rows count as invalid: a list entry
|
|
19
|
+
* with no colour is not a colour.
|
|
20
|
+
*
|
|
21
|
+
* Given a `name`, the list also carries a hidden `<input name>` whose value is
|
|
22
|
+
* the rows as JSON, and fires `input` on it for every edit and `change` for
|
|
23
|
+
* every committed one (a row's `change`, add, remove, move). That is the
|
|
24
|
+
* surface a host form listens on; `openForm` reads the list through
|
|
25
|
+
* `getValue()`, not through that input.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { SortableList } from './sortable_list.js';
|
|
29
|
+
import { ColorField, isHexColor, normalizeHex } from './color_field.js';
|
|
30
|
+
|
|
31
|
+
let _seq = 0;
|
|
32
|
+
|
|
33
|
+
/** A row's text as `#rrggbb`, or null when it is not a hex colour. */
|
|
34
|
+
const hexOf = (text) => {
|
|
35
|
+
const t = String(text ?? '').trim();
|
|
36
|
+
return isHexColor(t) ? normalizeHex(t) : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class ColorListField {
|
|
40
|
+
/**
|
|
41
|
+
* @param {Object} [o]
|
|
42
|
+
* @param {string[]} [o.value] initial colours (hex or rgb())
|
|
43
|
+
* @param {string[]} [o.presets] preset colours offered on every row
|
|
44
|
+
* @param {string} [o.newColor] colour a newly added row starts on
|
|
45
|
+
* @param {string} [o.addLabel] text of the add button
|
|
46
|
+
* @param {string} [o.name] `name` of the hidden input, for a host form
|
|
47
|
+
* @param {Function} [o.onChange] `(colors, { commit })`, colors as `getValue()`
|
|
48
|
+
*/
|
|
49
|
+
constructor({
|
|
50
|
+
value = [], presets = [], newColor = '#000000', addLabel = 'Add colour',
|
|
51
|
+
name = null, onChange = null,
|
|
52
|
+
} = {}) {
|
|
53
|
+
this.presets = Array.isArray(presets) ? presets : [];
|
|
54
|
+
// Converted once, as #toItems does, so an added row is what it shows.
|
|
55
|
+
this.newColor = normalizeHex(String(newColor ?? '')) ?? String(newColor ?? '');
|
|
56
|
+
this.addLabel = addLabel;
|
|
57
|
+
this.name = name;
|
|
58
|
+
this.onChange = typeof onChange === 'function' ? onChange : null;
|
|
59
|
+
|
|
60
|
+
/** Row index -> its ColorField; always exactly the rendered rows. */
|
|
61
|
+
this._fields = [];
|
|
62
|
+
/** Whether the SortableList change being reported is a committed one. */
|
|
63
|
+
this._commit = true;
|
|
64
|
+
/** True while a row's own edit is written into the list's items. */
|
|
65
|
+
this._rowEdit = false;
|
|
66
|
+
this.list = new SortableList({
|
|
67
|
+
items: this.#toItems(value),
|
|
68
|
+
addButtonText: addLabel,
|
|
69
|
+
emptyMessage: 'No colours',
|
|
70
|
+
createItem: () => ({ id: ++_seq, text: this.newColor }),
|
|
71
|
+
renderItem: (container, item, index, callbacks) =>
|
|
72
|
+
this.#renderRow(container, item, index, callbacks),
|
|
73
|
+
// Every SortableList change but a row's own edit (add, remove,
|
|
74
|
+
// move, drag) is followed by a refresh that rebuilds all rows —
|
|
75
|
+
// down to none — so the old fields go first.
|
|
76
|
+
onChange: () => {
|
|
77
|
+
if (!this._rowEdit) this.#dropFields();
|
|
78
|
+
this.#emit(this._commit);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
this.root = null;
|
|
82
|
+
this.input = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render() {
|
|
86
|
+
if (this.root) return this.root;
|
|
87
|
+
const root = document.createElement('div');
|
|
88
|
+
root.className = 'twm-color-list';
|
|
89
|
+
if (this.name) {
|
|
90
|
+
const input = document.createElement('input');
|
|
91
|
+
input.type = 'hidden';
|
|
92
|
+
input.name = this.name;
|
|
93
|
+
root.appendChild(input);
|
|
94
|
+
this.input = input;
|
|
95
|
+
}
|
|
96
|
+
root.appendChild(this.list.render());
|
|
97
|
+
this.root = root;
|
|
98
|
+
this.#writeInput();
|
|
99
|
+
return root;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The colours in order as `#rrggbb`, or null while any row is not one. */
|
|
103
|
+
getValue() {
|
|
104
|
+
const out = this.getTexts().map(hexOf);
|
|
105
|
+
return out.includes(null) ? null : out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The rows' text as it stands, valid or not. */
|
|
109
|
+
getTexts() {
|
|
110
|
+
return this.list.getItems().map((item) => String(item.text ?? ''));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Indexes of the rows that do not hold a hex colour. */
|
|
114
|
+
invalidIndexes() {
|
|
115
|
+
const bad = [];
|
|
116
|
+
this.getTexts().forEach((text, i) => { if (!hexOf(text)) bad.push(i); });
|
|
117
|
+
return bad;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Replace the colours without firing `onChange`. */
|
|
121
|
+
setValue(colors) {
|
|
122
|
+
this.#dropFields();
|
|
123
|
+
this.list.setItems(this.#toItems(colors));
|
|
124
|
+
this.#writeInput();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Focus the first invalid row, else the first row, else the add button. */
|
|
128
|
+
focus() {
|
|
129
|
+
const bad = this.invalidIndexes()[0];
|
|
130
|
+
const field = this._fields[bad ?? 0];
|
|
131
|
+
if (field) { field.focus(); return; }
|
|
132
|
+
this.root?.querySelector('.twm-sortable-list__add')?.focus();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dispose() {
|
|
136
|
+
this.#dropFields();
|
|
137
|
+
this.list.dispose();
|
|
138
|
+
this.root = null;
|
|
139
|
+
this.input = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#toItems(colors) {
|
|
143
|
+
// A colour that converts is kept as its hex; anything else is kept as
|
|
144
|
+
// typed, so it shows — and is reported — rather than vanishing.
|
|
145
|
+
return (Array.isArray(colors) ? colors : []).map((c) => ({
|
|
146
|
+
id: ++_seq,
|
|
147
|
+
text: normalizeHex(String(c ?? '')) ?? String(c ?? ''),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Dispose every row's field: the rows are about to be rebuilt. */
|
|
152
|
+
#dropFields() {
|
|
153
|
+
for (const f of this._fields) f?.dispose();
|
|
154
|
+
this._fields = [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#renderRow(container, item, index, callbacks) {
|
|
158
|
+
const count = this.list.getItems().length;
|
|
159
|
+
|
|
160
|
+
const row = document.createElement('div');
|
|
161
|
+
row.className = 'twm-color-list__row';
|
|
162
|
+
const field = new ColorField({
|
|
163
|
+
value: item.text,
|
|
164
|
+
presets: this.presets,
|
|
165
|
+
label: `Colour ${index + 1}`,
|
|
166
|
+
// Every edit goes into the list's items, so the list never holds
|
|
167
|
+
// a stale row; `_commit` tells its onChange which kind it was.
|
|
168
|
+
onChange: (_v, { commit }) => {
|
|
169
|
+
this._commit = commit;
|
|
170
|
+
this._rowEdit = true;
|
|
171
|
+
try { callbacks.update({ text: field.getText() }); }
|
|
172
|
+
finally { this._commit = true; this._rowEdit = false; }
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
this._fields[index] = field;
|
|
176
|
+
row.appendChild(field.render());
|
|
177
|
+
|
|
178
|
+
const move = (icon, label, disabled, fn) => {
|
|
179
|
+
const btn = document.createElement('button');
|
|
180
|
+
btn.type = 'button';
|
|
181
|
+
btn.className = 'twm-color-list__move';
|
|
182
|
+
btn.title = label;
|
|
183
|
+
btn.setAttribute('aria-label', label);
|
|
184
|
+
btn.disabled = disabled;
|
|
185
|
+
const glyph = document.createElement('span');
|
|
186
|
+
glyph.className = 'material-symbols-outlined';
|
|
187
|
+
glyph.setAttribute('aria-hidden', 'true');
|
|
188
|
+
glyph.textContent = icon;
|
|
189
|
+
btn.appendChild(glyph);
|
|
190
|
+
btn.addEventListener('click', fn);
|
|
191
|
+
row.appendChild(btn);
|
|
192
|
+
};
|
|
193
|
+
move('arrow_upward', 'Move up', index === 0, callbacks.moveUp);
|
|
194
|
+
move('arrow_downward', 'Move down', index === count - 1, callbacks.moveDown);
|
|
195
|
+
container.appendChild(row);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#writeInput() {
|
|
199
|
+
if (this.input) this.input.value = JSON.stringify(this.getTexts());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#emit(commit) {
|
|
203
|
+
this.#writeInput();
|
|
204
|
+
if (this.input) {
|
|
205
|
+
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
206
|
+
if (commit) this.input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
207
|
+
}
|
|
208
|
+
this.onChange?.(this.getValue(), { commit });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
File without changes
|