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.
Files changed (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -23,6 +23,7 @@ const GAP = 8;
23
23
  * trigger: buttonElement,
24
24
  * options: [
25
25
  * { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },
26
+ * { separator: true },
26
27
  * { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }
27
28
  * ],
28
29
  * onSelect: (option) => console.log('Selected:', option.type),
@@ -34,12 +35,22 @@ const GAP = 8;
34
35
  * dropdown.toggle();
35
36
  * dropdown.destroy();
36
37
  * ```
38
+ *
39
+ * ── Separators ────────────────────────────────────────────────────────
40
+ * An entry `{ separator: true }` draws a divider instead of a button: a
41
+ * `<div role="separator" class="twm-action-dropdown-separator">`, which takes
42
+ * no focus, is never reached by the arrow keys (they move between the option
43
+ * buttons only) and never reaches `onSelect` (it has no click handler). A
44
+ * separator only separates: one at the start or the end of the list, or one
45
+ * right after another, is dropped, so a list assembled from groups that may
46
+ * come out empty never draws a stray or doubled rule.
37
47
  */
38
48
  export class ActionDropdown {
39
49
  /**
40
50
  * @param {Object} config
41
51
  * @param {HTMLElement} config.trigger - Button element that triggers the dropdown
42
- * @param {Array} config.options - Array of option objects with { type, label, icon, description? }
52
+ * @param {Array} config.options - Array of option objects with { type, label, icon, description? },
53
+ * or `{ separator: true }` for a divider between them
43
54
  * @param {Function} config.onSelect - Callback when an option is selected
44
55
  * @param {string} [config.className=''] - Additional CSS class for the dropdown
45
56
  * @param {string} [config.menuId] - Optional ID for the menu element
@@ -98,13 +109,30 @@ export class ActionDropdown {
98
109
  }
99
110
 
100
111
  /**
101
- * Render option buttons into a container element.
112
+ * Render option buttons, and dividers for separator entries, into a
113
+ * container element. `data-index` stays the entry's index in `options`.
102
114
  * @param {HTMLElement} container
103
115
  * @private
104
116
  */
105
117
  _renderOptions(container) {
106
118
  container.innerHTML = '';
119
+ let pendingSeparator = false;
107
120
  this.options.forEach((opt, index) => {
121
+ if (opt?.separator) {
122
+ // Drawn only once an option follows it, and only if an option
123
+ // came before it: leading, trailing and doubled separators
124
+ // collapse.
125
+ pendingSeparator = container.childElementCount > 0;
126
+ return;
127
+ }
128
+ if (pendingSeparator) {
129
+ const sep = document.createElement('div');
130
+ sep.className = 'twm-action-dropdown-separator';
131
+ sep.setAttribute('role', 'separator');
132
+ container.appendChild(sep);
133
+ pendingSeparator = false;
134
+ }
135
+
108
136
  const optionBtn = document.createElement('button');
109
137
  optionBtn.type = 'button';
110
138
  optionBtn.className = 'twm-action-dropdown-option';
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,293 @@
1
+ /**
2
+ * cell_editor_host.js — STORY-0003. ONE CELL BEING EDITED: open an editor in
3
+ * it, and close it exactly once — by a commit or by a cancel.
4
+ *
5
+ * The editor itself comes from a FACTORY, so a consumer can bring any editor
6
+ * it likes (DataTable's `editors` option); the plain text box below is the
7
+ * default. A factory is called with
8
+ *
9
+ * { ...args, value, row, rowIdx, colIdx, column: {index, header, type},
10
+ * initialText, commit(value, move?), cancel() }
11
+ *
12
+ * and returns either an element, or
13
+ * `{ el, getValue?, parse?, multiline?, focus?, destroy? }`.
14
+ * `initialText` is the character typed to open the editor (null when it was
15
+ * opened by F2, Enter or a double-click): the editor starts with it in place
16
+ * of the value, as a spreadsheet does.
17
+ *
18
+ * ══ THE KEYS, WHICH THE HOST OWNS ════════════════════════════════════════
19
+ *
20
+ * Enter commit, and the table moves the cursor down (Shift: up)
21
+ * Tab commit, and the cursor moves right (Shift: left)
22
+ * Escape cancel: the cell is drawn as it was, and nothing is called
23
+ * focus out commit, without moving — a click elsewhere keeps the edit
24
+ *
25
+ * Enter and Tab commit `getValue()` — an input, select or textarea's `value`
26
+ * when the factory returned the field itself; any other editor without a
27
+ * `getValue` calls `commit(value)` itself. Such an editor has no value the host
28
+ * can read: Enter, Tab and the focus leaving do nothing, and `commit()` from
29
+ * outside (its row leaving a virtual window) CANCELS it. Every key
30
+ * pressed in the editor stops there: the table's own keys (the arrows, Ctrl+A,
31
+ * Ctrl+C) are not the editor's.
32
+ *
33
+ * ══ A VALUE THAT DOES NOT PARSE (STORY-0087) ═══════════════════════════
34
+ *
35
+ * An editor with a `parse()` — `() => {ok: true, value} | {ok: false, error}`
36
+ * — commits what `parse()` reads, not `getValue()`. A refusal on Enter or Tab
37
+ * keeps the editor open, and its cell carries the message
38
+ * (`twm-dt-cell--error`, `title`, `data-error`) until the next keystroke in
39
+ * it; nothing is committed. A refusal when the focus leaves, or on `commit()`
40
+ * from outside, closes the editor and hands `onRefuse` the message and the
41
+ * text (`getValue()`); still nothing is committed. A `multiline` editor
42
+ * keeps Enter for a new line: Ctrl/Cmd+Enter commits it.
43
+ *
44
+ * `commit(value, move?)` takes an optional move ('down', 'up', 'right',
45
+ * 'left'), for an editor that commits on a key of its own.
46
+ *
47
+ * ══ EXACTLY ONCE ═════════════════════════════════════════════════════════
48
+ *
49
+ * A session closes once. Escape's own repaint takes the focused input out of
50
+ * the document, and a browser fires `focusout` for that — the commit listener.
51
+ * The session is marked closed BEFORE anything is taken out, so that late
52
+ * `focusout` (and a second Enter, and an editor that calls `commit` twice)
53
+ * finds nothing to do.
54
+ */
55
+
56
+ /**
57
+ * The default editor: a text box holding the value as text (or the character
58
+ * that opened it). Left unchanged, it commits the value it was given — not
59
+ * that value turned into a string — so opening and leaving a number cell is
60
+ * no edit at all.
61
+ */
62
+ export function createTextEditor({ value, initialText = null }) {
63
+ const input = document.createElement('input');
64
+ input.type = 'text';
65
+ input.className = 'twm-dt-editor';
66
+ input.spellcheck = false;
67
+ input.setAttribute('autocomplete', 'off');
68
+ const start = value == null ? '' : String(value);
69
+ input.value = initialText != null ? String(initialText) : start;
70
+ return {
71
+ el: input,
72
+ getValue: () => (initialText == null && input.value === start ? value : input.value),
73
+ focus: () => {
74
+ input.focus({ preventScroll: true });
75
+ const end = input.value.length;
76
+ try { input.setSelectionRange(end, end); } catch (_) { /* not a text type */ }
77
+ },
78
+ };
79
+ }
80
+
81
+ /** What a factory returned, as one shape — or null when it returned nothing. */
82
+ function normalise(made) {
83
+ if (!made) return null;
84
+ const isNode = typeof Node !== 'undefined' && made instanceof Node;
85
+ const el = isNode ? made : made.el;
86
+ if (!el || (typeof Node !== 'undefined' && !(el instanceof Node))) return null;
87
+ const own = isNode ? {} : made;
88
+ let getValue = typeof own.getValue === 'function' ? own.getValue : null;
89
+ // A form field's value is the editor's; any other element says for itself.
90
+ if (!getValue && el.matches?.('input, select, textarea')) getValue = () => el.value;
91
+ const parse = typeof own.parse === 'function' ? own.parse : null;
92
+ // What a commit reads: the parse, else the value as it is.
93
+ let read = null;
94
+ if (parse) {
95
+ read = () => {
96
+ let r;
97
+ try { r = parse(); } catch (err) { r = { ok: false, error: err?.message || String(err) }; }
98
+ return r && r.ok === false ? { ok: false, error: String(r.error ?? 'Not valid') }
99
+ : { ok: true, value: r ? r.value : undefined };
100
+ };
101
+ } else if (getValue) read = () => ({ ok: true, value: getValue() });
102
+ return {
103
+ el,
104
+ getValue,
105
+ read,
106
+ multiline: !!own.multiline,
107
+ focus: typeof own.focus === 'function' ? own.focus : null,
108
+ destroy: typeof own.destroy === 'function' ? own.destroy : null,
109
+ };
110
+ }
111
+
112
+ const MOVES = new Set(['down', 'up', 'right', 'left']);
113
+
114
+ export class CellEditorHost {
115
+ /**
116
+ * @param {object} o
117
+ * @param {(session, value, move, reason) => void} o.onCommit after the
118
+ * cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
119
+ * or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
120
+ * @param {(session, reason) => void} o.onCancel
121
+ * @param {(session, error, text, reason) => void} [o.onRefuse] a value
122
+ * that did not parse, when the editor closed with it (the focus left,
123
+ * `commit()` from outside); without it such a close is a cancel.
124
+ */
125
+ constructor({ onCommit, onCancel, onRefuse = null }) {
126
+ this._onCommit = onCommit;
127
+ this._onCancel = onCancel;
128
+ this._onRefuse = onRefuse;
129
+ /** The open session, or null. */
130
+ this.session = null;
131
+ }
132
+
133
+ /**
134
+ * Open an editor in `td`; `args` go to the factory beside the cell's own
135
+ * (DataTable's `locale`, `utc`, `messages`). Returns whether one opened (a factory that
136
+ * throws or returns nothing opens none, and the cell is untouched).
137
+ */
138
+ open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, args = {}, ...extra }) {
139
+ if (this.session) return false;
140
+ const s = {
141
+ ...extra, td, value, row, rowIdx, colIdx, column,
142
+ previous: value, closed: false, offs: [], saved: null, editor: null,
143
+ };
144
+ let made = null;
145
+ try {
146
+ made = factory({
147
+ ...args, value, row, rowIdx, colIdx, column, initialText,
148
+ commit: (v, move = null) => this._finish(s, v, MOVES.has(move) ? move : null, 'editor'),
149
+ cancel: () => this._cancel(s, 'editor'),
150
+ });
151
+ } catch (err) {
152
+ console.error('[DataTable] cell editor factory threw', err);
153
+ return false;
154
+ }
155
+ const editor = normalise(made);
156
+ if (!editor) return false;
157
+ s.editor = editor;
158
+ s.saved = [...td.childNodes];
159
+ s.savedTitle = td.getAttribute('title');
160
+ s.savedError = {
161
+ on: td.classList.contains('twm-dt-cell--error'),
162
+ message: td.getAttribute('data-error'),
163
+ };
164
+ this.session = s;
165
+ td.classList.add('twm-dt-cell--editing');
166
+ td.replaceChildren(editor.el);
167
+
168
+ const on = (type, fn) => {
169
+ editor.el.addEventListener(type, fn);
170
+ s.offs.push(() => editor.el.removeEventListener(type, fn));
171
+ };
172
+ on('keydown', (ev) => {
173
+ if (ev.isComposing) return;
174
+ ev.stopPropagation();
175
+ if (ev.key === 'Escape') {
176
+ ev.preventDefault();
177
+ this._cancel(s, 'key');
178
+ } else if (ev.key === 'Enter' && !ev.altKey && editor.read
179
+ && (editor.multiline ? (ev.ctrlKey || ev.metaKey) : !(ev.ctrlKey || ev.metaKey))) {
180
+ ev.preventDefault();
181
+ this._tryKey(s, ev.shiftKey ? 'up' : 'down');
182
+ } else if (ev.key === 'Tab' && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.read) {
183
+ ev.preventDefault();
184
+ this._tryKey(s, ev.shiftKey ? 'left' : 'right');
185
+ }
186
+ });
187
+ // A refusal's message stands until the text changes.
188
+ on('input', () => this._unmark(s));
189
+ // A press inside the editor is the editor's, not a click on the cell.
190
+ for (const type of ['click', 'dblclick', 'mousedown', 'contextmenu']) {
191
+ on(type, (ev) => ev.stopPropagation());
192
+ }
193
+ on('focusout', (ev) => {
194
+ if (s.closed || !editor.read) return;
195
+ const to = ev.relatedTarget;
196
+ if (to && editor.el.contains(to)) return;
197
+ // The window losing focus (another app) is not a click elsewhere.
198
+ const doc = editor.el.ownerDocument;
199
+ if (!to && doc && typeof doc.hasFocus === 'function' && !doc.hasFocus()) return;
200
+ this._settle(s, null, 'blur');
201
+ });
202
+
203
+ try {
204
+ if (editor.focus) editor.focus();
205
+ else editor.el.focus?.({ preventScroll: true });
206
+ } catch (_) { /* not focusable */ }
207
+ return true;
208
+ }
209
+
210
+ /** Commit the open session with what its editor holds. */
211
+ commit(move = null, reason = 'editor') {
212
+ const s = this.session;
213
+ if (!s) return false;
214
+ if (!s.editor.read) { this._cancel(s, reason); return false; }
215
+ return this._settle(s, move, reason);
216
+ }
217
+
218
+ /** Enter or Tab: commit what parses; a refusal keeps the editor open,
219
+ * the message on its cell. */
220
+ _tryKey(s, move) {
221
+ const r = s.editor.read();
222
+ if (r.ok) return this._finish(s, r.value, move, 'key');
223
+ s.td.classList.add('twm-dt-cell--error');
224
+ s.td.setAttribute('data-error', r.error);
225
+ s.td.setAttribute('title', r.error);
226
+ s.marked = true;
227
+ return false;
228
+ }
229
+
230
+ _unmark(s) {
231
+ if (!s.marked) return;
232
+ s.marked = false;
233
+ this._restoreMark(s);
234
+ }
235
+
236
+ _restoreMark(s) {
237
+ const { on, message } = s.savedError;
238
+ s.td.classList.toggle('twm-dt-cell--error', on);
239
+ if (message == null) s.td.removeAttribute('data-error');
240
+ else s.td.setAttribute('data-error', message);
241
+ if (s.savedTitle == null) s.td.removeAttribute('title');
242
+ else s.td.setAttribute('title', s.savedTitle);
243
+ }
244
+
245
+ /** The editor closing with what it holds (the focus left, `commit()`):
246
+ * committed when it parses, else handed to `onRefuse`. */
247
+ _settle(s, move, reason) {
248
+ const r = s.editor.read();
249
+ if (r.ok) return this._finish(s, r.value, move, reason);
250
+ if (s.closed) return false;
251
+ let text = '';
252
+ try { text = s.editor.getValue ? s.editor.getValue() : ''; } catch (_) { /* gone */ }
253
+ this._close(s);
254
+ if (this._onRefuse) this._onRefuse(s, r.error, text, reason);
255
+ else this._onCancel?.(s, reason);
256
+ return true;
257
+ }
258
+
259
+ /** Cancel the open session: the cell is drawn as it was. */
260
+ cancel(reason = 'editor') {
261
+ const s = this.session;
262
+ if (!s) return false;
263
+ return this._cancel(s, reason);
264
+ }
265
+
266
+ /** Take the editor out and put the cell's own content back. */
267
+ _close(s) {
268
+ s.closed = true;
269
+ if (this.session === s) this.session = null;
270
+ for (const off of s.offs) { try { off(); } catch (_) { /* gone */ } }
271
+ s.offs = [];
272
+ try { s.editor.destroy?.(); } catch (err) {
273
+ console.error('[DataTable] cell editor destroy threw', err);
274
+ }
275
+ s.td.classList.remove('twm-dt-cell--editing');
276
+ s.td.replaceChildren(...s.saved);
277
+ this._restoreMark(s);
278
+ }
279
+
280
+ _finish(s, value, move, reason) {
281
+ if (s.closed) return false;
282
+ this._close(s);
283
+ this._onCommit?.(s, value, move, reason);
284
+ return true;
285
+ }
286
+
287
+ _cancel(s, reason) {
288
+ if (s.closed) return false;
289
+ this._close(s);
290
+ this._onCancel?.(s, reason);
291
+ return true;
292
+ }
293
+ }