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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cell_editors.js — STORY-0087. THE SIX BUILT-IN CELL EDITORS, and the parse
|
|
3
|
+
* each one reads its text with.
|
|
4
|
+
*
|
|
5
|
+
* text a text box commits the text
|
|
6
|
+
* multitext a text area commits the text; Enter is a new
|
|
7
|
+
* line, Ctrl/Cmd+Enter commits
|
|
8
|
+
* number a text box commits a Number (or null)
|
|
9
|
+
* bool a checkbox commits true or false
|
|
10
|
+
* choice an AutocompleteField over commits one option's value (or
|
|
11
|
+
* the column's `options` null; other text only with
|
|
12
|
+
* `allowOther`)
|
|
13
|
+
* datetime a text box commits a Date (or null)
|
|
14
|
+
*
|
|
15
|
+
* Each is `{create, parse}` in `CELL_EDITORS`. `create` is an editor factory
|
|
16
|
+
* as `cell_editor_host.js` takes one; `parse(text, ctx)` is the same reading
|
|
17
|
+
* as plain text, for anything that has text and not an editor (a paste):
|
|
18
|
+
*
|
|
19
|
+
* parse(text, { locale, utc, options, allowOther, messages })
|
|
20
|
+
* → { ok: true, value } | { ok: false, error }
|
|
21
|
+
*
|
|
22
|
+
* An editor built here hands the host a `parse()` of its own (what it holds,
|
|
23
|
+
* read the same way): Enter and Tab commit only what parses, and a refusal
|
|
24
|
+
* keeps the editor open with the message on the cell.
|
|
25
|
+
*
|
|
26
|
+
* NUMBER. Read by the separators of `locale` (Intl's): '1.234,56' is
|
|
27
|
+
* 1234.56 under 'de-DE' and '1,234.56' is under 'en-US'. A group separator
|
|
28
|
+
* is accepted only between groups of three digits in the whole part, so
|
|
29
|
+
* '1.234,56' under 'en-US' is refused rather than read as 1.23456. Spaces
|
|
30
|
+
* around the text are ignored. A space inside it is accepted only as the
|
|
31
|
+
* group separator of a locale that groups with a space (fr-FR, sv-SE, ...;
|
|
32
|
+
* any space, a no-break one included), and only between groups of three
|
|
33
|
+
* digits, so '1 234,5' is 1234.5 under 'fr-FR'; any other inner space is
|
|
34
|
+
* refused ('1 234' under 'en-US' is refused, not read as 1234). A minus may
|
|
35
|
+
* be '-' or '−'.
|
|
36
|
+
*
|
|
37
|
+
* DATETIME. ISO 8601 ('2026-12-31', '2026-12-31T08:30', with 'Z' or an
|
|
38
|
+
* offset an exact instant), or day, month and year in the order `locale`
|
|
39
|
+
* writes them, separated by '/', '.' or '-', with a four-digit year and an
|
|
40
|
+
* optional time ('31/12/2026 08:30'). When that order makes no date but the
|
|
41
|
+
* day-month swap does ('31/12/2026' under 'en-US') the swap is read — there
|
|
42
|
+
* is no other reading. A date without a zone is in the table's zone
|
|
43
|
+
* (`utc`: UTC, else local).
|
|
44
|
+
*
|
|
45
|
+
* A value opened and left as it was commits the value the cell was given,
|
|
46
|
+
* not its text read back, so opening and leaving a cell is no edit at all.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { AutocompleteField } from './autocomplete_field.js';
|
|
50
|
+
import { createTextEditor } from './cell_editor_host.js';
|
|
51
|
+
|
|
52
|
+
/** The refusals' words. DataTable's `editorMessages` overrides any of them. */
|
|
53
|
+
export const EDITOR_MESSAGES = Object.freeze({
|
|
54
|
+
number: 'Not a number',
|
|
55
|
+
datetime: 'Not a date',
|
|
56
|
+
bool: 'Not true or false',
|
|
57
|
+
choice: 'Not one of the options',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const ok = (value) => ({ ok: true, value });
|
|
61
|
+
const refuse = (ctx, type) => ({ ok: false, error: (ctx?.messages || {})[type] || EDITOR_MESSAGES[type] });
|
|
62
|
+
const isBlank = (text) => text == null || String(text).trim() === '';
|
|
63
|
+
|
|
64
|
+
/** The type names of the table's columns that the built-in editors answer to. */
|
|
65
|
+
const ALIASES = { num: 'number', date: 'datetime' };
|
|
66
|
+
|
|
67
|
+
/** The built-in editor's name for a column type: 'num' is 'number' and 'date'
|
|
68
|
+
* is 'datetime'; any other name is itself. */
|
|
69
|
+
export function editorTypeOf(type) {
|
|
70
|
+
const t = type == null ? 'text' : String(type);
|
|
71
|
+
return ALIASES[t] || t;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── number ──────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function safeLocale(locale) {
|
|
77
|
+
if (!locale) return undefined;
|
|
78
|
+
try { return Intl.NumberFormat.supportedLocalesOf(locale).length ? locale : undefined; }
|
|
79
|
+
catch (_) { return undefined; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `{group, decimal}`: the separators `locale` writes numbers with. */
|
|
83
|
+
export function numberSeparators(locale) {
|
|
84
|
+
const parts = new Intl.NumberFormat(safeLocale(locale)).formatToParts(1234567.5);
|
|
85
|
+
return {
|
|
86
|
+
group: parts.find((p) => p.type === 'group')?.value ?? ',',
|
|
87
|
+
decimal: parts.find((p) => p.type === 'decimal')?.value ?? '.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SPACES = /[\s\u00a0\u202f]/g;
|
|
92
|
+
|
|
93
|
+
/** Text to a Number by `locale`'s separators; blank is null. Spaces around
|
|
94
|
+
* the text are ignored; inside it a space is a group separator only where
|
|
95
|
+
* the locale groups with one (fr-FR, sv-SE, ...) and only between groups of
|
|
96
|
+
* three digits; anywhere else it is refused. */
|
|
97
|
+
export function parseNumber(text, ctx = {}) {
|
|
98
|
+
if (isBlank(text)) return ok(null);
|
|
99
|
+
const { group, decimal } = numberSeparators(ctx.locale);
|
|
100
|
+
let s = String(text).trim().replace(/\u2212/g, '-');
|
|
101
|
+
let sign = '';
|
|
102
|
+
if (s[0] === '-' || s[0] === '+') { sign = s[0] === '-' ? '-' : ''; s = s.slice(1); }
|
|
103
|
+
const halves = s.split(decimal);
|
|
104
|
+
if (halves.length > 2) return refuse(ctx, 'number');
|
|
105
|
+
let [whole, frac = null] = halves;
|
|
106
|
+
let exp = '';
|
|
107
|
+
const tail = frac ?? whole;
|
|
108
|
+
const e = /[eE][+-]?\d+$/.exec(tail);
|
|
109
|
+
if (e) {
|
|
110
|
+
exp = e[0];
|
|
111
|
+
if (frac != null) frac = frac.slice(0, -exp.length);
|
|
112
|
+
else whole = whole.slice(0, -exp.length);
|
|
113
|
+
}
|
|
114
|
+
const spaceGroup = group !== '' && group.replace(SPACES, '') === '';
|
|
115
|
+
const g = spaceGroup ? ' ' : group;
|
|
116
|
+
if (spaceGroup) whole = whole.replace(SPACES, ' ');
|
|
117
|
+
if (g && whole.includes(g)) {
|
|
118
|
+
const esc = g.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
119
|
+
if (!new RegExp(`^\\d{1,3}(${esc}\\d{3})+$`).test(whole)) return refuse(ctx, 'number');
|
|
120
|
+
whole = whole.split(g).join('');
|
|
121
|
+
}
|
|
122
|
+
if (!/^\d*$/.test(whole) || (frac != null && !/^\d*$/.test(frac))) return refuse(ctx, 'number');
|
|
123
|
+
if (!whole && !frac) return refuse(ctx, 'number');
|
|
124
|
+
const n = Number(`${sign}${whole || '0'}.${frac || '0'}${exp}`);
|
|
125
|
+
return Number.isFinite(n) ? ok(n) : refuse(ctx, 'number');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A number as the editor shows it: `locale`'s decimal separator, no groups,
|
|
129
|
+
* every digit (so it reads back as itself). */
|
|
130
|
+
export function formatNumberForEdit(value, ctx = {}) {
|
|
131
|
+
if (value == null) return '';
|
|
132
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return String(value);
|
|
133
|
+
const { decimal } = numberSeparators(ctx.locale);
|
|
134
|
+
return String(value).replace('.', decimal);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── bool ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const TRUE_WORDS = new Set(['true', 'yes', 'y', '1', 'on', 'x', '✓', '✔']);
|
|
140
|
+
const FALSE_WORDS = new Set(['false', 'no', 'n', '0', 'off', '']);
|
|
141
|
+
|
|
142
|
+
/** 'true', 'yes', 'y', '1', 'on', 'x' are true; 'false', 'no', 'n', '0', 'off'
|
|
143
|
+
* and blank are false (any case); anything else is refused. */
|
|
144
|
+
export function parseBool(text, ctx = {}) {
|
|
145
|
+
if (typeof text === 'boolean') return ok(text);
|
|
146
|
+
const t = String(text ?? '').trim().toLowerCase();
|
|
147
|
+
if (TRUE_WORDS.has(t)) return ok(true);
|
|
148
|
+
if (FALSE_WORDS.has(t)) return ok(false);
|
|
149
|
+
return refuse(ctx, 'bool');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function truthy(value) {
|
|
153
|
+
if (typeof value === 'boolean') return value;
|
|
154
|
+
if (value == null) return false;
|
|
155
|
+
const r = parseBool(String(value));
|
|
156
|
+
return r.ok ? r.value : true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── datetime ────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/** The order `locale` writes a date's parts in, e.g. ['day', 'month', 'year']. */
|
|
162
|
+
export function dateOrder(locale) {
|
|
163
|
+
let loc;
|
|
164
|
+
try { loc = Intl.DateTimeFormat.supportedLocalesOf(locale || []).length ? locale : undefined; }
|
|
165
|
+
catch (_) { loc = undefined; }
|
|
166
|
+
return new Intl.DateTimeFormat(loc, { year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
167
|
+
.formatToParts(new Date(2026, 11, 31))
|
|
168
|
+
.map((p) => p.type)
|
|
169
|
+
.filter((t) => t === 'day' || t === 'month' || t === 'year');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function daysIn(y, m) {
|
|
173
|
+
return new Date(Date.UTC(y, m, 0)).getUTCDate();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A Date from its parts, checked, in the table's zone (or at `offsetMin`). */
|
|
177
|
+
function makeDate({ y, mo, d, h = 0, mi = 0, s = 0, ms = 0 }, utc, offsetMin = null) {
|
|
178
|
+
if (!(mo >= 1 && mo <= 12) || !(d >= 1 && d <= daysIn(y, mo))) return null;
|
|
179
|
+
if (h > 23 || mi > 59 || s > 59) return null;
|
|
180
|
+
if (offsetMin != null) return new Date(Date.UTC(y, mo - 1, d, h, mi, s, ms) - offsetMin * 60000);
|
|
181
|
+
if (utc) return new Date(Date.UTC(y, mo - 1, d, h, mi, s, ms));
|
|
182
|
+
const date = new Date(y, mo - 1, d, h, mi, s, ms);
|
|
183
|
+
date.setFullYear(y); // years below 100 are not 19xx
|
|
184
|
+
return date;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3})\d*)?)?\s*(Z|[+-]\d{2}:?\d{2})?)?$/i;
|
|
188
|
+
const LOCAL = /^(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})\.?(?:[,\sT]+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([ap])?\.?m?\.?)?$/i;
|
|
189
|
+
|
|
190
|
+
/** Text to a Date: ISO 8601, or `locale`'s day/month/year order; blank is null. */
|
|
191
|
+
export function parseDateTime(text, ctx = {}) {
|
|
192
|
+
if (isBlank(text)) return ok(null);
|
|
193
|
+
const t = String(text).trim();
|
|
194
|
+
const iso = ISO.exec(t);
|
|
195
|
+
if (iso) {
|
|
196
|
+
const [, y, mo, d, h, mi, s, ms, zone] = iso;
|
|
197
|
+
let offset = null;
|
|
198
|
+
if (zone) {
|
|
199
|
+
if (zone.toUpperCase() === 'Z') offset = 0;
|
|
200
|
+
else {
|
|
201
|
+
const z = zone.replace(':', '');
|
|
202
|
+
offset = (z[0] === '-' ? -1 : 1) * (Number(z.slice(1, 3)) * 60 + Number(z.slice(3, 5)));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const date = makeDate({
|
|
206
|
+
y: +y, mo: +mo, d: +d, h: +(h || 0), mi: +(mi || 0), s: +(s || 0),
|
|
207
|
+
ms: ms ? Number(ms.padEnd(3, '0')) : 0,
|
|
208
|
+
}, !!ctx.utc, offset);
|
|
209
|
+
return date ? ok(date) : refuse(ctx, 'datetime');
|
|
210
|
+
}
|
|
211
|
+
const m = LOCAL.exec(t);
|
|
212
|
+
if (!m) return refuse(ctx, 'datetime');
|
|
213
|
+
const [, a, b, c, hh, mi, ss, ap] = m;
|
|
214
|
+
let h = hh == null ? 0 : +hh;
|
|
215
|
+
if (ap) {
|
|
216
|
+
if (h < 1 || h > 12) return refuse(ctx, 'datetime');
|
|
217
|
+
h = (h % 12) + (ap.toLowerCase() === 'p' ? 12 : 0);
|
|
218
|
+
}
|
|
219
|
+
const time = { h, mi: +(mi || 0), s: +(ss || 0) };
|
|
220
|
+
const order = dateOrder(ctx.locale);
|
|
221
|
+
const nums = [a, b, c];
|
|
222
|
+
const read = (ord) => {
|
|
223
|
+
const at = {};
|
|
224
|
+
ord.forEach((type, i) => { at[type] = nums[i]; });
|
|
225
|
+
if (!at.year || at.year.length !== 4) return null;
|
|
226
|
+
return makeDate({ y: +at.year, mo: +at.month, d: +at.day, ...time }, !!ctx.utc);
|
|
227
|
+
};
|
|
228
|
+
let date = read(order);
|
|
229
|
+
if (!date) {
|
|
230
|
+
// Day and month the other way round, when that is the only reading.
|
|
231
|
+
const swapped = order.map((p) => (p === 'day' ? 'month' : p === 'month' ? 'day' : p));
|
|
232
|
+
date = read(swapped);
|
|
233
|
+
}
|
|
234
|
+
return date ? ok(date) : refuse(ctx, 'datetime');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
238
|
+
|
|
239
|
+
/** A date as the editor shows it: ISO, 'YYYY-MM-DD', with ' HH:MM(:SS)' when
|
|
240
|
+
* it has a time of day — in the table's zone. Anything else as text. */
|
|
241
|
+
export function formatDateTimeForEdit(value, ctx = {}) {
|
|
242
|
+
if (value == null || value === '') return '';
|
|
243
|
+
if (!(value instanceof Date)) return String(value);
|
|
244
|
+
if (Number.isNaN(value.getTime())) return '';
|
|
245
|
+
const u = !!ctx.utc;
|
|
246
|
+
const y = u ? value.getUTCFullYear() : value.getFullYear();
|
|
247
|
+
const mo = (u ? value.getUTCMonth() : value.getMonth()) + 1;
|
|
248
|
+
const d = u ? value.getUTCDate() : value.getDate();
|
|
249
|
+
const h = u ? value.getUTCHours() : value.getHours();
|
|
250
|
+
const mi = u ? value.getUTCMinutes() : value.getMinutes();
|
|
251
|
+
const s = u ? value.getUTCSeconds() : value.getSeconds();
|
|
252
|
+
const day = `${pad(y, 4)}-${pad(mo)}-${pad(d)}`;
|
|
253
|
+
if (!h && !mi && !s) return day;
|
|
254
|
+
return `${day} ${pad(h)}:${pad(mi)}${s ? `:${pad(s)}` : ''}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── choice ──────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/** A column's `options` as `[{value, label}]`: a bare value is its own label. */
|
|
260
|
+
export function normalizeOptions(options) {
|
|
261
|
+
if (!Array.isArray(options)) return [];
|
|
262
|
+
return options.map((o) => (o && typeof o === 'object'
|
|
263
|
+
? { value: o.value, label: String(o.label ?? o.value ?? '') }
|
|
264
|
+
: { value: o, label: String(o ?? '') }));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Text to one option's value — by its label or its value, ignoring case
|
|
268
|
+
* and the spaces around it; blank is null. Other text is refused, unless
|
|
269
|
+
* `allowOther`, when it is the value. */
|
|
270
|
+
export function parseChoice(text, ctx = {}) {
|
|
271
|
+
if (isBlank(text)) return ok(null);
|
|
272
|
+
const t = String(text).trim();
|
|
273
|
+
const lower = t.toLowerCase();
|
|
274
|
+
const opts = normalizeOptions(ctx.options);
|
|
275
|
+
const hit = opts.find((o) => o.label.trim().toLowerCase() === lower)
|
|
276
|
+
|| opts.find((o) => String(o.value ?? '').trim().toLowerCase() === lower);
|
|
277
|
+
if (hit) return ok(hit.value);
|
|
278
|
+
return ctx.allowOther ? ok(t) : refuse(ctx, 'choice');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function labelOf(value, ctx) {
|
|
282
|
+
if (value == null) return '';
|
|
283
|
+
const hit = normalizeOptions(ctx.options).find((o) => Object.is(o.value, value) || String(o.value) === String(value));
|
|
284
|
+
return hit ? hit.label : String(value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── the editors ─────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/** What a factory is told, as a parse's `ctx`. */
|
|
290
|
+
function contextOf(args) {
|
|
291
|
+
const col = args.column || {};
|
|
292
|
+
return {
|
|
293
|
+
locale: args.locale, utc: !!args.utc, messages: args.messages,
|
|
294
|
+
options: col.options, allowOther: !!col.allowOther,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** A text field whose text a parse reads; left as it opened, it is the value
|
|
299
|
+
* it was given. */
|
|
300
|
+
function parsedField(el, args, start, parse) {
|
|
301
|
+
const { value, initialText = null } = args;
|
|
302
|
+
el.value = initialText != null ? String(initialText) : start;
|
|
303
|
+
const ctx = contextOf(args);
|
|
304
|
+
return {
|
|
305
|
+
el,
|
|
306
|
+
getValue: () => el.value,
|
|
307
|
+
parse: () => (initialText == null && el.value === start ? ok(value) : parse(el.value, ctx)),
|
|
308
|
+
focus: () => {
|
|
309
|
+
el.focus({ preventScroll: true });
|
|
310
|
+
const end = el.value.length;
|
|
311
|
+
try { el.setSelectionRange(end, end); } catch (_) { /* not a text type */ }
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function textInput(kind) {
|
|
317
|
+
const input = document.createElement('input');
|
|
318
|
+
input.type = 'text';
|
|
319
|
+
input.className = `twm-dt-editor twm-dt-editor--${kind}`;
|
|
320
|
+
input.spellcheck = false;
|
|
321
|
+
input.setAttribute('autocomplete', 'off');
|
|
322
|
+
return input;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** number: a text box (not type=number, which throws '1.234,56' away as it is
|
|
326
|
+
* typed) read by `locale`. */
|
|
327
|
+
export function createNumberEditor(args) {
|
|
328
|
+
const input = textInput('number');
|
|
329
|
+
input.inputMode = 'decimal';
|
|
330
|
+
return parsedField(input, args, formatNumberForEdit(args.value, contextOf(args)), parseNumber);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** datetime: a text box (not type=date, which takes no '31/12/2026') read as
|
|
334
|
+
* ISO or by `locale`'s order. */
|
|
335
|
+
export function createDateTimeEditor(args) {
|
|
336
|
+
const input = textInput('datetime');
|
|
337
|
+
return parsedField(input, args, formatDateTimeForEdit(args.value, contextOf(args)), parseDateTime);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** multitext: a text area. Enter is a new line; Ctrl/Cmd+Enter commits. */
|
|
341
|
+
export function createMultiTextEditor(args) {
|
|
342
|
+
const area = document.createElement('textarea');
|
|
343
|
+
area.className = 'twm-dt-editor twm-dt-editor--multitext';
|
|
344
|
+
area.spellcheck = false;
|
|
345
|
+
area.rows = Number(args.column?.rows) > 0 ? Number(args.column.rows) : 3;
|
|
346
|
+
const start = args.value == null ? '' : String(args.value);
|
|
347
|
+
const field = parsedField(area, args, start, (text) => ok(text));
|
|
348
|
+
return { ...field, multiline: true };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** bool: a checkbox. Ticking or unticking it commits; Enter and Tab commit it
|
|
352
|
+
* as it is. A space that opened it ticks it over; a word that reads as true
|
|
353
|
+
* or false sets it. */
|
|
354
|
+
export function createBoolEditor(args) {
|
|
355
|
+
const { value, initialText = null, commit } = args;
|
|
356
|
+
const box = document.createElement('input');
|
|
357
|
+
box.type = 'checkbox';
|
|
358
|
+
box.className = 'twm-dt-editor twm-dt-editor--bool';
|
|
359
|
+
const start = truthy(value);
|
|
360
|
+
box.checked = start;
|
|
361
|
+
if (initialText === ' ') box.checked = !start;
|
|
362
|
+
else if (initialText != null) {
|
|
363
|
+
const r = parseBool(initialText);
|
|
364
|
+
if (r.ok) box.checked = r.value;
|
|
365
|
+
}
|
|
366
|
+
box.addEventListener('change', () => commit(box.checked));
|
|
367
|
+
const read = () => (box.checked === start && initialText == null ? ok(value) : ok(box.checked));
|
|
368
|
+
return {
|
|
369
|
+
el: box,
|
|
370
|
+
getValue: () => box.checked,
|
|
371
|
+
parse: read,
|
|
372
|
+
focus: () => box.focus({ preventScroll: true }),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let uid = 0;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* choice: an AutocompleteField offering the column's `options`, filtered by
|
|
380
|
+
* what is typed. Picking one (a click, or Enter or Tab on the list) commits
|
|
381
|
+
* its value; Enter and Tab with the list shut commit the text read by
|
|
382
|
+
* `parseChoice`, and on an emptied box (no arrow having chosen an option)
|
|
383
|
+
* commit null. Escape with the list open closes the list only.
|
|
384
|
+
*/
|
|
385
|
+
export function createChoiceEditor(args) {
|
|
386
|
+
const { value, initialText = null, commit } = args;
|
|
387
|
+
const ctx = contextOf(args);
|
|
388
|
+
const opts = normalizeOptions(ctx.options);
|
|
389
|
+
const start = labelOf(value, ctx);
|
|
390
|
+
const shown = initialText != null ? String(initialText) : start;
|
|
391
|
+
let picked = null; // {label, value}: the last option picked
|
|
392
|
+
let keying = null; // the key being handled, while it is
|
|
393
|
+
let done = false;
|
|
394
|
+
|
|
395
|
+
const ac = new AutocompleteField({
|
|
396
|
+
fieldId: `twm-dt-choice-${++uid}`,
|
|
397
|
+
field: { value: shown },
|
|
398
|
+
skipNamespacePrefix: true,
|
|
399
|
+
typeIcons: { option: 'list' },
|
|
400
|
+
provider: ({ value: term }) => {
|
|
401
|
+
const q = String(term ?? '').trim().toLowerCase();
|
|
402
|
+
const all = !q || (initialText == null && term === start);
|
|
403
|
+
return opts
|
|
404
|
+
.filter((o) => all || o.label.toLowerCase().includes(q))
|
|
405
|
+
.map((o) => ({ label: o.label, insertText: o.label, iconType: 'option', metadata: { value: o.value } }));
|
|
406
|
+
},
|
|
407
|
+
onChange: (text, meta) => {
|
|
408
|
+
if (done || !meta?.immediate || !meta.metadata) return;
|
|
409
|
+
picked = { label: String(text ?? ''), value: meta.metadata.value };
|
|
410
|
+
// A pick by Enter or Tab moves the cursor as those keys do.
|
|
411
|
+
const move = keying
|
|
412
|
+
? (keying.key === 'Tab' ? (keying.shift ? 'left' : 'right') : (keying.shift ? 'up' : 'down'))
|
|
413
|
+
: null;
|
|
414
|
+
commit(picked.value, move);
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const root = ac.render();
|
|
418
|
+
root.classList.add('twm-dt-editor--choice');
|
|
419
|
+
const input = ac.input;
|
|
420
|
+
input.classList.add('twm-dt-editor');
|
|
421
|
+
|
|
422
|
+
let browsed = false; // an arrow moved through the list since the last keystroke
|
|
423
|
+
const onCapture = (ev) => {
|
|
424
|
+
const commitKey = (ev.key === 'Enter' || ev.key === 'Tab') && !ev.ctrlKey && !ev.metaKey && !ev.altKey;
|
|
425
|
+
keying = commitKey ? { key: ev.key, shift: ev.shiftKey } : null;
|
|
426
|
+
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') browsed = true;
|
|
427
|
+
// An emptied box is no option, even with the list open on all of
|
|
428
|
+
// them: Enter and Tab commit null — unless an arrow chose one.
|
|
429
|
+
if (commitKey && !browsed && !String(input.value).trim() && !ev.isComposing) {
|
|
430
|
+
ev.preventDefault();
|
|
431
|
+
ev.stopPropagation();
|
|
432
|
+
const move = ev.key === 'Tab' ? (ev.shiftKey ? 'left' : 'right') : (ev.shiftKey ? 'up' : 'down');
|
|
433
|
+
commit(null, move);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const onInput = () => { browsed = false; };
|
|
437
|
+
const onAfter = (ev) => {
|
|
438
|
+
keying = null;
|
|
439
|
+
// Escape with the list open was the list's (it closed it).
|
|
440
|
+
if (ev.key === 'Escape' && ev.defaultPrevented) ev.stopPropagation();
|
|
441
|
+
};
|
|
442
|
+
root.addEventListener('keydown', onCapture, true);
|
|
443
|
+
input.addEventListener('keydown', onAfter);
|
|
444
|
+
input.addEventListener('input', onInput);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
el: root,
|
|
448
|
+
getValue: () => ac.input?.value ?? '',
|
|
449
|
+
parse: () => {
|
|
450
|
+
const text = ac.input?.value ?? '';
|
|
451
|
+
if (initialText == null && text === start) return ok(value);
|
|
452
|
+
if (picked && text === picked.label) return ok(picked.value);
|
|
453
|
+
return parseChoice(text, ctx);
|
|
454
|
+
},
|
|
455
|
+
focus: () => {
|
|
456
|
+
ac.input?.focus({ preventScroll: true });
|
|
457
|
+
const end = ac.input?.value.length ?? 0;
|
|
458
|
+
try { ac.input?.setSelectionRange(end, end); } catch (_) { /* gone */ }
|
|
459
|
+
},
|
|
460
|
+
destroy: () => {
|
|
461
|
+
done = true;
|
|
462
|
+
root.removeEventListener('keydown', onCapture, true);
|
|
463
|
+
input.removeEventListener('keydown', onAfter);
|
|
464
|
+
input.removeEventListener('input', onInput);
|
|
465
|
+
ac.dispose();
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const identity = (text) => ok(text == null ? '' : String(text));
|
|
471
|
+
|
|
472
|
+
/** The six, by type: `{create, parse}` each. */
|
|
473
|
+
export const CELL_EDITORS = Object.freeze({
|
|
474
|
+
text: Object.freeze({ create: createTextEditor, parse: identity }),
|
|
475
|
+
multitext: Object.freeze({ create: createMultiTextEditor, parse: identity }),
|
|
476
|
+
number: Object.freeze({ create: createNumberEditor, parse: parseNumber }),
|
|
477
|
+
bool: Object.freeze({ create: createBoolEditor, parse: parseBool }),
|
|
478
|
+
choice: Object.freeze({ create: createChoiceEditor, parse: parseChoice }),
|
|
479
|
+
datetime: Object.freeze({ create: createDateTimeEditor, parse: parseDateTime }),
|
|
480
|
+
});
|