@zeyos/zx 2.1.0 → 2.2.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 +39 -0
- package/README.md +15 -0
- package/dist/types/compat/base.d.ts +156 -0
- package/dist/types/compat/globals.d.ts +20 -0
- package/dist/types/compat/index.d.ts +128 -0
- package/dist/types/compat/map/basic.d.ts +175 -0
- package/dist/types/compat/map/data.d.ts +102 -0
- package/dist/types/compat/map/date-time.d.ts +80 -0
- package/dist/types/compat/map/factory.d.ts +9 -0
- package/dist/types/compat/map/form.d.ts +98 -0
- package/dist/types/compat/map/helpers.d.ts +12 -0
- package/dist/types/compat/map/http.d.ts +36 -0
- package/dist/types/compat/map/layout.d.ts +106 -0
- package/dist/types/compat/map/options.d.ts +80 -0
- package/dist/types/compat/map/select.d.ts +95 -0
- package/dist/types/compat/map/stubs.d.ts +32 -0
- package/dist/types/compat/map/utilities.d.ts +37 -0
- package/dist/types/compat-entry.d.ts +5 -0
- package/dist/types/components/badge/badge.d.ts +46 -0
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
- package/dist/types/components/button/button.d.ts +42 -0
- package/dist/types/components/check-button/check-button.d.ts +98 -0
- package/dist/types/components/checklist/checklist.d.ts +143 -0
- package/dist/types/components/checklist/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +130 -0
- package/dist/types/components/copy/copy-button.d.ts +48 -0
- package/dist/types/components/copy/copy-input.d.ts +91 -0
- package/dist/types/components/data-filter/data-filter.d.ts +250 -0
- package/dist/types/components/data-filter/filter-core.d.ts +52 -0
- package/dist/types/components/date-picker/date-picker.d.ts +137 -0
- package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
- package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
- package/dist/types/components/date-picker/month-picker.d.ts +75 -0
- package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
- package/dist/types/components/date-picker/time-picker.d.ts +127 -0
- package/dist/types/components/datebox/date-range-box.d.ts +218 -0
- package/dist/types/components/datebox/datebox.d.ts +160 -0
- package/dist/types/components/datebox/field-adapter.d.ts +5 -0
- package/dist/types/components/dialog/dialog.d.ts +284 -0
- package/dist/types/components/dropdown/dropdown.d.ts +118 -0
- package/dist/types/components/empty-state/empty-state.d.ts +38 -0
- package/dist/types/components/field/field.d.ts +231 -0
- package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
- package/dist/types/components/field-upload/field-upload.d.ts +213 -0
- package/dist/types/components/fieldset/fieldset.d.ts +125 -0
- package/dist/types/components/finder/finder.d.ts +252 -0
- package/dist/types/components/form/form.d.ts +194 -0
- package/dist/types/components/groupbox/groupbox.d.ts +89 -0
- package/dist/types/components/layout/layout.d.ts +132 -0
- package/dist/types/components/loading/inline-loading.d.ts +86 -0
- package/dist/types/components/loading/progress-bar.d.ts +168 -0
- package/dist/types/components/loading/spinner.d.ts +34 -0
- package/dist/types/components/master-panel/master-panel.d.ts +112 -0
- package/dist/types/components/menu-button/menu-button.d.ts +142 -0
- package/dist/types/components/message/message.d.ts +212 -0
- package/dist/types/components/modal/modal.d.ts +105 -0
- package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
- package/dist/types/components/number-field/field-adapter.d.ts +5 -0
- package/dist/types/components/number-field/number-field.d.ts +292 -0
- package/dist/types/components/pagination/pagination.d.ts +267 -0
- package/dist/types/components/panel/panel.d.ts +194 -0
- package/dist/types/components/permission/permission.d.ts +93 -0
- package/dist/types/components/rating/field-adapter.d.ts +5 -0
- package/dist/types/components/rating/rating.d.ts +213 -0
- package/dist/types/components/search/search.d.ts +98 -0
- package/dist/types/components/select/field-adapter.d.ts +5 -0
- package/dist/types/components/select/filter.d.ts +11 -0
- package/dist/types/components/select/select.d.ts +235 -0
- package/dist/types/components/skeleton/skeleton.d.ts +97 -0
- package/dist/types/components/slider/field-adapter.d.ts +5 -0
- package/dist/types/components/slider/slider.d.ts +216 -0
- package/dist/types/components/split-view/split-view.d.ts +335 -0
- package/dist/types/components/stepper/stepper.d.ts +314 -0
- package/dist/types/components/stepper/steps.d.ts +131 -0
- package/dist/types/components/tabbox/tabbox.d.ts +230 -0
- package/dist/types/components/table/sort.d.ts +49 -0
- package/dist/types/components/table/table.d.ts +872 -0
- package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
- package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
- package/dist/types/components/timebox/field-adapter.d.ts +5 -0
- package/dist/types/components/timebox/timebox.d.ts +121 -0
- package/dist/types/components/toggle/field-adapter.d.ts +5 -0
- package/dist/types/components/toggle/toggle.d.ts +94 -0
- package/dist/types/components/toolbar/toolbar.d.ts +230 -0
- package/dist/types/components/tooltip/tooltip.d.ts +183 -0
- package/dist/types/components/tree/hierarchy.d.ts +113 -0
- package/dist/types/components/tree/tree.d.ts +383 -0
- package/dist/types/components/truncate/truncate.d.ts +61 -0
- package/dist/types/components/value-list/field-adapter.d.ts +5 -0
- package/dist/types/components/value-list/value-list.d.ts +143 -0
- package/dist/types/core/breakpoint.d.ts +68 -0
- package/dist/types/core/component.d.ts +115 -0
- package/dist/types/core/date.d.ts +50 -0
- package/dist/types/core/dom.d.ts +86 -0
- package/dist/types/core/export.d.ts +70 -0
- package/dist/types/core/fontawesome.d.ts +107 -0
- package/dist/types/core/format.d.ts +107 -0
- package/dist/types/core/http.d.ts +122 -0
- package/dist/types/core/i18n.d.ts +35 -0
- package/dist/types/core/icons.d.ts +137 -0
- package/dist/types/core/keyboard.d.ts +72 -0
- package/dist/types/core/position.d.ts +45 -0
- package/dist/types/core/storage.d.ts +46 -0
- package/dist/types/core/util.d.ts +117 -0
- package/dist/types/elements/define.d.ts +73 -0
- package/dist/types/elements/reflect.d.ts +27 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/zeyos/connect.d.ts +65 -0
- package/dist/types/zeyos/form.d.ts +140 -0
- package/dist/types/zeyos/icons.d.ts +104 -0
- package/dist/types/zeyos/index.d.ts +12 -0
- package/dist/types/zeyos/modules.d.ts +110 -0
- package/dist/types/zeyos/query.d.ts +157 -0
- package/dist/types/zeyos/schema.d.ts +79 -0
- package/dist/types/zeyos/select.d.ts +167 -0
- package/dist/types/zeyos/table.d.ts +205 -0
- package/dist/zx-compat.esm.js +362 -33
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +359 -67
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +342 -43
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +68 -6
- package/dist/zx.esm.js +346 -67
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +346 -67
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +48 -2
- package/docs/llms.md +28 -0
- package/package.json +37 -9
- package/src/compat/base.js +291 -0
- package/src/compat/globals.js +165 -0
- package/src/compat/index.js +84 -0
- package/src/compat/map/basic.js +464 -0
- package/src/compat/map/data.js +232 -0
- package/src/compat/map/date-time.js +177 -0
- package/src/compat/map/factory.js +45 -0
- package/src/compat/map/form.js +330 -0
- package/src/compat/map/helpers.js +46 -0
- package/src/compat/map/http.js +117 -0
- package/src/compat/map/layout.js +256 -0
- package/src/compat/map/options.js +267 -0
- package/src/compat/map/select.js +198 -0
- package/src/compat/map/stubs.js +26 -0
- package/src/compat/map/utilities.js +113 -0
- package/src/compat-entry.js +11 -0
- package/src/components/badge/badge.css +147 -0
- package/src/components/badge/badge.js +92 -0
- package/src/components/breadcrumb/breadcrumb.css +105 -0
- package/src/components/breadcrumb/breadcrumb.js +331 -0
- package/src/components/button/button.css +104 -0
- package/src/components/button/button.js +76 -0
- package/src/components/check-button/check-button.css +66 -0
- package/src/components/check-button/check-button.js +167 -0
- package/src/components/checklist/checklist.css +65 -0
- package/src/components/checklist/checklist.js +282 -0
- package/src/components/checklist/field-adapter.js +46 -0
- package/src/components/context-menu/context-menu.css +54 -0
- package/src/components/context-menu/context-menu.js +334 -0
- package/src/components/copy/copy-button.css +27 -0
- package/src/components/copy/copy-button.js +78 -0
- package/src/components/copy/copy-input.css +59 -0
- package/src/components/copy/copy-input.js +123 -0
- package/src/components/data-filter/data-filter.css +53 -0
- package/src/components/data-filter/data-filter.js +400 -0
- package/src/components/data-filter/filter-core.js +128 -0
- package/src/components/date-picker/date-picker.css +181 -0
- package/src/components/date-picker/date-picker.js +500 -0
- package/src/components/date-picker/date-range-picker.css +264 -0
- package/src/components/date-picker/date-range-picker.js +990 -0
- package/src/components/date-picker/field-adapter.js +36 -0
- package/src/components/date-picker/month-picker.js +223 -0
- package/src/components/date-picker/range-field-adapter.js +31 -0
- package/src/components/date-picker/time-picker.css +191 -0
- package/src/components/date-picker/time-picker.js +406 -0
- package/src/components/datebox/date-range-box.css +128 -0
- package/src/components/datebox/date-range-box.js +492 -0
- package/src/components/datebox/datebox.css +108 -0
- package/src/components/datebox/datebox.js +358 -0
- package/src/components/datebox/field-adapter.js +34 -0
- package/src/components/dialog/dialog.css +74 -0
- package/src/components/dialog/dialog.js +450 -0
- package/src/components/dropdown/dropdown.css +24 -0
- package/src/components/dropdown/dropdown.js +288 -0
- package/src/components/empty-state/empty-state.css +80 -0
- package/src/components/empty-state/empty-state.js +92 -0
- package/src/components/field/field.css +127 -0
- package/src/components/field/field.js +605 -0
- package/src/components/field-upload/field-adapter.js +21 -0
- package/src/components/field-upload/field-upload.css +108 -0
- package/src/components/field-upload/field-upload.js +475 -0
- package/src/components/fieldset/fieldset.css +37 -0
- package/src/components/fieldset/fieldset.js +307 -0
- package/src/components/finder/finder.css +98 -0
- package/src/components/finder/finder.js +509 -0
- package/src/components/form/form.css +32 -0
- package/src/components/form/form.js +302 -0
- package/src/components/groupbox/groupbox.css +51 -0
- package/src/components/groupbox/groupbox.js +199 -0
- package/src/components/layout/layout.css +117 -0
- package/src/components/layout/layout.js +136 -0
- package/src/components/loading/inline-loading.css +34 -0
- package/src/components/loading/inline-loading.js +140 -0
- package/src/components/loading/progress-bar.css +115 -0
- package/src/components/loading/progress-bar.js +259 -0
- package/src/components/loading/spinner.css +58 -0
- package/src/components/loading/spinner.js +57 -0
- package/src/components/master-panel/master-panel.css +51 -0
- package/src/components/master-panel/master-panel.js +203 -0
- package/src/components/menu-button/menu-button.css +49 -0
- package/src/components/menu-button/menu-button.js +294 -0
- package/src/components/message/message.css +122 -0
- package/src/components/message/message.js +499 -0
- package/src/components/modal/modal.css +47 -0
- package/src/components/modal/modal.js +203 -0
- package/src/components/multi-value-editor/field-adapter.js +44 -0
- package/src/components/multi-value-editor/multi-value-editor.css +56 -0
- package/src/components/multi-value-editor/multi-value-editor.js +170 -0
- package/src/components/navigation-bar/navigation-bar.css +130 -0
- package/src/components/navigation-bar/navigation-bar.js +450 -0
- package/src/components/number-field/field-adapter.js +40 -0
- package/src/components/number-field/number-field.css +77 -0
- package/src/components/number-field/number-field.js +545 -0
- package/src/components/pagination/pagination.css +95 -0
- package/src/components/pagination/pagination.js +481 -0
- package/src/components/panel/panel.css +123 -0
- package/src/components/panel/panel.js +336 -0
- package/src/components/permission/permission.css +47 -0
- package/src/components/permission/permission.js +198 -0
- package/src/components/rating/field-adapter.js +34 -0
- package/src/components/rating/rating.css +97 -0
- package/src/components/rating/rating.js +395 -0
- package/src/components/search/search.css +70 -0
- package/src/components/search/search.js +193 -0
- package/src/components/select/field-adapter.js +24 -0
- package/src/components/select/filter.js +39 -0
- package/src/components/select/select.css +180 -0
- package/src/components/select/select.js +663 -0
- package/src/components/skeleton/skeleton.css +76 -0
- package/src/components/skeleton/skeleton.js +139 -0
- package/src/components/slider/field-adapter.js +36 -0
- package/src/components/slider/slider.css +181 -0
- package/src/components/slider/slider.js +399 -0
- package/src/components/split-view/split-view.css +139 -0
- package/src/components/split-view/split-view.js +806 -0
- package/src/components/stepper/stepper.css +234 -0
- package/src/components/stepper/stepper.js +459 -0
- package/src/components/stepper/steps.js +131 -0
- package/src/components/tabbox/tabbox.css +281 -0
- package/src/components/tabbox/tabbox.js +484 -0
- package/src/components/table/sort.js +85 -0
- package/src/components/table/table.css +364 -0
- package/src/components/table/table.js +1693 -0
- package/src/components/tag-picker/field-adapter.js +35 -0
- package/src/components/tag-picker/tag-picker.css +169 -0
- package/src/components/tag-picker/tag-picker.js +837 -0
- package/src/components/timebox/field-adapter.js +24 -0
- package/src/components/timebox/timebox.css +51 -0
- package/src/components/timebox/timebox.js +276 -0
- package/src/components/toggle/field-adapter.js +24 -0
- package/src/components/toggle/toggle.css +66 -0
- package/src/components/toggle/toggle.js +150 -0
- package/src/components/toolbar/toolbar.css +81 -0
- package/src/components/toolbar/toolbar.js +511 -0
- package/src/components/tooltip/tooltip.css +36 -0
- package/src/components/tooltip/tooltip.js +420 -0
- package/src/components/tree/hierarchy.js +151 -0
- package/src/components/tree/tree.css +99 -0
- package/src/components/tree/tree.js +784 -0
- package/src/components/truncate/truncate.css +20 -0
- package/src/components/truncate/truncate.js +92 -0
- package/src/components/value-list/field-adapter.js +24 -0
- package/src/components/value-list/value-list.css +108 -0
- package/src/components/value-list/value-list.js +336 -0
- package/src/core/breakpoint.js +150 -0
- package/src/core/component.js +292 -0
- package/src/core/date.js +216 -0
- package/src/core/dom.js +260 -0
- package/src/core/export.js +204 -0
- package/src/core/fontawesome.js +177 -0
- package/src/core/format.js +283 -0
- package/src/core/http.js +246 -0
- package/src/core/i18n.js +83 -0
- package/src/core/icons.js +283 -0
- package/src/core/keyboard.js +240 -0
- package/src/core/position.js +177 -0
- package/src/core/storage.js +175 -0
- package/src/core/util.js +336 -0
- package/src/elements/define.js +715 -0
- package/src/elements/reflect.js +79 -0
- package/src/index.js +131 -0
- package/src/zeyos/connect.js +142 -0
- package/src/zeyos/form.js +254 -0
- package/src/zeyos/icons.js +92 -0
- package/src/zeyos/index.js +21 -0
- package/src/zeyos/modules.js +269 -0
- package/src/zeyos/query.js +199 -0
- package/src/zeyos/schema.js +474 -0
- package/src/zeyos/select.js +175 -0
- package/src/zeyos/table.js +272 -0
- package/styles/base.css +124 -0
- package/styles/icons.css +34 -0
- package/styles/tokens/dark.css +84 -0
- package/styles/tokens/density.css +9 -0
- package/styles/tokens/global.css +80 -0
- package/styles/tokens/modules.css +109 -0
- package/styles/tokens/semantic.css +49 -0
- package/styles/zx.css +61 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h, resolveElement } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { rovingTabindex, typeahead } from '../../core/keyboard.js';
|
|
5
|
+
import { Dropdown } from '../dropdown/dropdown.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../menu-button/menu-button.js').MenuItem} MenuItem */
|
|
8
|
+
/** @typedef {MenuItem|'-'} ContextMenuEntry */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} ContextMenuOptions
|
|
12
|
+
* @property {ContextMenuEntry[]|((context: Element|null) => ContextMenuEntry[])} [items=[]] Menu
|
|
13
|
+
* items and `'-'` separators, or a function returning them for the element the menu is opening
|
|
14
|
+
* on — which is how a row menu disables the actions that row does not allow. Returning an empty
|
|
15
|
+
* array cancels the opening.
|
|
16
|
+
* @property {string|null} [selector=null] Restricts the menu to descendants matching this
|
|
17
|
+
* selector, and reports the matched element as the context of the selection. A table passes
|
|
18
|
+
* `'tbody tr'` here to get a per-row menu from one listener.
|
|
19
|
+
* @property {(event: CustomEvent<{value: unknown, item: MenuItem, context: Element|null}>) => void} [onselect]
|
|
20
|
+
* Selection listener.
|
|
21
|
+
* @property {(event: CustomEvent<{context: Element|null}>) => void} [onopen] Open listener.
|
|
22
|
+
* @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close listener.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const MENU_ITEM_SELECTOR = '[role="menuitem"]';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Right-click menu for a region, implementing the APG menu pattern.
|
|
29
|
+
*
|
|
30
|
+
* Attaches to an existing element and never changes it: the menu itself lives in the top layer,
|
|
31
|
+
* anchored to a zero-sized element parked at the pointer. `selector` turns one instance into a
|
|
32
|
+
* per-row menu — the listener stays on the container, and the row that was clicked arrives with
|
|
33
|
+
* the selection as `context`.
|
|
34
|
+
*
|
|
35
|
+
* Reachable from the keyboard as the platform expects: the Menu key or Shift+F10 opens the menu at
|
|
36
|
+
* the focused element, and closing returns focus to where it came from. Pointer-only context
|
|
37
|
+
* menus are a common accessibility failure, so both routes are wired here rather than left to the
|
|
38
|
+
* application.
|
|
39
|
+
*
|
|
40
|
+
* Note that the component's root is the anchor, which lives on `<body>` — so the bubbling `zx-*`
|
|
41
|
+
* DOM events land there, not in the target's tree. Listen with `on('select')` or `onselect`.
|
|
42
|
+
*
|
|
43
|
+
* @fires ContextMenu#select
|
|
44
|
+
* @fires ContextMenu#open
|
|
45
|
+
* @fires ContextMenu#close
|
|
46
|
+
* @extends {Component<ContextMenuOptions>}
|
|
47
|
+
*/
|
|
48
|
+
export class ContextMenu extends Component {
|
|
49
|
+
/** @type {ContextMenuOptions} */
|
|
50
|
+
static defaults = {
|
|
51
|
+
items: [],
|
|
52
|
+
selector: null
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** @type {Element} */
|
|
56
|
+
#target;
|
|
57
|
+
/** @type {Dropdown} */
|
|
58
|
+
#dropdown;
|
|
59
|
+
/** @type {HTMLElement} */
|
|
60
|
+
#panel;
|
|
61
|
+
/** @type {ReturnType<typeof rovingTabindex>} */
|
|
62
|
+
#roving;
|
|
63
|
+
/** @type {(event: KeyboardEvent|string) => void} */
|
|
64
|
+
#typeahead;
|
|
65
|
+
/** @type {ContextMenuEntry[]} */
|
|
66
|
+
#items = [];
|
|
67
|
+
/** @type {Element|null} */
|
|
68
|
+
#context = null;
|
|
69
|
+
/** @type {Element|null} */
|
|
70
|
+
#returnFocus = null;
|
|
71
|
+
#destroyed = false;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Attaches a context menu to an existing element.
|
|
75
|
+
* @param {Element|string} target Element or selector the menu belongs to.
|
|
76
|
+
* @param {ContextMenuOptions} [options={}] Context-menu options.
|
|
77
|
+
*/
|
|
78
|
+
constructor(target, options = {}) {
|
|
79
|
+
const resolved = resolveElement(target);
|
|
80
|
+
if (!resolved) throw new TypeError('ContextMenu target could not be resolved');
|
|
81
|
+
super(null, options);
|
|
82
|
+
this.#target = resolved;
|
|
83
|
+
document.body.append(this.el);
|
|
84
|
+
|
|
85
|
+
this.#dropdown = new Dropdown(this.el, { openOn: 'manual', placement: 'bottom-start' });
|
|
86
|
+
this.#panel = this.#dropdown.getPanel();
|
|
87
|
+
this.#panel.classList.add('zx-context-menu');
|
|
88
|
+
this.#panel.setAttribute('role', 'menu');
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(this.options.items)) this.setItems(this.options.items);
|
|
91
|
+
this.#roving = rovingTabindex(this.#panel, MENU_ITEM_SELECTOR);
|
|
92
|
+
this.#typeahead = typeahead(() => this.#menuItems(), (item) => item.focus());
|
|
93
|
+
|
|
94
|
+
this.#dropdown.on('close', () => {
|
|
95
|
+
this.#context = null;
|
|
96
|
+
const restore = this.#returnFocus;
|
|
97
|
+
this.#returnFocus = null;
|
|
98
|
+
if (restore instanceof HTMLElement && restore.isConnected) restore.focus();
|
|
99
|
+
this.emit('close');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.listen(this.#target, 'contextmenu', (event) => this.#onContextMenu(event));
|
|
103
|
+
this.listen(this.#target, 'keydown', (event) => this.#onTargetKeydown(event));
|
|
104
|
+
this.listen(this.#panel, 'click', (event) => {
|
|
105
|
+
const item = event.target.closest?.(MENU_ITEM_SELECTOR);
|
|
106
|
+
if (item && this.#panel.contains(item)) this.#select(item);
|
|
107
|
+
});
|
|
108
|
+
this.listen(this.#panel, 'keydown', (event) => this.#onMenuKeydown(event));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates the zero-sized element the menu is anchored to.
|
|
113
|
+
* @returns {HTMLElement}
|
|
114
|
+
*/
|
|
115
|
+
render() {
|
|
116
|
+
return h('span', { class: 'zx-context-menu__anchor', ariaHidden: 'true' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Replaces the menu items.
|
|
121
|
+
* @param {ContextMenuEntry[]} items Menu definitions.
|
|
122
|
+
* @returns {this}
|
|
123
|
+
*/
|
|
124
|
+
setItems(items) {
|
|
125
|
+
this.#items = Array.isArray(items) ? items.slice() : [];
|
|
126
|
+
this.#panel.replaceChildren();
|
|
127
|
+
this.#items.forEach((item, index) => {
|
|
128
|
+
if (item === '-') {
|
|
129
|
+
this.#panel.append(h('div', { class: 'zx-context-menu__separator', role: 'separator' }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const children = [];
|
|
133
|
+
if (item.icon) children.push(h('span', { class: 'zx-context-menu__icon' }, icon(item.icon)));
|
|
134
|
+
children.push(h('span', { class: 'zx-context-menu__label' }, String(item.label ?? '')));
|
|
135
|
+
this.#panel.append(h('button', {
|
|
136
|
+
class: 'zx-context-menu__item',
|
|
137
|
+
type: 'button',
|
|
138
|
+
role: 'menuitem',
|
|
139
|
+
tabindex: '-1',
|
|
140
|
+
'data-menu-item': String(index),
|
|
141
|
+
'data-danger': item.danger ? 'true' : null,
|
|
142
|
+
ariaDisabled: item.disabled ? 'true' : null
|
|
143
|
+
}, children));
|
|
144
|
+
});
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Opens the menu at a point in viewport coordinates.
|
|
150
|
+
* @param {number} x Client X.
|
|
151
|
+
* @param {number} y Client Y.
|
|
152
|
+
* @param {Element|null} [context=null] Element the menu applies to.
|
|
153
|
+
* @returns {this}
|
|
154
|
+
* @fires ContextMenu#open
|
|
155
|
+
*/
|
|
156
|
+
openAt(x, y, context = null) {
|
|
157
|
+
if (!this.#prepare(context)) return this;
|
|
158
|
+
this.#openResolved(x, y, context);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Opens the menu against an element's box, which is where a keyboard invocation puts it.
|
|
164
|
+
* @param {Element} element Element to anchor to.
|
|
165
|
+
* @returns {this}
|
|
166
|
+
* @fires ContextMenu#open
|
|
167
|
+
*/
|
|
168
|
+
openAtElement(element) {
|
|
169
|
+
const box = element.getBoundingClientRect();
|
|
170
|
+
return this.openAt(box.left, box.bottom, element);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Closes the menu.
|
|
175
|
+
* @returns {this}
|
|
176
|
+
*/
|
|
177
|
+
close() {
|
|
178
|
+
this.#dropdown.close();
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reports whether the menu is open.
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
*/
|
|
186
|
+
isOpen() {
|
|
187
|
+
return this.#dropdown.isOpen();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The element the open menu applies to — the `selector` match under the pointer, or the target.
|
|
192
|
+
* @returns {Element|null}
|
|
193
|
+
*/
|
|
194
|
+
getContext() {
|
|
195
|
+
return this.#context;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detaches the menu and removes its anchor.
|
|
200
|
+
* @returns {void}
|
|
201
|
+
*/
|
|
202
|
+
destroy() {
|
|
203
|
+
if (this.#destroyed) return;
|
|
204
|
+
this.#destroyed = true;
|
|
205
|
+
this.#roving.destroy();
|
|
206
|
+
this.#dropdown.destroy();
|
|
207
|
+
super.destroy();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolves the items for this opening and reports whether it may proceed.
|
|
212
|
+
* @param {Element|null} context Element the menu applies to.
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
#prepare(context) {
|
|
216
|
+
const items = this.options.items;
|
|
217
|
+
if (typeof items === 'function') {
|
|
218
|
+
const resolved = items(context);
|
|
219
|
+
if (!Array.isArray(resolved) || resolved.length === 0) return false;
|
|
220
|
+
this.setItems(resolved);
|
|
221
|
+
}
|
|
222
|
+
return this.#items.length > 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Places the anchor and opens, with the items for this opening already resolved.
|
|
227
|
+
*
|
|
228
|
+
* `left`/`top` rather than the logical insets used everywhere else: these are viewport
|
|
229
|
+
* coordinates from a pointer event, which are physical whatever the writing direction is.
|
|
230
|
+
*
|
|
231
|
+
* @param {number} x Client X.
|
|
232
|
+
* @param {number} y Client Y.
|
|
233
|
+
* @param {Element|null} context Element the menu applies to.
|
|
234
|
+
* @returns {void}
|
|
235
|
+
*/
|
|
236
|
+
#openResolved(x, y, context) {
|
|
237
|
+
const root = /** @type {HTMLElement} */ (this.el);
|
|
238
|
+
root.style.left = `${x}px`;
|
|
239
|
+
root.style.top = `${y}px`;
|
|
240
|
+
this.#context = context;
|
|
241
|
+
this.#returnFocus = document.activeElement;
|
|
242
|
+
this.#dropdown.open();
|
|
243
|
+
this.#roving.focusFirst();
|
|
244
|
+
this.emit('open', { context });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @param {Event} event @returns {void} */
|
|
248
|
+
#onContextMenu(event) {
|
|
249
|
+
const pointer = /** @type {PointerEvent & {target: Element}} */ (event);
|
|
250
|
+
const context = this.#contextFor(pointer.target);
|
|
251
|
+
// Only take over the platform menu once there is something of our own to show — and resolve
|
|
252
|
+
// the items once for the opening, not once to decide and again to open.
|
|
253
|
+
if (!context || !this.#prepare(context)) return;
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
this.#openResolved(pointer.clientX, pointer.clientY, context);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {Event} event @returns {void} */
|
|
259
|
+
#onTargetKeydown(event) {
|
|
260
|
+
const key = /** @type {KeyboardEvent} */ (event);
|
|
261
|
+
if (key.key !== 'ContextMenu' && !(key.key === 'F10' && key.shiftKey)) return;
|
|
262
|
+
const context = this.#contextFor(/** @type {Element} */ (key.target));
|
|
263
|
+
if (!context || !this.#prepare(context)) return;
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
const box = context.getBoundingClientRect();
|
|
266
|
+
this.#openResolved(box.left, box.bottom, context);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The element a pointer or key event applies to, honoring `selector`.
|
|
271
|
+
* @param {Element|null} from Event target.
|
|
272
|
+
* @returns {Element|null}
|
|
273
|
+
*/
|
|
274
|
+
#contextFor(from) {
|
|
275
|
+
if (!this.options.selector) return this.#target;
|
|
276
|
+
const match = from?.closest?.(String(this.options.selector));
|
|
277
|
+
return match && this.#target.contains(match) ? match : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @param {Event} event @returns {void} */
|
|
281
|
+
#onMenuKeydown(event) {
|
|
282
|
+
const key = /** @type {KeyboardEvent} */ (event);
|
|
283
|
+
if (key.key === 'Escape' || key.key === 'Tab') {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
this.close();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (key.key === 'Enter' || key.key === ' ') {
|
|
289
|
+
const item = /** @type {Element} */ (key.target).closest?.(MENU_ITEM_SELECTOR);
|
|
290
|
+
if (!item || !this.#panel.contains(item)) return;
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
this.#select(item);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.#typeahead(key);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @param {Element} element @returns {void} */
|
|
299
|
+
#select(element) {
|
|
300
|
+
const index = Number(element.getAttribute('data-menu-item'));
|
|
301
|
+
const item = this.#items[index];
|
|
302
|
+
if (!item || item === '-' || item.disabled) return;
|
|
303
|
+
const context = this.#context;
|
|
304
|
+
this.emit('select', { value: item.value, item, context });
|
|
305
|
+
try {
|
|
306
|
+
item.onselect?.(item.value, item, this);
|
|
307
|
+
} finally {
|
|
308
|
+
this.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @returns {HTMLElement[]} */
|
|
313
|
+
#menuItems() {
|
|
314
|
+
return Array.from(this.#panel.querySelectorAll(MENU_ITEM_SELECTOR));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Fired when an enabled item is activated. `context` is the element the menu was opened on.
|
|
320
|
+
* @event ContextMenu#select
|
|
321
|
+
* @type {CustomEvent<{value: unknown, item: MenuItem, context: Element|null}>}
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Fired when the menu opens.
|
|
326
|
+
* @event ContextMenu#open
|
|
327
|
+
* @type {CustomEvent<{context: Element|null}>}
|
|
328
|
+
*/
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Fired when the menu closes.
|
|
332
|
+
* @event ContextMenu#close
|
|
333
|
+
* @type {CustomEvent<Record<string, never>>}
|
|
334
|
+
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.zx-copy-button {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--zx-space-2);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.zx-copy-button__icon {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
flex: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.zx-copy-button[data-copied] {
|
|
13
|
+
color: var(--zx-color-success);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The confirmation is spoken, not drawn: the tick already carries it visually, and a live region
|
|
18
|
+
* that also took up space would move the layout every time someone copied something.
|
|
19
|
+
*/
|
|
20
|
+
.zx-copy-button__status {
|
|
21
|
+
position: absolute;
|
|
22
|
+
clip-path: inset(50%);
|
|
23
|
+
inline-size: 1px;
|
|
24
|
+
block-size: 1px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { h } from '../../core/dom.js';
|
|
2
|
+
import { copyToClipboard } from '../../core/export.js';
|
|
3
|
+
import { icon as createIcon } from '../../core/icons.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} CopyButtonOptions
|
|
7
|
+
* @property {string|(() => string)} [text=''] Text to copy, or a function returning it — a
|
|
8
|
+
* function is what a button beside a live value wants, so the copy is never stale.
|
|
9
|
+
* @property {string} [label=''] Visible label. Empty renders an icon-only ghost button.
|
|
10
|
+
* @property {string} [title='Copy'] Accessible name, and the native tooltip on the icon-only form.
|
|
11
|
+
* @property {string} [feedback='Copied'] Confirmation announced and shown after a copy.
|
|
12
|
+
* @property {number} [feedbackDuration=2000] Milliseconds the confirmation stays.
|
|
13
|
+
* @property {'md'|'sm'} [size='md'] Control size.
|
|
14
|
+
* @property {'default'|'primary'|'ghost'} [kind='ghost'] Visual intent.
|
|
15
|
+
* @property {(text: string, copied: boolean) => void} [oncopy] Called with the text and whether
|
|
16
|
+
* the clipboard accepted it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {Readonly<CopyButtonOptions>} */
|
|
20
|
+
const buttonDefaults = Object.freeze({
|
|
21
|
+
text: '',
|
|
22
|
+
label: '',
|
|
23
|
+
title: 'Copy',
|
|
24
|
+
feedback: 'Copied',
|
|
25
|
+
feedbackDuration: 2000,
|
|
26
|
+
size: 'md',
|
|
27
|
+
kind: 'ghost'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a button that copies a string and confirms it in place.
|
|
32
|
+
*
|
|
33
|
+
* The confirmation is the point: a clipboard write is silent, so without it nobody knows whether
|
|
34
|
+
* the click worked. It swaps the glyph for a tick and speaks through a live region, then reverts.
|
|
35
|
+
* A failed write — no permission, an insecure context — leaves the button untouched and reports
|
|
36
|
+
* `false` to `oncopy`, rather than claiming a copy that did not happen.
|
|
37
|
+
*
|
|
38
|
+
* @param {CopyButtonOptions} [opts={}] Copy-button options.
|
|
39
|
+
* @returns {HTMLButtonElement}
|
|
40
|
+
*/
|
|
41
|
+
export function copyButton(opts = {}) {
|
|
42
|
+
const options = { ...buttonDefaults, ...opts };
|
|
43
|
+
const iconOnly = !options.label;
|
|
44
|
+
const element = /** @type {HTMLButtonElement} */ (h('button', {
|
|
45
|
+
class: ['zx-copy-button', iconOnly ? 'zx-icon-btn' : 'zx-btn'],
|
|
46
|
+
type: 'button',
|
|
47
|
+
dataset: { size: options.size === 'sm' ? 'sm' : 'md', kind: iconOnly ? null : options.kind },
|
|
48
|
+
title: iconOnly ? String(options.title) : null,
|
|
49
|
+
ariaLabel: iconOnly ? String(options.title) : null
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const glyph = h('span', { class: 'zx-copy-button__icon', ariaHidden: 'true' },
|
|
53
|
+
createIcon('copy', { size: options.size === 'sm' ? 12 : 14 }));
|
|
54
|
+
const status = h('span', { class: 'zx-copy-button__status', role: 'status' });
|
|
55
|
+
element.append(glyph);
|
|
56
|
+
if (!iconOnly) element.append(h('span', { class: 'zx-copy-button__label' }, String(options.label)));
|
|
57
|
+
element.append(status);
|
|
58
|
+
|
|
59
|
+
let timer = 0;
|
|
60
|
+
element.addEventListener('click', async () => {
|
|
61
|
+
const text = typeof options.text === 'function' ? String(options.text()) : String(options.text ?? '');
|
|
62
|
+
const copied = await copyToClipboard(text);
|
|
63
|
+
options.oncopy?.(text, copied);
|
|
64
|
+
if (!copied) return;
|
|
65
|
+
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
element.dataset.copied = 'true';
|
|
68
|
+
glyph.replaceChildren(createIcon('check', { size: options.size === 'sm' ? 12 : 14 }));
|
|
69
|
+
status.textContent = String(options.feedback);
|
|
70
|
+
timer = setTimeout(() => {
|
|
71
|
+
if (!element.isConnected) return;
|
|
72
|
+
delete element.dataset.copied;
|
|
73
|
+
glyph.replaceChildren(createIcon('copy', { size: options.size === 'sm' ? 12 : 14 }));
|
|
74
|
+
status.textContent = '';
|
|
75
|
+
}, Math.max(0, Number(options.feedbackDuration) || 0));
|
|
76
|
+
});
|
|
77
|
+
return element;
|
|
78
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.zx-copy-input {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--zx-space-1);
|
|
5
|
+
inline-size: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.zx-copy-input__label {
|
|
9
|
+
color: var(--zx-color-text);
|
|
10
|
+
font-size: var(--zx-control-font-size);
|
|
11
|
+
font-weight: 500;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.zx-copy-input[data-hide-label] .zx-copy-input__label {
|
|
15
|
+
position: absolute;
|
|
16
|
+
clip-path: inset(50%);
|
|
17
|
+
inline-size: 1px;
|
|
18
|
+
block-size: 1px;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.zx-copy-input__box {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: var(--zx-space-1);
|
|
27
|
+
min-inline-size: 0;
|
|
28
|
+
padding-inline-end: var(--zx-space-1);
|
|
29
|
+
border: 1px solid var(--zx-color-border-control);
|
|
30
|
+
border-radius: var(--zx-control-radius);
|
|
31
|
+
background: var(--zx-color-bg-control);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.zx-copy-input__box:focus-within {
|
|
35
|
+
border-color: var(--zx-color-accent);
|
|
36
|
+
box-shadow: var(--zx-focus-ring);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* The box draws the control chrome, so the field inside it carries none of its own. */
|
|
40
|
+
.zx-copy-input__field {
|
|
41
|
+
flex: 1 1 auto;
|
|
42
|
+
min-inline-size: 0;
|
|
43
|
+
border: 0;
|
|
44
|
+
border-radius: 0;
|
|
45
|
+
background: none;
|
|
46
|
+
font-family: var(--zx-font-mono);
|
|
47
|
+
font-size: var(--zx-text-sm);
|
|
48
|
+
text-overflow: ellipsis;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.zx-copy-input__field:focus-visible {
|
|
52
|
+
outline: 0;
|
|
53
|
+
box-shadow: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.zx-copy-input[data-size="sm"] .zx-copy-input__field {
|
|
57
|
+
min-block-size: calc(var(--zx-control-height) - 4px);
|
|
58
|
+
font-size: var(--zx-text-xs);
|
|
59
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
3
|
+
import { uid } from '../../core/util.js';
|
|
4
|
+
import { copyButton } from './copy-button.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} CopyInputOptions
|
|
8
|
+
* @property {string} [value=''] Text shown and copied.
|
|
9
|
+
* @property {string} [label=''] Visible label above the box.
|
|
10
|
+
* @property {boolean} [hideLabel=false] Whether the label is kept for assistive technology only.
|
|
11
|
+
* @property {string} [feedback='Copied'] Confirmation shown after a copy.
|
|
12
|
+
* @property {string} [buttonTitle='Copy'] Accessible name of the copy button.
|
|
13
|
+
* @property {'md'|'sm'} [size='md'] Control size.
|
|
14
|
+
* @property {(event: CustomEvent<{value: string, copied: boolean}>) => void} [oncopy] Copy listener.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A read-only box holding a value to be copied — an API endpoint, a document number, a share link
|
|
19
|
+
* — with the copy button attached.
|
|
20
|
+
*
|
|
21
|
+
* The field is `readonly` rather than `disabled` so the text stays selectable and reachable by
|
|
22
|
+
* keyboard; focusing it selects the whole value, which is what someone reaching for Ctrl+C
|
|
23
|
+
* expects.
|
|
24
|
+
*
|
|
25
|
+
* @fires CopyInput#copy
|
|
26
|
+
* @extends {Component<CopyInputOptions>}
|
|
27
|
+
*/
|
|
28
|
+
export class CopyInput extends Component {
|
|
29
|
+
static cssName = 'copy-input';
|
|
30
|
+
|
|
31
|
+
/** @type {CopyInputOptions} */
|
|
32
|
+
static defaults = {
|
|
33
|
+
value: '',
|
|
34
|
+
label: '',
|
|
35
|
+
hideLabel: false,
|
|
36
|
+
feedback: 'Copied',
|
|
37
|
+
buttonTitle: 'Copy',
|
|
38
|
+
size: 'md'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** @returns {HTMLElement} */
|
|
42
|
+
render() {
|
|
43
|
+
// render() runs inside the base constructor, before class-field initializers would run.
|
|
44
|
+
this._createdRoot = this.el === null;
|
|
45
|
+
this._snapshot = this._createdRoot ? null : snapshotTarget(this.el);
|
|
46
|
+
|
|
47
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
48
|
+
this.el = root;
|
|
49
|
+
const id = uid('zx-copy-input');
|
|
50
|
+
|
|
51
|
+
const input = /** @type {HTMLInputElement} */ (h('input', {
|
|
52
|
+
ref: 'input',
|
|
53
|
+
class: 'zx-copy-input__field',
|
|
54
|
+
type: 'text',
|
|
55
|
+
id,
|
|
56
|
+
readOnly: true,
|
|
57
|
+
value: String(this.options.value ?? '')
|
|
58
|
+
}));
|
|
59
|
+
const button = copyButton({
|
|
60
|
+
text: () => this.get(),
|
|
61
|
+
title: String(this.options.buttonTitle),
|
|
62
|
+
feedback: String(this.options.feedback),
|
|
63
|
+
size: this.options.size === 'sm' ? 'sm' : 'md',
|
|
64
|
+
oncopy: (value, copied) => this.emit('copy', { value, copied })
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
root.replaceChildren(
|
|
68
|
+
h('label', { ref: 'label', class: 'zx-copy-input__label', for: id },
|
|
69
|
+
String(this.options.label ?? '')),
|
|
70
|
+
h('div', { class: 'zx-copy-input__box' }, input, button)
|
|
71
|
+
);
|
|
72
|
+
root.dataset.size = this.options.size === 'sm' ? 'sm' : 'md';
|
|
73
|
+
root.toggleAttribute('data-hide-label', Boolean(this.options.hideLabel));
|
|
74
|
+
|
|
75
|
+
this.listen(input, 'focus', () => input.select());
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the current value.
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
get() {
|
|
84
|
+
return this.refs.input.value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Replaces the value.
|
|
89
|
+
* @param {string} value Text to show and copy.
|
|
90
|
+
* @returns {this}
|
|
91
|
+
*/
|
|
92
|
+
set(value) {
|
|
93
|
+
this.refs.input.value = String(value ?? '');
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Focuses the box and selects its contents.
|
|
99
|
+
*
|
|
100
|
+
* The selection is made here as well as in the `focus` listener: the listener covers a click or
|
|
101
|
+
* a Tab, but a programmatic `focus()` fires no focus event when the document itself is not
|
|
102
|
+
* focused, and the value would then be left uncopied under the caret.
|
|
103
|
+
* @returns {this}
|
|
104
|
+
*/
|
|
105
|
+
focus() {
|
|
106
|
+
this.refs.input.focus();
|
|
107
|
+
this.refs.input.select();
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Restores an enhanced target to the markup it had before the takeover. @returns {void} */
|
|
112
|
+
destroy() {
|
|
113
|
+
const root = this.el;
|
|
114
|
+
super.destroy();
|
|
115
|
+
if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fired after a copy attempt; `copied` is false when the clipboard refused the write.
|
|
121
|
+
* @event CopyInput#copy
|
|
122
|
+
* @type {CustomEvent<{value: string, copied: boolean}>}
|
|
123
|
+
*/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.zx-data-filter {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
align-items: end;
|
|
5
|
+
gap: var(--zx-space-3);
|
|
6
|
+
padding: var(--zx-space-3);
|
|
7
|
+
border: 1px solid var(--zx-color-border);
|
|
8
|
+
border-radius: var(--zx-radius-md);
|
|
9
|
+
background: var(--zx-color-bg-surface);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.zx-data-filter__controls {
|
|
13
|
+
display: contents;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.zx-data-filter__field {
|
|
17
|
+
display: grid;
|
|
18
|
+
flex: 1 1 180px;
|
|
19
|
+
gap: var(--zx-space-1);
|
|
20
|
+
min-inline-size: 150px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.zx-data-filter__field > input:not([type="checkbox"]):not([type="radio"]),
|
|
24
|
+
.zx-data-filter__field > select {
|
|
25
|
+
inline-size: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.zx-data-filter__label {
|
|
29
|
+
color: var(--zx-color-text-muted);
|
|
30
|
+
font-size: var(--zx-text-sm);
|
|
31
|
+
font-weight: 650;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.zx-data-filter__badge {
|
|
35
|
+
align-self: center;
|
|
36
|
+
min-inline-size: var(--zx-control-height);
|
|
37
|
+
border-radius: var(--zx-radius-full);
|
|
38
|
+
background: var(--zx-color-bg-selected);
|
|
39
|
+
color: var(--zx-color-accent);
|
|
40
|
+
padding: var(--zx-space-1) var(--zx-space-2);
|
|
41
|
+
font-size: var(--zx-text-sm);
|
|
42
|
+
font-weight: 650;
|
|
43
|
+
text-align: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.zx-data-filter__clear {
|
|
47
|
+
align-self: end;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.zx-data-filter__badge[hidden],
|
|
51
|
+
.zx-data-filter__clear[hidden] {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|