flexdesk 0.17.17 → 0.18.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 +70 -0
- package/LICENSE +0 -0
- package/README.md +1 -0
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +0 -0
- package/charts.js +0 -0
- package/core.js +0 -0
- package/css/flexdesk.css +7499 -5851
- package/css/reset.css +1 -1
- package/css/tokens.css +23 -1
- package/dist/canvas.js +0 -0
- package/dist/canvas.js.map +0 -0
- package/dist/charts.js +10 -6
- package/dist/charts.js.map +1 -1
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-5W4EEKSK.js +778 -0
- package/dist/chunk-5W4EEKSK.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/chunk-GOQLR5WR.js +0 -0
- package/dist/chunk-GOQLR5WR.js.map +0 -0
- package/dist/{chunk-JKZLNKG6.js → chunk-HKPB6OFA.js} +90 -25
- package/dist/{chunk-JKZLNKG6.js.map → chunk-HKPB6OFA.js.map} +2 -2
- package/dist/chunk-HNU6MM2B.js +0 -0
- package/dist/chunk-HNU6MM2B.js.map +0 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/chunk-N7HC3ADV.js +0 -0
- package/dist/chunk-N7HC3ADV.js.map +0 -0
- package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
- package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
- package/dist/chunk-RKVYIXDC.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
- package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
- package/dist/chunk-ZL3VKINP.js.map +7 -0
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7499 -5851
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/host.js +0 -0
- package/dist/host.js.map +0 -0
- package/dist/reset.css +1 -1
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +23 -1
- package/dist/widgets.js +3969 -1189
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +121 -33
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/chat-shell.md +198 -0
- package/docs/components/command-palette.md +0 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/data-table-grid.md +0 -0
- package/docs/components/data-table.md +0 -0
- package/docs/components/editor.md +0 -0
- package/docs/components/filter-builder.md +246 -0
- package/docs/components/flow-canvas-editor.md +0 -0
- package/docs/components/flow-editors.md +0 -0
- package/docs/components/flow-lane-editor.md +0 -0
- package/docs/components/flow-outline-editor.md +0 -0
- package/docs/components/forms-and-dialogs.md +22 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +167 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/markdown-and-outline.md +0 -0
- package/docs/components/menus.md +120 -3
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/record-list-and-fact-table.md +0 -0
- package/docs/components/settings-view.md +0 -0
- package/docs/components/shell.md +104 -1
- package/docs/components/wizard.md +0 -0
- package/docs/components/workspace-picker.md +149 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/domain-neutrality.md +0 -0
- package/docs/concepts/host-port.md +0 -0
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/peer-dependencies.md +0 -0
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/editor.js +0 -0
- package/flow.js +0 -0
- package/git.js +13 -0
- package/host.js +0 -0
- package/package.json +6 -1
- package/src/canvas/canvas_adapter.js +0 -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 +0 -0
- package/src/canvas/layered_layout.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +0 -0
- package/src/charts/plot_config.js +0 -0
- package/src/charts/plotly_wrapper.js +0 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +0 -0
- package/src/core/settings.js +0 -0
- package/src/core/state_guard.js +0 -0
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +0 -0
- package/src/editor/monaco_editor_factory.js +0 -0
- package/src/editor/monaco_loader.js +0 -0
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +0 -0
- package/src/flow/canvas_editor/connections.js +0 -0
- package/src/flow/canvas_editor/editor.js +0 -0
- package/src/flow/canvas_editor/index.js +0 -0
- package/src/flow/canvas_editor/layout.js +0 -0
- package/src/flow/canvas_editor/strings.js +0 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +0 -0
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +0 -0
- package/src/flow/kit/keys.js +0 -0
- package/src/flow/kit/logic_graph.js +0 -0
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +0 -0
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/strings.js +0 -0
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/kit/widgets.js +0 -0
- package/src/flow/lanes/editor.js +0 -0
- package/src/flow/lanes/index.js +0 -0
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +0 -0
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +0 -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 +1 -1
- 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/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/help/help_modal.js +0 -0
- package/src/help/help_registry.js +0 -0
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +0 -0
- package/src/host/host.js +0 -0
- package/src/host/photino_host.js +0 -0
- package/src/host/photino_host.md +0 -0
- package/src/host/pywebview_host.js +0 -0
- package/src/tiles/chart_tiles.js +0 -0
- package/src/tiles/config_schema.js +0 -0
- package/src/tiles/figure_tile.js +0 -0
- package/src/tiles/layout_persistence.js +0 -0
- package/src/tiles/list_tiles.js +0 -0
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiles/tile_config_form.js +0 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +0 -0
- package/src/tiling/command_palette.js +0 -0
- package/src/tiling/content_registry.js +0 -0
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +0 -0
- package/src/tiling/page_factory.js +0 -0
- package/src/tiling/panel_availability.js +0 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_renderer.js +2 -3
- package/src/tiling/tile_tab_menu.js +0 -0
- 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 +4 -78
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/anchored_layer.js +255 -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 +0 -0
- package/src/ui/components/cell_editors.js +0 -0
- package/src/ui/components/cell_grid.js +0 -0
- package/src/ui/components/cell_paste.js +0 -0
- package/src/ui/components/cell_selection.js +0 -0
- package/src/ui/components/chat_shell.js +920 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +0 -0
- package/src/ui/components/column_order.js +0 -0
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/crosstab.js +0 -0
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/detail_header.js +0 -0
- package/src/ui/components/document_outline.js +0 -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 +0 -0
- package/src/ui/components/filter_builder.js +766 -0
- package/src/ui/components/filter_model.js +614 -0
- package/src/ui/components/gallery_picker.js +0 -0
- package/src/ui/components/header_actions.js +0 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +0 -0
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/menu_bar.js +101 -24
- package/src/ui/components/modal.js +79 -109
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/progress.js +0 -0
- package/src/ui/components/recent_items.js +169 -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 +0 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/series.js +0 -0
- package/src/ui/components/settings_view.js +0 -0
- package/src/ui/components/slide_out_panel.js +0 -0
- package/src/ui/components/slider_field.js +0 -0
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +0 -0
- package/src/ui/components/staged_history.js +0 -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 +0 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +0 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/toast.js +0 -0
- package/src/ui/components/toggle_field.js +0 -0
- package/src/ui/components/tsv.js +0 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/workspace_picker.js +1155 -0
- package/src/ui/controllers/window_chrome_controller.js +4 -1
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/markdown.js +0 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +0 -0
- 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 +0 -0
- package/src/ui/utils/tooltip_service.js +0 -0
- package/tiles.js +0 -0
- package/widgets.js +12 -2
- package/wm.js +0 -0
- package/css/base.css +0 -10527
- package/css/overrides.css +0 -2596
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
* }
|
|
78
78
|
*
|
|
79
79
|
* Dependent-property pick-or-create — a `select` field that carries a
|
|
80
|
-
* `create` spec renders as a
|
|
80
|
+
* `create` spec renders as a `createCombobox` instead of a
|
|
81
81
|
* dropdown: the user picks an existing option OR types a new value.
|
|
82
82
|
* On submit, a value that isn't an existing option is created via
|
|
83
83
|
* `create.onCreate(value)` before the form resolves:
|
|
@@ -115,29 +115,11 @@ import { ToggleField } from '../../ui/components/toggle_field.js';
|
|
|
115
115
|
import { ColorField } from './color_field.js';
|
|
116
116
|
import { ColorListField } from './color_list_field.js';
|
|
117
117
|
import { choiceList, findChoice, flatChoices, selectBox, toggleChips } from './choice_controls.js';
|
|
118
|
+
import { createCombobox } from './combobox.js';
|
|
119
|
+
import { modalHost, setModalHost } from '../utils/modal_host.js';
|
|
120
|
+
import { fireEvent } from '../utils/fire_event.js';
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
* C25. WHERE A DIALOG OPENS, when the application spans more than one window.
|
|
121
|
-
*
|
|
122
|
-
* Every modal here is a `ManagedWindow` with no `container`, so it mounts into
|
|
123
|
-
* `document.body` — and "document" is the document this MODULE was loaded in.
|
|
124
|
-
* A consumer that opens a second browser window with `window.open` and builds
|
|
125
|
-
* its DOM from the opener's realm (which is how a pop-out grid works: same
|
|
126
|
-
* modules, same JS context, a different document) therefore gets its dialogs in
|
|
127
|
-
* the window it popped OUT of. The user presses "Add column" on their second
|
|
128
|
-
* monitor and a dialog appears on the first, behind whatever is there.
|
|
129
|
-
*
|
|
130
|
-
* Passing a container at each call site was the other option and it is worse:
|
|
131
|
-
* twenty-one of them across eleven files, every one of which would have to be
|
|
132
|
-
* given a document it has no other reason to know about, and any one missed is
|
|
133
|
-
* this bug again with no way to see it from here.
|
|
134
|
-
*
|
|
135
|
-
* So the HOST is ambient and the consumer sets it when its focus moves. Null —
|
|
136
|
-
* every consumer today — means `document.body`, exactly as before.
|
|
137
|
-
*/
|
|
138
|
-
let _modalHost = null;
|
|
139
|
-
export function setModalHost(el) { _modalHost = el || null; }
|
|
140
|
-
export function modalHost() { return _modalHost; }
|
|
122
|
+
export { setModalHost, modalHost };
|
|
141
123
|
|
|
142
124
|
let _modalSeq = 1;
|
|
143
125
|
|
|
@@ -198,7 +180,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
198
180
|
`;
|
|
199
181
|
|
|
200
182
|
const win = new ManagedWindow({
|
|
201
|
-
container:
|
|
183
|
+
container: modalHost(),
|
|
202
184
|
id: `twm-modal-${_modalSeq++}`,
|
|
203
185
|
title: title || 'Dialog',
|
|
204
186
|
icon: 'edit_note',
|
|
@@ -237,12 +219,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
237
219
|
// The options each field offers NOW — `setOptions` replaces them, the
|
|
238
220
|
// caller's field object is never written to.
|
|
239
221
|
const optionsOf = new Map(formFields.map((f) => [f.name, f.options]));
|
|
240
|
-
const comboHints = new Map();
|
|
241
222
|
for (const f of formFields) {
|
|
242
|
-
if (f.type === 'select' && f.create) {
|
|
243
|
-
const update = _wireComboboxHint(body, f, () => optionsOf.get(f.name));
|
|
244
|
-
if (update) comboHints.set(f.name, update);
|
|
245
|
-
}
|
|
246
223
|
if (f.type === 'picker') _wirePicker(body, f);
|
|
247
224
|
}
|
|
248
225
|
const rowHidden = (name) => !!body.querySelector(`[data-field-row="${name}"]`)?.hidden;
|
|
@@ -281,13 +258,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
281
258
|
* read a combobox through this, so they agree (`'2'` is option `2`). */
|
|
282
259
|
const comboOption = (f, text) => findChoice(flatChoices(optionsOf.get(f.name)), text, true);
|
|
283
260
|
/** A disabled option's reason ('' with none) when one is the value, else null. */
|
|
284
|
-
const unavailable = (f
|
|
285
|
-
const c = controls.get(f.name);
|
|
286
|
-
if (c) return c.unavailable();
|
|
287
|
-
if (f.type !== 'select' || !f.create || value == null || value === '') return null;
|
|
288
|
-
const o = comboOption(f, value);
|
|
289
|
-
return o?.disabled ? (o.reason || '') : null;
|
|
290
|
-
};
|
|
261
|
+
const unavailable = (f) => controls.get(f.name)?.unavailable() ?? null;
|
|
291
262
|
|
|
292
263
|
// Clear per-field errors on input so the user sees the row recover.
|
|
293
264
|
for (const f of formFields) {
|
|
@@ -337,11 +308,6 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
337
308
|
const el = body.querySelector(`[name="${name}"]`);
|
|
338
309
|
if (!el) return undefined;
|
|
339
310
|
if (el.type === 'checkbox') return !!el.checked;
|
|
340
|
-
const f = fieldOf(name);
|
|
341
|
-
if (f?.type === 'select' && f.create) {
|
|
342
|
-
const match = comboOption(f, el.value);
|
|
343
|
-
return match ? match.value : el.value;
|
|
344
|
-
}
|
|
345
311
|
return el.value;
|
|
346
312
|
},
|
|
347
313
|
set(name, value) {
|
|
@@ -350,29 +316,17 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
350
316
|
if (!el) return;
|
|
351
317
|
if (el.type === 'checkbox') { el.checked = !!value; return; }
|
|
352
318
|
const f = fieldOf(name);
|
|
353
|
-
|
|
354
|
-
const shut = f?.type === 'select' && f.create && comboOption(f, value)?.disabled;
|
|
355
|
-
el.value = value == null || shut ? '' : String(value);
|
|
356
|
-
comboHints.get(name)?.();
|
|
319
|
+
el.value = value == null ? '' : String(value);
|
|
357
320
|
if (f?.type === 'picker') {
|
|
358
321
|
const display = body.querySelector(`[data-picker-display="${name}"]`);
|
|
359
322
|
if (display) display.value = _pickerLabel(f, el.value);
|
|
360
323
|
}
|
|
361
324
|
},
|
|
362
325
|
setOptions(name, options) {
|
|
363
|
-
const f = fieldOf(name);
|
|
364
|
-
if (!f) return;
|
|
365
326
|
const c = controls.get(name);
|
|
366
|
-
if (c?.setOptions)
|
|
367
|
-
optionsOf.set(name, options);
|
|
368
|
-
c.setOptions(options);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (f.type !== 'select' || !f.create) return;
|
|
327
|
+
if (!c?.setOptions) return;
|
|
372
328
|
optionsOf.set(name, options);
|
|
373
|
-
|
|
374
|
-
if (list) list.innerHTML = _datalistOptions(options);
|
|
375
|
-
comboHints.get(name)?.();
|
|
329
|
+
c.setOptions(options);
|
|
376
330
|
},
|
|
377
331
|
};
|
|
378
332
|
const fire = (name) => {
|
|
@@ -444,7 +398,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
444
398
|
if (rowHidden(f.name)) {
|
|
445
399
|
// Not asked about — but a disabled option is still never
|
|
446
400
|
// the value: a hidden row holding one submits no value.
|
|
447
|
-
if (unavailable(f
|
|
401
|
+
if (unavailable(f) != null) data[f.name] = f.type === 'radio' ? null : '';
|
|
448
402
|
continue;
|
|
449
403
|
}
|
|
450
404
|
const v = data[f.name];
|
|
@@ -454,7 +408,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
|
|
|
454
408
|
let msg = f.required && _missing(f, v) ? 'Required.' : null;
|
|
455
409
|
if (!msg && _outOfRange(f, v)) msg = _rangeMessage(f);
|
|
456
410
|
if (!msg) {
|
|
457
|
-
const reason = unavailable(f
|
|
411
|
+
const reason = unavailable(f);
|
|
458
412
|
if (reason != null) msg = reason ? `Not available: ${reason}` : 'Not available.';
|
|
459
413
|
}
|
|
460
414
|
if (!msg) msg = controls.get(f.name)?.error?.() ?? null;
|
|
@@ -527,7 +481,7 @@ export function openConfirm({
|
|
|
527
481
|
|
|
528
482
|
let _resolved = false;
|
|
529
483
|
const win = new ManagedWindow({
|
|
530
|
-
container:
|
|
484
|
+
container: modalHost(),
|
|
531
485
|
id: `twm-confirm-${_modalSeq++}`,
|
|
532
486
|
title: title || 'Confirm',
|
|
533
487
|
icon: icon || (danger ? 'warning' : 'help'),
|
|
@@ -601,6 +555,9 @@ export function openConfirm({
|
|
|
601
555
|
* existing caller has always got.
|
|
602
556
|
* - `onMaximizeChange` `(maximized, contentEl) => void`, called on each
|
|
603
557
|
* flip. Only useful with `maximizable`.
|
|
558
|
+
* - `beforeClose` `() => boolean | Promise<boolean>`: asked when Esc or
|
|
559
|
+
* the X would dismiss; answering false keeps the modal open. An
|
|
560
|
+
* action button does not ask it.
|
|
604
561
|
*
|
|
605
562
|
* ══ C29. WHY A DIALOG'S MAXIMISE IS THE GEOMETRIC ONE ═══════════════════
|
|
606
563
|
*
|
|
@@ -642,6 +599,7 @@ export function openModal({
|
|
|
642
599
|
backdropOpacity = undefined,
|
|
643
600
|
maximizable = false,
|
|
644
601
|
onMaximizeChange = null,
|
|
602
|
+
beforeClose = null,
|
|
645
603
|
} = {}) {
|
|
646
604
|
return new Promise((resolve) => {
|
|
647
605
|
const body = document.createElement('div');
|
|
@@ -726,7 +684,7 @@ export function openModal({
|
|
|
726
684
|
_onMax = null;
|
|
727
685
|
};
|
|
728
686
|
const win = new ManagedWindow({
|
|
729
|
-
container:
|
|
687
|
+
container: modalHost(),
|
|
730
688
|
id: `twm-modal-${_modalSeq++}`,
|
|
731
689
|
title,
|
|
732
690
|
icon,
|
|
@@ -747,6 +705,7 @@ export function openModal({
|
|
|
747
705
|
canDrag: true,
|
|
748
706
|
defaultWidth: width,
|
|
749
707
|
defaultHeight: height,
|
|
708
|
+
beforeClose,
|
|
750
709
|
// Esc, the X and the backdrop all land here without passing through
|
|
751
710
|
// `close`, so the listener is dropped here as well as there — the
|
|
752
711
|
// three dismissals a user reaches for most are exactly the ones
|
|
@@ -953,14 +912,68 @@ function _offered(options) {
|
|
|
953
912
|
return flatChoices(options).filter((o) => !o.disabled);
|
|
954
913
|
}
|
|
955
914
|
|
|
956
|
-
|
|
957
|
-
|
|
915
|
+
/**
|
|
916
|
+
* A `select` with `create`: `createCombobox` over the enabled options. The box holds
|
|
917
|
+
* the text; `read` is the value of the option that text names, else the text. A
|
|
918
|
+
* disabled option is not offered, and a default or `write` naming one leaves the box
|
|
919
|
+
* empty; typed all the same, it is `unavailable`.
|
|
920
|
+
*/
|
|
921
|
+
function _mountCombobox(slot, f, value) {
|
|
922
|
+
let options = f.options;
|
|
923
|
+
const named = (text) => findChoice(flatChoices(options), text, true);
|
|
924
|
+
const shown = (v) => (v == null || named(v)?.disabled ? '' : String(v));
|
|
925
|
+
|
|
926
|
+
const hint = document.createElement('span');
|
|
927
|
+
hint.className = 'twm-combobox__hint';
|
|
928
|
+
hint.hidden = true;
|
|
929
|
+
// With `create: true` the typed value simply IS the value — there is no
|
|
930
|
+
// entity being made, so promising to "create" one is a lie that reads as
|
|
931
|
+
// a warning.
|
|
932
|
+
const noun = f.create === true ? null : _createSpec(f).hint || 'new entry';
|
|
933
|
+
const updateHint = () => {
|
|
934
|
+
const v = c.value.trim();
|
|
935
|
+
const isNew = noun && v && !flatChoices(options).some((o) => String(o.value) === v);
|
|
936
|
+
if (isNew) hint.textContent = `↳ will create ${noun} “${v}”`;
|
|
937
|
+
hint.hidden = !isNew;
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const c = createCombobox({
|
|
941
|
+
options: _offered(options),
|
|
942
|
+
value: shown(value),
|
|
943
|
+
name: f.name,
|
|
944
|
+
placeholder: f.placeholder ?? 'type to pick or create…',
|
|
945
|
+
// Enter keeps what was typed: this field exists to accept a value nobody listed.
|
|
946
|
+
autoHighlight: false,
|
|
947
|
+
onChange: () => updateHint(),
|
|
948
|
+
});
|
|
949
|
+
if (f.required) c.input.required = true;
|
|
950
|
+
c.el.appendChild(hint);
|
|
951
|
+
slot.replaceWith(c.el);
|
|
952
|
+
updateHint();
|
|
953
|
+
return {
|
|
954
|
+
read() {
|
|
955
|
+
const o = named(c.value);
|
|
956
|
+
return o ? o.value : c.value;
|
|
957
|
+
},
|
|
958
|
+
write(v) { c.value = shown(v); updateHint(); },
|
|
959
|
+
focus: c.focus,
|
|
960
|
+
unavailable() {
|
|
961
|
+
const o = c.value === '' ? undefined : named(c.value);
|
|
962
|
+
return o?.disabled ? (o.reason || '') : null;
|
|
963
|
+
},
|
|
964
|
+
setOptions(next) {
|
|
965
|
+
options = next;
|
|
966
|
+
c.setOptions(_offered(next));
|
|
967
|
+
updateHint();
|
|
968
|
+
},
|
|
969
|
+
dispose: c.destroy,
|
|
970
|
+
};
|
|
958
971
|
}
|
|
959
972
|
|
|
960
973
|
/**
|
|
961
974
|
* Mount a field that is a component rather than markup — a `toggle`, a
|
|
962
|
-
* `select
|
|
963
|
-
* `
|
|
975
|
+
* `select`, `checklist` or `radio` drawn by `choice_controls.js`, a `select`
|
|
976
|
+
* with `create` drawn by `createCombobox`, or a `color` / `colorList` — into the
|
|
964
977
|
* `data-control-slot` its row left, and hand back what the form needs of it:
|
|
965
978
|
* `{ read(), write(v), focus(), unavailable(), submit?(), error?(),
|
|
966
979
|
* setOptions?(options), dispose?() }`. `unavailable()` is the reason (or '')
|
|
@@ -1018,6 +1031,7 @@ function _mountControl(host, f, value, onChange) {
|
|
|
1018
1031
|
dispose: () => c.dispose(),
|
|
1019
1032
|
};
|
|
1020
1033
|
}
|
|
1034
|
+
if (f.type === 'select' && f.create) return _mountCombobox(slot, f, value);
|
|
1021
1035
|
const common = { keepMissing: false, loose: true };
|
|
1022
1036
|
const disabledReason = (entry) => (entry?.disabled ? (entry.reason || '') : null);
|
|
1023
1037
|
if (f.type === 'select') {
|
|
@@ -1113,7 +1127,7 @@ function _wirePicker(host, f) {
|
|
|
1113
1127
|
? String(picked.label) : _pickerLabel(f, hidden.value);
|
|
1114
1128
|
// A hidden input fires no `change` of its own; this is the one the
|
|
1115
1129
|
// field-error clear and `onFieldChange` listen for.
|
|
1116
|
-
hidden
|
|
1130
|
+
fireEvent(hidden, 'change', { bubbles: true });
|
|
1117
1131
|
};
|
|
1118
1132
|
btn.addEventListener('click', open);
|
|
1119
1133
|
// The read-only box is the obvious thing to click; make it work too.
|
|
@@ -1121,33 +1135,6 @@ function _wirePicker(host, f) {
|
|
|
1121
1135
|
}
|
|
1122
1136
|
|
|
1123
1137
|
|
|
1124
|
-
/** Show / hide the "↳ will create …" hint as the user types into a
|
|
1125
|
-
* create-capable combobox. */
|
|
1126
|
-
function _wireComboboxHint(host, f, optionsOf) {
|
|
1127
|
-
const input = host.querySelector(`input[name="${f.name}"]`);
|
|
1128
|
-
const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
|
|
1129
|
-
if (!input || !hintEl) return;
|
|
1130
|
-
// With `create: true` the typed value simply IS the value — there is no
|
|
1131
|
-
// entity being made, so promising to "create" one is a lie that reads as
|
|
1132
|
-
// a warning.
|
|
1133
|
-
if (f.create === true) return;
|
|
1134
|
-
const label = _createSpec(f).hint || 'new entry';
|
|
1135
|
-
const update = () => {
|
|
1136
|
-
const v = input.value.trim();
|
|
1137
|
-
const known = new Set(flatChoices(optionsOf()).map((o) => String(o.value)));
|
|
1138
|
-
if (v && !known.has(v)) {
|
|
1139
|
-
hintEl.textContent = `↳ will create ${label} “${v}”`;
|
|
1140
|
-
hintEl.hidden = false;
|
|
1141
|
-
} else {
|
|
1142
|
-
hintEl.hidden = true;
|
|
1143
|
-
}
|
|
1144
|
-
};
|
|
1145
|
-
input.addEventListener('input', update);
|
|
1146
|
-
update();
|
|
1147
|
-
return update;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
1138
|
function _renderEntry(f, defaults) {
|
|
1152
1139
|
if (f && f.section != null) {
|
|
1153
1140
|
// A named section is addressable by `setVisible`, so a heading can be
|
|
@@ -1247,23 +1234,6 @@ function _renderField(f, value) {
|
|
|
1247
1234
|
</div>
|
|
1248
1235
|
`);
|
|
1249
1236
|
}
|
|
1250
|
-
if (f.type === 'select' && f.create) {
|
|
1251
|
-
// A default naming a disabled option is not taken, as in a select.
|
|
1252
|
-
const shown = findChoice(flatChoices(f.options), value, true)?.disabled ? '' : v;
|
|
1253
|
-
const dlId = `ea-dl-${f.name}-${_modalSeq}`;
|
|
1254
|
-
const opts = _datalistOptions(f.options);
|
|
1255
|
-
const placeholder = f.placeholder
|
|
1256
|
-
? `placeholder="${_esc(f.placeholder)}"` : 'placeholder="type to pick or create…"';
|
|
1257
|
-
return wrap(`
|
|
1258
|
-
<div class="twm-combobox">
|
|
1259
|
-
<input name="${f.name}" type="text" list="${dlId}"
|
|
1260
|
-
value="${_esc(shown)}" ${placeholder} ${required}
|
|
1261
|
-
autocomplete="off">
|
|
1262
|
-
<datalist id="${dlId}" data-datalist-for="${f.name}">${opts}</datalist>
|
|
1263
|
-
<span class="twm-combobox__hint" data-for="${f.name}" hidden></span>
|
|
1264
|
-
</div>
|
|
1265
|
-
`);
|
|
1266
|
-
}
|
|
1267
1237
|
if (f.type === 'select') return wrap(slot);
|
|
1268
1238
|
const step = f.step != null ? `step="${f.step}"` : '';
|
|
1269
1239
|
const placeholder = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : '';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ROWS `createSelect`, `createCombobox` AND `createTagInput`'s SUGGESTIONS
|
|
3
|
+
* DRAW: a `role="listbox"` of options, each with a glyph, a label, a second line,
|
|
4
|
+
* a hint at the end of the row and, when disabled, its reason; `{ group, options }`
|
|
5
|
+
* sections under a heading; one active row that the arrow keys walk.
|
|
6
|
+
*
|
|
7
|
+
* Focus never comes here. The text box or trigger that holds it names the active
|
|
8
|
+
* row with `aria-activedescendant` (`onActive`), and a press on a row is
|
|
9
|
+
* prevented from taking focus, so that a list row can be chosen with the box
|
|
10
|
+
* still holding the caret, and no blur closes the list under the pointer.
|
|
11
|
+
*
|
|
12
|
+
* `aria-selected` says which row IS the current value; the active row is a class.
|
|
13
|
+
* They are different things in a select, where the highlight walks away from the
|
|
14
|
+
* value and comes back. Every word is built with `textContent`: these lists carry
|
|
15
|
+
* text users wrote.
|
|
16
|
+
*
|
|
17
|
+
* An internal part: not exported from `@flexdesk/widgets`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
let SEQ = 0;
|
|
21
|
+
|
|
22
|
+
function node(tag, className, text) {
|
|
23
|
+
const n = document.createElement(tag);
|
|
24
|
+
if (className) n.className = className;
|
|
25
|
+
if (text !== undefined && text !== null) n.textContent = String(text);
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `text` with the first run of `needle` in a `<mark>`, so it is plain why a row is offered. */
|
|
30
|
+
function marked(target, text, needle) {
|
|
31
|
+
const at = needle ? text.toLowerCase().indexOf(needle.toLowerCase()) : -1;
|
|
32
|
+
if (at < 0) { target.textContent = text; return; }
|
|
33
|
+
target.append(text.slice(0, at), node('mark', 'twm-option-list__match', text.slice(at, at + needle.length)),
|
|
34
|
+
text.slice(at + needle.length));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} o
|
|
39
|
+
* @param {(option: object) => void} o.onPick A row was clicked. Never a disabled one.
|
|
40
|
+
* @param {(id: string|null) => void} [o.onActive] The active row's id changed.
|
|
41
|
+
*/
|
|
42
|
+
export function createOptionList({ onPick, onActive = null }) {
|
|
43
|
+
const id = `twm-option-list-${++SEQ}`;
|
|
44
|
+
const el = node('div', 'twm-option-list');
|
|
45
|
+
el.id = id;
|
|
46
|
+
el.setAttribute('role', 'listbox');
|
|
47
|
+
|
|
48
|
+
/** Every row drawn: `{ option, row }`. */
|
|
49
|
+
let rows = [];
|
|
50
|
+
let active = -1;
|
|
51
|
+
|
|
52
|
+
const paintActive = () => {
|
|
53
|
+
rows.forEach(({ row }, i) => row.classList.toggle('twm-option-list__row--active', i === active));
|
|
54
|
+
const row = rows[active]?.row;
|
|
55
|
+
// jsdom does not implement it, and a test harness is no reason to drop it.
|
|
56
|
+
row?.scrollIntoView?.({ block: 'nearest' });
|
|
57
|
+
onActive?.(row ? row.id : null);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const rowFor = (option, { glyphs, mark, isCurrent }) => {
|
|
61
|
+
const row = node('div', 'twm-option-list__row');
|
|
62
|
+
row.id = `${id}-${rows.length}`;
|
|
63
|
+
row.setAttribute('role', 'option');
|
|
64
|
+
row.setAttribute('aria-selected', String(!!isCurrent?.(option)));
|
|
65
|
+
if (option.disabled) row.setAttribute('aria-disabled', 'true');
|
|
66
|
+
row.title = option.description ? `${option.label} — ${option.description}` : option.label;
|
|
67
|
+
if (glyphs) {
|
|
68
|
+
const glyph = node('span', 'twm-option-list__glyph material-symbols-outlined', option.icon || '');
|
|
69
|
+
glyph.setAttribute('aria-hidden', 'true');
|
|
70
|
+
row.appendChild(glyph);
|
|
71
|
+
}
|
|
72
|
+
const words = node('span', 'twm-option-list__words');
|
|
73
|
+
const label = node('span', 'twm-option-list__label');
|
|
74
|
+
marked(label, option.label, mark);
|
|
75
|
+
words.appendChild(label);
|
|
76
|
+
if (option.description) words.appendChild(node('span', 'twm-option-list__description', option.description));
|
|
77
|
+
if (option.disabled && option.reason) words.appendChild(node('span', 'twm-choice__reason', option.reason));
|
|
78
|
+
row.appendChild(words);
|
|
79
|
+
if (option.hint) row.appendChild(node('span', 'twm-option-list__hint', option.hint));
|
|
80
|
+
return row;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const enabled = () => rows.map((r, i) => (r.option.disabled ? -1 : i)).filter((i) => i >= 0);
|
|
84
|
+
|
|
85
|
+
// A press on the list leaves focus where it is. See the header.
|
|
86
|
+
el.addEventListener('mousedown', (ev) => ev.preventDefault());
|
|
87
|
+
el.addEventListener('click', (ev) => {
|
|
88
|
+
const row = ev.target.closest?.('[role="option"]');
|
|
89
|
+
const hit = rows.find((r) => r.row === row);
|
|
90
|
+
if (hit && !hit.option.disabled) onPick(hit.option);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
el,
|
|
95
|
+
id,
|
|
96
|
+
/**
|
|
97
|
+
* Draw `sections` (`[{ heading, items }]`, as `choiceSections` gives them).
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {string} [opts.mark] The text to mark in each label.
|
|
100
|
+
* @param {(option: object) => boolean} [opts.isCurrent] Is this the current value?
|
|
101
|
+
*/
|
|
102
|
+
setRows(sections, { mark = '', isCurrent = null } = {}) {
|
|
103
|
+
rows = [];
|
|
104
|
+
active = -1;
|
|
105
|
+
el.replaceChildren();
|
|
106
|
+
const glyphs = sections.some((s) => s.items.some((o) => o.icon));
|
|
107
|
+
for (const { heading, items } of sections) {
|
|
108
|
+
let parent = el;
|
|
109
|
+
if (heading != null) {
|
|
110
|
+
parent = node('div', 'twm-option-list__section');
|
|
111
|
+
parent.setAttribute('role', 'group');
|
|
112
|
+
parent.setAttribute('aria-label', heading);
|
|
113
|
+
const title = node('div', 'twm-choice__group', heading);
|
|
114
|
+
title.setAttribute('aria-hidden', 'true');
|
|
115
|
+
parent.appendChild(title);
|
|
116
|
+
el.appendChild(parent);
|
|
117
|
+
}
|
|
118
|
+
for (const option of items) {
|
|
119
|
+
const row = rowFor(option, { glyphs, mark, isCurrent });
|
|
120
|
+
rows.push({ option, row });
|
|
121
|
+
parent.appendChild(row);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
paintActive();
|
|
125
|
+
},
|
|
126
|
+
/** One line of text in place of the rows: nothing matched, still loading, a failure. */
|
|
127
|
+
setMessage(text) {
|
|
128
|
+
rows = [];
|
|
129
|
+
active = -1;
|
|
130
|
+
el.replaceChildren(node('div', 'twm-option-list__message', text));
|
|
131
|
+
paintActive();
|
|
132
|
+
},
|
|
133
|
+
/** How many rows are drawn. */
|
|
134
|
+
get size() { return rows.length; },
|
|
135
|
+
/** The drawn options, in order. */
|
|
136
|
+
options: () => rows.map((r) => r.option),
|
|
137
|
+
/** The active row's option, or null. */
|
|
138
|
+
activeOption: () => rows[active]?.option ?? null,
|
|
139
|
+
/** Make row `index` active; -1, or a row that is disabled, makes none active. */
|
|
140
|
+
setActive(index) {
|
|
141
|
+
active = rows[index] && !rows[index].option.disabled ? index : -1;
|
|
142
|
+
paintActive();
|
|
143
|
+
},
|
|
144
|
+
/** The next row that may be chosen, by `step`, wrapping. From none: the first or the last. */
|
|
145
|
+
move(step) {
|
|
146
|
+
const stops = enabled();
|
|
147
|
+
if (!stops.length) return;
|
|
148
|
+
const at = stops.indexOf(active);
|
|
149
|
+
const next = at < 0 ? (step > 0 ? 0 : stops.length - 1) : (at + step + stops.length) % stops.length;
|
|
150
|
+
active = stops[next];
|
|
151
|
+
paintActive();
|
|
152
|
+
},
|
|
153
|
+
/** The first (`step` 1) or last (`step` -1) row that may be chosen. */
|
|
154
|
+
moveToEnd(step) {
|
|
155
|
+
const stops = enabled();
|
|
156
|
+
active = stops.length ? stops[step > 0 ? 0 : stops.length - 1] : -1;
|
|
157
|
+
paintActive();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A POPOVER — a panel of rich content hung off the element that opened it. (0.18.0)
|
|
3
|
+
*
|
|
4
|
+
* `role="dialog"`, on an anchored layer (`anchored_layer.js`, which says how it is
|
|
5
|
+
* placed and what closes it). Focus moves into it on open and comes back to the
|
|
6
|
+
* anchor on close. Opening one from the anchor that holds an open one closes it,
|
|
7
|
+
* a toggle; opening one anywhere else closes the open one, unless that one owns
|
|
8
|
+
* the new anchor (`anchored_layer.js`).
|
|
9
|
+
*
|
|
10
|
+
* TAB STAYS INSIDE, cycling the panel's controls, as a modal's and a `CategoryPicker`'s
|
|
11
|
+
* do. The panel is mounted at the end of the page, so past its edge lies whatever comes
|
|
12
|
+
* next in the document, which is not the anchor's neighbour and, over a modal, is behind
|
|
13
|
+
* it. Escape is the way out.
|
|
14
|
+
*/
|
|
15
|
+
import { openAnchoredLayer } from './anchored_layer.js';
|
|
16
|
+
import { collectFocusable } from '../utils/focusable.js';
|
|
17
|
+
|
|
18
|
+
/** The open popovers, to find the one an anchor holds. */
|
|
19
|
+
const OPEN = new Set();
|
|
20
|
+
|
|
21
|
+
/** What a call answers when nothing opened. */
|
|
22
|
+
const nothingOpened = (toggled) => ({ el: null, close() {}, closed: Promise.resolve(), reposition() {}, toggled });
|
|
23
|
+
|
|
24
|
+
/** Tab at the panel's edge goes round to the other edge. */
|
|
25
|
+
function keepTabInside(el) {
|
|
26
|
+
el.addEventListener('keydown', (ev) => {
|
|
27
|
+
if (ev.key !== 'Tab' || ev.defaultPrevented) return;
|
|
28
|
+
const controls = collectFocusable(el);
|
|
29
|
+
if (!controls.length) { ev.preventDefault(); return; }
|
|
30
|
+
const first = controls[0];
|
|
31
|
+
const last = controls[controls.length - 1];
|
|
32
|
+
const active = el.ownerDocument.activeElement;
|
|
33
|
+
if (ev.shiftKey ? (active === first || active === el) : active === last) {
|
|
34
|
+
ev.preventDefault();
|
|
35
|
+
(ev.shiftKey ? last : first).focus();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {object} o
|
|
42
|
+
* @param {HTMLElement} o.anchor What opened it; focus returns here.
|
|
43
|
+
* @param {HTMLElement} o.content The popover's body.
|
|
44
|
+
* @param {string} [o.label] Its accessible name.
|
|
45
|
+
* @param {string} [o.className] Extra classes on the panel.
|
|
46
|
+
* @param {HTMLElement} [o.focus] What takes focus on open; else the panel.
|
|
47
|
+
* @param {'start'|'end'} [o.align] Which anchor edge it lines up with first.
|
|
48
|
+
* @param {string} [o.inside] A selector for floating things of your own that
|
|
49
|
+
* are not "outside" it.
|
|
50
|
+
* @param {(reason: string) => void} [o.onClose] After it is gone.
|
|
51
|
+
* @returns {{el: HTMLElement|null, close: (reason?: string) => void, closed: Promise<void>,
|
|
52
|
+
* reposition: () => void, toggled: boolean}}
|
|
53
|
+
* `el` is null when nothing opened, and `onClose` is not called for it: `toggled`
|
|
54
|
+
* is true when the call only CLOSED the popover its anchor held, false when
|
|
55
|
+
* the anchor has left the document.
|
|
56
|
+
*/
|
|
57
|
+
export function openPopover({ anchor, content, label = '', className = '', focus = null, align = 'start',
|
|
58
|
+
inside = null, onClose = null }) {
|
|
59
|
+
for (const open of OPEN) {
|
|
60
|
+
if (anchor && open.anchor === anchor) {
|
|
61
|
+
open.close('toggle');
|
|
62
|
+
return nothingOpened(true);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const el = document.createElement('div');
|
|
67
|
+
el.className = `twm-popover${className ? ` ${className}` : ''}`;
|
|
68
|
+
el.setAttribute('role', 'dialog');
|
|
69
|
+
if (label) el.setAttribute('aria-label', label);
|
|
70
|
+
el.tabIndex = -1;
|
|
71
|
+
el.appendChild(content);
|
|
72
|
+
keepTabInside(el);
|
|
73
|
+
|
|
74
|
+
let settle;
|
|
75
|
+
const closed = new Promise((resolve) => { settle = resolve; });
|
|
76
|
+
const layer = openAnchoredLayer({
|
|
77
|
+
anchor, el, align, gap: 4, inside,
|
|
78
|
+
onClose(reason) {
|
|
79
|
+
OPEN.delete(handle);
|
|
80
|
+
anchor?.classList?.remove('twm-is-open');
|
|
81
|
+
anchor?.setAttribute?.('aria-expanded', 'false');
|
|
82
|
+
onClose?.(reason);
|
|
83
|
+
settle();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
if (!layer) return nothingOpened(false);
|
|
87
|
+
const handle = { el, anchor, closed, toggled: false, close: layer.close, reposition: layer.place };
|
|
88
|
+
OPEN.add(handle);
|
|
89
|
+
anchor?.classList?.add('twm-is-open');
|
|
90
|
+
if (anchor && !anchor.hasAttribute?.('aria-haspopup')) anchor.setAttribute?.('aria-haspopup', 'dialog');
|
|
91
|
+
anchor?.setAttribute?.('aria-expanded', 'true');
|
|
92
|
+
(focus || el).focus?.({ preventScroll: true });
|
|
93
|
+
return handle;
|
|
94
|
+
}
|
|
File without changes
|