@vmz/ui 0.3.6 → 0.3.8

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 (68) hide show
  1. package/README.md +53 -51
  2. package/contracts/token-requirements.v0.json +58 -1
  3. package/package.json +5 -2
  4. package/presets/web-surface.v0.json +14 -12
  5. package/src/components/Accordion.vmz +127 -94
  6. package/src/components/Alert.vmz +125 -28
  7. package/src/components/AppShell.vmz +125 -57
  8. package/src/components/Autocomplete.vmz +261 -154
  9. package/src/components/Avatar.vmz +20 -12
  10. package/src/components/Badge.vmz +62 -70
  11. package/src/components/Breadcrumb.vmz +86 -72
  12. package/src/components/BulkActions.vmz +56 -51
  13. package/src/components/Button.vmz +237 -211
  14. package/src/components/Callout.vmz +178 -77
  15. package/src/components/Card.vmz +141 -120
  16. package/src/components/Checkbox.vmz +131 -55
  17. package/src/components/CodeBlock.vmz +77 -30
  18. package/src/components/Collapse.vmz +64 -11
  19. package/src/components/ConsoleShell.vmz +405 -251
  20. package/src/components/Content.vmz +15 -1
  21. package/src/components/DataTable.vmz +294 -194
  22. package/src/components/DatePicker.vmz +766 -674
  23. package/src/components/Descriptions.vmz +90 -0
  24. package/src/components/Dialog.vmz +137 -38
  25. package/src/components/Divider.vmz +8 -1
  26. package/src/components/Drawer.vmz +518 -427
  27. package/src/components/Dropdown.vmz +195 -51
  28. package/src/components/Empty.vmz +96 -54
  29. package/src/components/Field.vmz +170 -115
  30. package/src/components/FilterBar.vmz +34 -4
  31. package/src/components/Flex.vmz +3 -1
  32. package/src/components/Footer.vmz +6 -3
  33. package/src/components/Form.vmz +41 -0
  34. package/src/components/FormItem.vmz +78 -79
  35. package/src/components/Grid.vmz +6 -3
  36. package/src/components/Header.vmz +193 -6
  37. package/src/components/Layout.vmz +3 -1
  38. package/src/components/Link.vmz +25 -10
  39. package/src/components/List.vmz +124 -102
  40. package/src/components/Menu.vmz +416 -243
  41. package/src/components/Message.vmz +167 -0
  42. package/src/components/Notification.vmz +132 -37
  43. package/src/components/Pagination.vmz +237 -69
  44. package/src/components/Popconfirm.vmz +287 -0
  45. package/src/components/Popover.vmz +341 -223
  46. package/src/components/Progress.vmz +16 -5
  47. package/src/components/Prose.vmz +180 -81
  48. package/src/components/QueryForm.vmz +31 -9
  49. package/src/components/RadioGroup.vmz +204 -124
  50. package/src/components/Result.vmz +60 -27
  51. package/src/components/Segmented.vmz +86 -51
  52. package/src/components/Select.vmz +620 -533
  53. package/src/components/Sider.vmz +120 -7
  54. package/src/components/Skeleton.vmz +9 -7
  55. package/src/components/Space.vmz +9 -3
  56. package/src/components/Spinner.vmz +5 -4
  57. package/src/components/Steps.vmz +118 -103
  58. package/src/components/Switch.vmz +131 -98
  59. package/src/components/Table.vmz +270 -165
  60. package/src/components/Tabs.vmz +125 -110
  61. package/src/components/Tag.vmz +60 -43
  62. package/src/components/TextArea.vmz +125 -118
  63. package/src/components/Timeline.vmz +111 -76
  64. package/src/components/Toc.vmz +90 -74
  65. package/src/components/Tooltip.vmz +107 -58
  66. package/src/components/Tree.vmz +214 -191
  67. package/src/components/Typography.vmz +23 -8
  68. package/src/components/Upload.vmz +1394 -1289
@@ -1,533 +1,620 @@
1
- <template>
2
- <div data-vmz-ui="select" :data-mode="mode" :data-searchable="searchableAttr" :data-invalid="invalidAttr" :class='"vmz-ui-select" + (open ? " is-opened" : "") + (isMultiple ? " is-multiple" : "")'>
3
- <label v-if="label" :for="controlId" class="vmz-ui-select__label">
4
- {{ label }}
5
- </label>
6
- <button type="button" :id="controlId" role="combobox" :disabled="disabled" aria-haspopup="listbox" :aria-expanded="open ? 'true' : 'false'" :aria-controls="listId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" :aria-multiselectable="isMultiple ? 'true' : 'false'" @click="toggle" :onKeyDown="onTriggerKeyDown" class="vmz-ui-select__control">
7
- <span class="vmz-ui-select__value">
8
- {{ displayLabel }}
9
- </span>
10
- <span aria-hidden="true" class="vmz-ui-select__chev" />
11
- </button>
12
- <div v-if="open" :id="listId" data-vmz-overlay="select" data-vmz-overlay-owner="select" data-vmz-focus="enter" class="vmz-ui-select__panel">
13
- <div v-if="searchable" class="vmz-ui-select__search">
14
- <input type="search" data-vmz-select="search" :value="searchQuery" :placeholder="searchPlaceholder" :aria-label="searchPlaceholder" @input="onSearchInput" :onKeyDown="onSearchKeyDown" class="vmz-ui-select__search-input" />
15
- </div>
16
- <ul role="listbox" :aria-multiselectable="isMultiple ? 'true' : 'false'" tabindex="-1" :onKeyDown="onListKeyDown" class="vmz-ui-select__list">
17
- <li v-if="showEmpty" role="presentation" class="vmz-ui-select__empty">
18
- {{ emptyText }}
19
- </li>
20
- <li v-for="opt in visibleOptions" :key="opt.value" role="presentation">
21
- <button type="button" role="option" :data-vmz-option="opt.value" :aria-selected="opt.selected ? 'true' : 'false'" @click="onOptionClick" :class='"vmz-ui-select__option" + (opt.selected ? " is-on" : "")'>
22
- {{ opt.label }}
23
- </button>
24
- </li>
25
- </ul>
26
- <div v-if="isMultiple" class="vmz-ui-select__footer">
27
- <button type="button" data-vmz-select="done" @click="close" class="vmz-ui-select__done">
28
- Done
29
- </button>
30
- </div>
31
- </div>
32
- <p v-if="description" :id="descriptionId" class="vmz-ui-select__description">
33
- {{ description }}
34
- </p>
35
- <p v-if="error" :id="errorId" role="alert" class="vmz-ui-select__error">
36
- {{ error }}
37
- </p>
38
- </div>
39
- </template>
40
- <style>
41
- .vmz-ui-select {
42
- position: relative;
43
- display: flex;
44
- flex-direction: column;
45
- gap: 0.375rem;
46
- max-width: 28rem;
47
- }
48
-
49
- .vmz-ui-select__label {
50
- font: inherit;
51
- font-weight: 500;
52
- font-size: 0.875rem;
53
- }
54
-
55
- .vmz-ui-select__control {
56
- display: inline-flex;
57
- align-items: center;
58
- justify-content: space-between;
59
- gap: 0.5rem;
60
- width: 100%;
61
- min-height: var(--vmz-control-height, 2rem);
62
- box-sizing: border-box;
63
- font: inherit;
64
- font-size: 0.875rem;
65
- text-align: left;
66
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
67
- border: 1px solid var(--vmz-line-default, #d9d9d9);
68
- border-radius: var(--vmz-radius-md, 6px);
69
- background: var(--vmz-surface-paper, #fff);
70
- color: inherit;
71
- outline: none;
72
- cursor: pointer;
73
- transition:
74
- border-color var(--vmz-motion-control-duration, 120ms)
75
- var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1)),
76
- box-shadow var(--vmz-motion-control-duration, 120ms)
77
- var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
78
- }
79
-
80
- .vmz-ui-select[data-invalid="true"] .vmz-ui-select__control {
81
- border-color: var(--vmz-status-danger-accent);
82
- }
83
-
84
- .vmz-ui-select__control:hover:not(:disabled) {
85
- border-color: var(--vmz-action-primary-background);
86
- }
87
-
88
- .vmz-ui-select__control:focus-visible,
89
- .vmz-ui-select.is-opened .vmz-ui-select__control {
90
- border-color: var(--vmz-action-primary-background);
91
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
92
- }
93
-
94
- .vmz-ui-select__control:disabled {
95
- opacity: 0.55;
96
- cursor: not-allowed;
97
- }
98
-
99
- .vmz-ui-select__value {
100
- flex: 1 1 auto;
101
- min-width: 0;
102
- overflow: hidden;
103
- text-overflow: ellipsis;
104
- white-space: nowrap;
105
- }
106
-
107
- .vmz-ui-select__chev {
108
- flex: 0 0 auto;
109
- width: 0.45rem;
110
- height: 0.45rem;
111
- border-right: 1.5px solid currentColor;
112
- border-bottom: 1.5px solid currentColor;
113
- transform: rotate(45deg) translate(-1px, -2px);
114
- opacity: 0.7;
115
- }
116
-
117
- .vmz-ui-select.is-opened .vmz-ui-select__chev {
118
- transform: rotate(225deg) translate(-1px, -1px);
119
- }
120
-
121
- .vmz-ui-select__panel {
122
- position: absolute;
123
- z-index: 40;
124
- top: calc(100% + 0.25rem);
125
- left: 0;
126
- right: 0;
127
- display: flex;
128
- flex-direction: column;
129
- border: 1px solid var(--vmz-line-default, #d9d9d9);
130
- border-radius: var(--vmz-radius-md, 6px);
131
- background: var(--vmz-surface-paper, #fff);
132
- color: inherit;
133
- box-shadow: var(
134
- --vmz-shadow-popup,
135
- 0 6px 16px 0 rgba(0, 0, 0, 0.08),
136
- 0 3px 6px -4px rgba(0, 0, 0, 0.12)
137
- );
138
- outline: none;
139
- }
140
-
141
- .vmz-ui-select__search {
142
- padding: 0.375rem 0.5rem 0.125rem;
143
- }
144
-
145
- .vmz-ui-select__search-input {
146
- width: 100%;
147
- box-sizing: border-box;
148
- font: inherit;
149
- font-size: 0.875rem;
150
- padding: 0.25rem 0.5rem;
151
- border: 1px solid var(--vmz-line-default, #d9d9d9);
152
- border-radius: var(--vmz-radius-md, 6px);
153
- background: var(--vmz-surface-paper, #fff);
154
- color: inherit;
155
- outline: none;
156
- }
157
-
158
- .vmz-ui-select__search-input:focus-visible {
159
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
160
- }
161
-
162
- .vmz-ui-select__list {
163
- margin: 0;
164
- padding: 0.25rem;
165
- list-style: none;
166
- max-height: 16rem;
167
- overflow: auto;
168
- outline: none;
169
- }
170
-
171
- .vmz-ui-select__empty {
172
- padding: 0.5rem 0.625rem;
173
- opacity: 0.7;
174
- font-size: 0.92em;
175
- }
176
-
177
- .vmz-ui-select__option {
178
- display: block;
179
- width: 100%;
180
- box-sizing: border-box;
181
- text-align: left;
182
- font: inherit;
183
- padding: 0.375rem 0.625rem;
184
- border: 0;
185
- border-radius: var(--vmz-radius-md, 6px);
186
- background: transparent;
187
- color: inherit;
188
- cursor: pointer;
189
- outline: none;
190
- }
191
-
192
- .vmz-ui-select__option:hover,
193
- .vmz-ui-select__option:focus-visible {
194
- background: color-mix(in srgb, var(--vmz-action-primary-background) 18%, transparent);
195
- }
196
-
197
- .vmz-ui-select__option.is-on {
198
- background: color-mix(in srgb, var(--vmz-action-primary-background) 28%, transparent);
199
- font-weight: 600;
200
- }
201
-
202
- .vmz-ui-select__footer {
203
- display: flex;
204
- justify-content: flex-end;
205
- padding: 0.375rem 0.5rem 0.5rem;
206
- border-top: 1px solid var(--vmz-line-default, #d9dee3);
207
- }
208
-
209
- .vmz-ui-select__done {
210
- font: inherit;
211
- font-weight: 600;
212
- padding: 0.25rem 0.7rem;
213
- border: 1px solid var(--vmz-line-default, #d9dee3);
214
- border-radius: var(--vmz-radius-md, 6px);
215
- background: var(--vmz-surface-paper, #fff);
216
- color: inherit;
217
- cursor: pointer;
218
- }
219
-
220
- .vmz-ui-select__description {
221
- margin: 0;
222
- opacity: 0.75;
223
- font-size: 0.9em;
224
- }
225
-
226
- .vmz-ui-select__error {
227
- margin: 0;
228
- color: var(--vmz-status-danger-foreground);
229
- font-size: 0.9em;
230
- }
231
- </style>
232
- <script client>
233
- /**
234
- * VMZ UI — Select.
235
- * Owned listbox (trigger + panel + role=option). Never native <select> popup.
236
- * Parent owns value (single) or values (multiple) + options.
237
- * mode=multiple keeps panel open until Done/Escape; searchable filters visible options.
238
- */
239
- export default class Select {
240
- public label: string = "";
241
- public description: string = "";
242
- public error: string = "";
243
- /** single mode */
244
- public value: string = "";
245
- /** multiple mode — parent owns array */
246
- public values: string[] = [];
247
- public options: Array<{
248
- value: string;
249
- label: string;
250
- }> = [];
251
- /** "single" | "multiple" */
252
- public mode: string = "single";
253
- public searchable: boolean = false;
254
- public searchPlaceholder: string = "Search";
255
- public emptyText: string = "No matches";
256
- public disabled: boolean = false;
257
- public invalidAttr: string = "false";
258
- public controlId: string = "vmz-select";
259
- public listId: string = "vmz-select-list";
260
- public descriptionId: string = "vmz-select-desc";
261
- public errorId: string = "vmz-select-err";
262
- public describedBy: string = "";
263
- public onChange:
264
- | ((e: {
265
- target: {
266
- value: string;
267
- values: string[];
268
- };
269
- }) => void)
270
- | null = null;
271
- open = false;
272
- displayLabel = "";
273
- searchQuery = "";
274
- isMultiple = false;
275
- searchableAttr = "false";
276
- showEmpty = false;
277
- visibleOptions: Array<{
278
- value: string;
279
- label: string;
280
- selected: boolean;
281
- }> = [];
282
- _docKeyHandler = null;
283
- _docPointerHandler = null;
284
- _observer = null;
285
- onMount() {
286
- this.listId = `${this.controlId}-list`;
287
- this._syncMode();
288
- this._rebuildVisible();
289
- if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
290
- const root = this.__vmzDomRoot;
291
- if (!root || typeof root.querySelector !== "function") return;
292
- this._observer = new MutationObserver(() => this._syncOverlay());
293
- this._observer.observe(root, {
294
- childList: true,
295
- subtree: true,
296
- });
297
- }
298
- onDestroy() {
299
- if (this._observer) {
300
- this._observer.disconnect();
301
- this._observer = null;
302
- }
303
- this._teardownDoc();
304
- }
305
- _syncMode() {
306
- this.isMultiple = String(this.mode || "single") === "multiple";
307
- this.searchableAttr = this.searchable ? "true" : "false";
308
- }
309
- _selectedSet() {
310
- if (this.isMultiple) {
311
- return new Set((this.values || []).map((v) => String(v)));
312
- }
313
- return new Set(this.value ? [String(this.value)] : []);
314
- }
315
- _rebuildVisible() {
316
- this._syncMode();
317
- const q = String(this.searchQuery || "")
318
- .trim()
319
- .toLowerCase();
320
- const selected = this._selectedSet();
321
- const all = this.options || [];
322
- const filtered = !q
323
- ? all
324
- : all.filter((o) => {
325
- const label = String(o.label || "").toLowerCase();
326
- const value = String(o.value || "").toLowerCase();
327
- return label.includes(q) || value.includes(q);
328
- });
329
- this.visibleOptions = filtered.map((o) => ({
330
- value: String(o.value),
331
- label: String(o.label),
332
- selected: selected.has(String(o.value)),
333
- }));
334
- this.showEmpty = this.visibleOptions.length === 0;
335
- this._syncLabel();
336
- }
337
- _syncLabel() {
338
- const opts = this.options || [];
339
- if (this.isMultiple) {
340
- const selected = this._selectedSet();
341
- const labels = opts.filter((o) => selected.has(String(o.value))).map((o) => o.label);
342
- if (!labels.length) this.displayLabel = "—";
343
- else if (labels.length <= 2) this.displayLabel = labels.join(", ");
344
- else this.displayLabel = `${labels[0]}, ${labels[1]} +${labels.length - 2}`;
345
- return;
346
- }
347
- const hit = opts.find((o) => String(o.value) === String(this.value));
348
- this.displayLabel = hit ? hit.label : this.value || "—";
349
- }
350
- toggle() {
351
- if (this.disabled) return;
352
- if (this.open) this.close();
353
- else this.openPanel();
354
- }
355
- openPanel() {
356
- this.searchQuery = "";
357
- this._rebuildVisible();
358
- this.open = true;
359
- }
360
- close() {
361
- this.open = false;
362
- this.searchQuery = "";
363
- this._rebuildVisible();
364
- }
365
- _emit(nextValue: string, nextValues: string[]) {
366
- const payload = {
367
- target: {
368
- value: nextValue,
369
- values: nextValues,
370
- },
371
- };
372
- this.emit("change", payload);
373
- if (typeof this.onChange === "function") {
374
- this.onChange(payload);
375
- }
376
- }
377
- /** Parent-owned prop writes (bindComponentProp) — keep derived label/options in sync. */
378
- __vmzOnParentProp(name: string) {
379
- if (name === "value" || name === "values" || name === "options") this._rebuildVisible();
380
- }
381
- choose(next: string) {
382
- if (!next && next !== "") return;
383
- if (this.isMultiple) {
384
- const cur = [...(this.values || [])].map(String);
385
- const idx = cur.indexOf(String(next));
386
- if (idx >= 0) cur.splice(idx, 1);
387
- else cur.push(String(next));
388
- this.values = cur;
389
- this._rebuildVisible();
390
- this._emit(cur.join(","), cur);
391
- return;
392
- }
393
- const nextStr = String(next);
394
- if (nextStr === String(this.value)) {
395
- this.close();
396
- return;
397
- }
398
- this.value = nextStr;
399
- this._rebuildVisible();
400
- this._emit(nextStr, nextStr ? [nextStr] : []);
401
- this.close();
402
- }
403
- onSearchInput(ev: Event) {
404
- const t = ev?.target as HTMLInputElement | null;
405
- this.searchQuery = t ? String(t.value || "") : "";
406
- this._rebuildVisible();
407
- }
408
- onSearchKeyDown(ev: KeyboardEvent) {
409
- if (!ev) return;
410
- if (ev.key === "Escape") {
411
- ev.preventDefault();
412
- this.close();
413
- } else if (ev.key === "ArrowDown") {
414
- ev.preventDefault();
415
- const root = this.__vmzDomRoot;
416
- const first =
417
- root && typeof root.querySelector === "function"
418
- ? root.querySelector('[data-vmz-overlay="select"] [role="option"]')
419
- : null;
420
- if (first instanceof HTMLElement) first.focus();
421
- }
422
- }
423
- onOptionClick(ev: Event) {
424
- const t = ev?.target as Element | null;
425
- const el =
426
- (t && typeof (t as HTMLElement).closest === "function"
427
- ? ((t as HTMLElement).closest("[data-vmz-option]") as HTMLElement | null)
428
- : null) || (ev?.currentTarget as HTMLElement | null);
429
- const id =
430
- el && typeof el.getAttribute === "function" ? el.getAttribute("data-vmz-option") : null;
431
- if (id != null) this.choose(id);
432
- }
433
- onTriggerKeyDown(ev: KeyboardEvent) {
434
- if (!ev || this.disabled) return;
435
- if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
436
- ev.preventDefault();
437
- this.openPanel();
438
- } else if (ev.key === "Escape" && this.open) {
439
- ev.preventDefault();
440
- this.close();
441
- }
442
- }
443
- onListKeyDown(ev: KeyboardEvent) {
444
- if (!ev) return;
445
- if (ev.key === "Escape") {
446
- ev.preventDefault();
447
- this.close();
448
- return;
449
- }
450
- const root = this.__vmzDomRoot;
451
- const list =
452
- root && typeof root.querySelector === "function"
453
- ? root.querySelector(`[data-vmz-overlay="select"] [role="listbox"]`)
454
- : null;
455
- if (!list) return;
456
- const nodes = [...list.querySelectorAll('[role="option"]')].filter(
457
- (n) => n instanceof HTMLElement && n.offsetParent !== null,
458
- );
459
- if (!nodes.length) return;
460
- const idx = nodes.indexOf(document.activeElement as Element);
461
- if (ev.key === "ArrowDown") {
462
- ev.preventDefault();
463
- (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
464
- } else if (ev.key === "ArrowUp") {
465
- ev.preventDefault();
466
- (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
467
- } else if (ev.key === "Home") {
468
- ev.preventDefault();
469
- (nodes[0] as HTMLElement).focus();
470
- } else if (ev.key === "End") {
471
- ev.preventDefault();
472
- (nodes[nodes.length - 1] as HTMLElement).focus();
473
- } else if (ev.key === "Enter" || ev.key === " ") {
474
- const active = document.activeElement as HTMLElement | null;
475
- const id = active?.getAttribute?.("data-vmz-option");
476
- if (id) {
477
- ev.preventDefault();
478
- this.choose(id);
479
- }
480
- }
481
- }
482
- _syncOverlay() {
483
- const root = this.__vmzDomRoot;
484
- const panel =
485
- root && typeof root.querySelector === "function"
486
- ? root.querySelector('[data-vmz-overlay="select"][data-vmz-focus="enter"]')
487
- : null;
488
- if (panel && this.open) this._bindDoc(panel as HTMLElement);
489
- else if (!panel) this._teardownDoc();
490
- this._rebuildVisible();
491
- }
492
- _bindDoc(panel: HTMLElement) {
493
- if (typeof document === "undefined") return;
494
- if (!this._docKeyHandler) {
495
- this._docKeyHandler = (ev: KeyboardEvent) => {
496
- if (ev.key === "Escape") {
497
- ev.preventDefault();
498
- this.close();
499
- }
500
- };
501
- document.addEventListener("keydown", this._docKeyHandler);
502
- }
503
- if (!this._docPointerHandler) {
504
- const root = this.__vmzDomRoot;
505
- this._docPointerHandler = (ev: Event) => {
506
- const t = ev.target;
507
- if (!(t instanceof Node)) return;
508
- if (root && typeof root.contains === "function" && root.contains(t)) return;
509
- this.close();
510
- };
511
- document.addEventListener("pointerdown", this._docPointerHandler, true);
512
- }
513
- const search = panel.querySelector('[data-vmz-select="search"]');
514
- if (search instanceof HTMLElement) {
515
- search.focus();
516
- return;
517
- }
518
- const first = panel.querySelector('[role="option"]');
519
- if (first instanceof HTMLElement) first.focus();
520
- else panel.focus();
521
- }
522
- _teardownDoc() {
523
- if (typeof document !== "undefined") {
524
- if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
525
- if (this._docPointerHandler) {
526
- document.removeEventListener("pointerdown", this._docPointerHandler, true);
527
- }
528
- }
529
- this._docKeyHandler = null;
530
- this._docPointerHandler = null;
531
- }
532
- }
533
- </script>
1
+ <template>
2
+ <div data-vmz-ui="select" :data-mode="mode" :data-searchable="searchableAttr" :data-invalid="invalidAttr" :class='"vmz-ui-select" + (open ? " is-opened" : "") + (isMultiple ? " is-multiple" : "")'>
3
+ <label v-if="label" :for="controlId" class="vmz-ui-select__label">
4
+ {{ label }}
5
+ </label>
6
+ <div class="vmz-ui-select__field">
7
+ <button type="button" :id="controlId" role="combobox" :disabled="disabled" aria-haspopup="listbox" :aria-expanded="open ? 'true' : 'false'" :aria-controls="listId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" :aria-multiselectable="isMultiple ? 'true' : 'false'" @click="toggle" :onKeyDown="onTriggerKeyDown" :class='"vmz-ui-select__control" + (displayLabel ? "" : " is-placeholder")'>
8
+ <span class="vmz-ui-select__value">
9
+ {{ displayLabel }}
10
+ </span>
11
+ <Icon name="action.chevron-down" size="md" decorative class="vmz-ui-select__chev" />
12
+ </button>
13
+ <div v-if="open" :id="listId" data-vmz-overlay="select" data-vmz-overlay-owner="select" data-vmz-focus="enter" class="vmz-ui-select__panel">
14
+ <div v-if="searchable" class="vmz-ui-select__search">
15
+ <input type="search" data-vmz-select="search" :value="searchQuery" :placeholder="searchPlaceholder" :aria-label="searchLabel()" @input="onSearchInput" :onKeyDown="onSearchKeyDown" class="vmz-ui-select__search-input" />
16
+ </div>
17
+ <ul role="listbox" :aria-multiselectable="isMultiple ? 'true' : 'false'" tabindex="-1" :onKeyDown="onListKeyDown" class="vmz-ui-select__list">
18
+ <li v-if="showEmpty && emptyText" role="presentation" class="vmz-ui-select__empty">
19
+ {{ emptyText }}
20
+ </li>
21
+ <li v-for="opt in visibleOptions" :key="opt.value" role="presentation">
22
+ <button type="button" role="option" :data-vmz-option="opt.value" :aria-selected="opt.selected ? 'true' : 'false'" @click="onOptionClick" :class='"vmz-ui-select__option" + (opt.selected ? " is-on" : "")'>
23
+ <span class="vmz-ui-select__option-label">
24
+ {{ opt.label }}
25
+ </span>
26
+ <Icon v-if="opt.selected" name="action.check" size="md" decorative class="vmz-ui-select__option-check" />
27
+ </button>
28
+ </li>
29
+ </ul>
30
+ <div v-if="isMultiple && doneLabel" class="vmz-ui-select__footer">
31
+ <button type="button" data-vmz-select="done" @click="close" class="vmz-ui-select__done">
32
+ {{ doneLabel }}
33
+ </button>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <p v-if="description" :id="descriptionId" class="vmz-ui-select__description">
38
+ {{ description }}
39
+ </p>
40
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-select__error">
41
+ {{ error }}
42
+ </p>
43
+ </div>
44
+ </template>
45
+ <style>
46
+ .vmz-ui-select {
47
+ position: relative;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--vmz-space-2);
51
+ width: 100%;
52
+ max-width: 28rem;
53
+ }
54
+
55
+ .vmz-ui-select__field {
56
+ position: relative;
57
+ display: flex;
58
+ flex-direction: column;
59
+ width: 100%;
60
+ }
61
+
62
+ .vmz-ui-select__label {
63
+ font: inherit;
64
+ font-weight: 500;
65
+ font-size: 0.875rem;
66
+ line-height: 1.5715;
67
+ color: var(--vmz-text-ink);
68
+ }
69
+
70
+ .vmz-ui-select__control {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: space-between;
74
+ gap: var(--vmz-space-2);
75
+ width: 100%;
76
+ box-sizing: border-box;
77
+ min-height: var(--vmz-control-height);
78
+ font: inherit;
79
+ font-size: 0.875rem;
80
+ line-height: 1.5715;
81
+ text-align: left;
82
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
83
+ border: 1px solid var(--vmz-line-default);
84
+ border-radius: var(--vmz-radius-lg);
85
+ background: var(--vmz-surface-paper);
86
+ color: var(--vmz-text-ink);
87
+ outline: none;
88
+ cursor: pointer;
89
+ transition:
90
+ border-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
91
+ box-shadow var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
92
+ background-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
93
+ }
94
+
95
+ .vmz-ui-select__control.is-placeholder .vmz-ui-select__value {
96
+ color: var(--vmz-text-tertiary);
97
+ }
98
+
99
+ .vmz-ui-select[data-invalid="true"] .vmz-ui-select__control {
100
+ border-color: var(--vmz-status-danger-accent);
101
+ }
102
+
103
+ .vmz-ui-select[data-invalid="true"] .vmz-ui-select__control:hover:not(:disabled),
104
+ .vmz-ui-select[data-invalid="true"] .vmz-ui-select__control:focus-visible,
105
+ .vmz-ui-select[data-invalid="true"].is-opened .vmz-ui-select__control {
106
+ border-color: var(--vmz-status-danger-foreground);
107
+ }
108
+
109
+ .vmz-ui-select__control:hover:not(:disabled):not(:focus-visible) {
110
+ border-color: var(--vmz-action-primary-hover);
111
+ }
112
+
113
+ .vmz-ui-select__control:focus-visible,
114
+ .vmz-ui-select.is-opened .vmz-ui-select__control {
115
+ border-color: var(--vmz-action-primary-background);
116
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
117
+ }
118
+
119
+ .vmz-ui-select__control:disabled {
120
+ opacity: 0.6;
121
+ color: var(--vmz-text-muted);
122
+ cursor: not-allowed;
123
+ background: var(--vmz-surface-mist);
124
+ }
125
+
126
+ .vmz-ui-select__value {
127
+ flex: 1 1 auto;
128
+ min-width: 0;
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ white-space: nowrap;
132
+ }
133
+
134
+ .vmz-ui-select__chev {
135
+ flex: 0 0 auto;
136
+ color: var(--vmz-text-tertiary);
137
+ transition: transform var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
138
+ }
139
+
140
+ .vmz-ui-select.is-opened .vmz-ui-select__chev {
141
+ transform: rotate(180deg);
142
+ }
143
+
144
+ .vmz-ui-select__panel {
145
+ position: absolute;
146
+ z-index: 40;
147
+ top: calc(100% + var(--vmz-space-1));
148
+ left: 0;
149
+ right: 0;
150
+ display: flex;
151
+ flex-direction: column;
152
+ padding: var(--vmz-space-1);
153
+ border: 1px solid var(--vmz-line-default);
154
+ border-radius: var(--vmz-radius-lg);
155
+ background: var(--vmz-surface-paper);
156
+ color: var(--vmz-text-ink);
157
+ box-shadow: var(--vmz-shadow-popup);
158
+ outline: none;
159
+ }
160
+
161
+ .vmz-ui-select__search {
162
+ padding: var(--vmz-space-1) var(--vmz-space-1) 0;
163
+ }
164
+
165
+ .vmz-ui-select__search-input {
166
+ width: 100%;
167
+ box-sizing: border-box;
168
+ min-height: var(--vmz-control-height);
169
+ font: inherit;
170
+ font-size: 0.875rem;
171
+ line-height: 1.5715;
172
+ appearance: none;
173
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
174
+ border: 1px solid var(--vmz-line-default);
175
+ border-radius: var(--vmz-radius-md);
176
+ background: var(--vmz-surface-paper);
177
+ color: var(--vmz-text-ink);
178
+ outline: none;
179
+ transition:
180
+ border-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
181
+ box-shadow var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
182
+ }
183
+
184
+ .vmz-ui-select__search-input::placeholder {
185
+ color: var(--vmz-text-tertiary);
186
+ opacity: 1;
187
+ }
188
+
189
+ .vmz-ui-select__search-input:focus-visible {
190
+ border-color: var(--vmz-action-primary-background);
191
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
192
+ }
193
+
194
+ .vmz-ui-select__list {
195
+ margin: 0;
196
+ padding: 0;
197
+ list-style: none;
198
+ max-height: 16rem;
199
+ overflow: auto;
200
+ outline: none;
201
+ }
202
+
203
+ .vmz-ui-select__empty {
204
+ padding: var(--vmz-space-2) var(--vmz-density-control-padding-x);
205
+ color: var(--vmz-text-tertiary);
206
+ font-size: 0.875rem;
207
+ line-height: 1.5715;
208
+ }
209
+
210
+ .vmz-ui-select__option {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ gap: var(--vmz-space-2);
215
+ width: 100%;
216
+ box-sizing: border-box;
217
+ min-height: var(--vmz-control-height);
218
+ text-align: left;
219
+ font: inherit;
220
+ font-size: 0.875rem;
221
+ line-height: 1.5715;
222
+ padding: 0 var(--vmz-density-control-padding-x);
223
+ border: 0;
224
+ border-radius: var(--vmz-radius-md);
225
+ background: transparent;
226
+ color: var(--vmz-text-ink);
227
+ cursor: pointer;
228
+ outline: none;
229
+ transition: background-color var(--vmz-motion-control-duration)
230
+ var(--vmz-motion-control-easing);
231
+ }
232
+
233
+ .vmz-ui-select__option-label {
234
+ flex: 1 1 auto;
235
+ min-width: 0;
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ }
240
+
241
+ .vmz-ui-select__option-check {
242
+ flex: 0 0 auto;
243
+ color: var(--vmz-action-primary-background);
244
+ }
245
+
246
+ .vmz-ui-select__option:hover,
247
+ .vmz-ui-select__option:focus-visible {
248
+ background: var(--vmz-surface-mist);
249
+ }
250
+
251
+ .vmz-ui-select__option:focus-visible {
252
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
253
+ }
254
+
255
+ .vmz-ui-select__option.is-on {
256
+ background: var(--vmz-action-primary-soft);
257
+ color: var(--vmz-action-primary-active);
258
+ }
259
+
260
+ .vmz-ui-select__footer {
261
+ display: flex;
262
+ justify-content: flex-end;
263
+ padding: var(--vmz-space-1) var(--vmz-space-1) 0;
264
+ border-top: 1px solid var(--vmz-line-subtle);
265
+ }
266
+
267
+ .vmz-ui-select__done {
268
+ font: inherit;
269
+ font-size: 0.875rem;
270
+ font-weight: 500;
271
+ line-height: 1.5715;
272
+ min-height: var(--vmz-control-height-sm);
273
+ padding: 0 var(--vmz-space-3);
274
+ border: 1px solid var(--vmz-action-secondary-border);
275
+ border-radius: var(--vmz-radius-md);
276
+ background: var(--vmz-action-secondary-background);
277
+ color: var(--vmz-action-secondary-foreground);
278
+ cursor: pointer;
279
+ outline: none;
280
+ transition:
281
+ background-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
282
+ border-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
283
+ }
284
+
285
+ .vmz-ui-select__done:hover {
286
+ background: var(--vmz-action-secondary-hover);
287
+ border-color: var(--vmz-action-primary-hover);
288
+ }
289
+
290
+ .vmz-ui-select__done:active {
291
+ background: var(--vmz-action-secondary-active);
292
+ }
293
+
294
+ .vmz-ui-select__done:focus-visible {
295
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
296
+ }
297
+
298
+ .vmz-ui-select__description {
299
+ margin: 0;
300
+ color: var(--vmz-text-steel);
301
+ font-size: 0.875rem;
302
+ line-height: 1.5715;
303
+ }
304
+
305
+ .vmz-ui-select__error {
306
+ margin: 0;
307
+ color: var(--vmz-status-danger-foreground);
308
+ font-size: 0.875rem;
309
+ line-height: 1.5715;
310
+ }
311
+ </style>
312
+ <script client>
313
+ /**
314
+ * VMZ UI — Select.
315
+ * Owned listbox (trigger + panel + role=option). Never native <select> popup.
316
+ * Parent owns value (single) or values (multiple) + options.
317
+ * mode=multiple keeps panel open until Done/Escape; searchable filters visible options.
318
+ */
319
+ export default class Select {
320
+ public label: string = "";
321
+ public description: string = "";
322
+ public error: string = "";
323
+ public placeholder: string = "";
324
+ /** single mode */
325
+ public value: string = "";
326
+ /** multiple mode — parent owns array */
327
+ public values: string[] = [];
328
+ public options: Array<{
329
+ value: string;
330
+ label: string;
331
+ }> = [];
332
+ /** "single" | "multiple" */
333
+ public mode: string = "single";
334
+ public searchable: boolean = false;
335
+ public searchPlaceholder: string = "";
336
+ public emptyText: string = "";
337
+ public doneLabel: string = "";
338
+ public disabled: boolean = false;
339
+ public invalidAttr: string = "false";
340
+ public controlId: string = "vmz-select";
341
+ public listId: string = "vmz-select-list";
342
+ public descriptionId: string = "vmz-select-desc";
343
+ public errorId: string = "vmz-select-err";
344
+ public describedBy: string = "";
345
+ public onChange:
346
+ | ((e: {
347
+ target: {
348
+ value: string;
349
+ values: string[];
350
+ };
351
+ }) => void)
352
+ | null = null;
353
+ open = false;
354
+ displayLabel = "";
355
+ searchQuery = "";
356
+ isMultiple = false;
357
+ searchableAttr = "false";
358
+ showEmpty = false;
359
+ visibleOptions: Array<{
360
+ value: string;
361
+ label: string;
362
+ selected: boolean;
363
+ }> = [];
364
+ _docKeyHandler = null;
365
+ _docPointerHandler = null;
366
+ _observer = null;
367
+ onMount() {
368
+ this.listId = `${this.controlId}-list`;
369
+ this._syncMode();
370
+ this._rebuildVisible();
371
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
372
+ const root = this.__vmzDomRoot;
373
+ if (!root || typeof root.querySelector !== "function") return;
374
+ this._observer = new MutationObserver(() => this._syncOverlay());
375
+ this._observer.observe(root, {
376
+ childList: true,
377
+ subtree: true,
378
+ });
379
+ }
380
+ onDestroy() {
381
+ if (this._observer) {
382
+ this._observer.disconnect();
383
+ this._observer = null;
384
+ }
385
+ this._teardownDoc();
386
+ }
387
+ _syncMode() {
388
+ this.isMultiple = String(this.mode || "single") === "multiple";
389
+ this.searchableAttr = this.searchable ? "true" : "false";
390
+ }
391
+ _selectedSet() {
392
+ if (this.isMultiple) {
393
+ return new Set((this.values || []).map((v) => String(v)));
394
+ }
395
+ return new Set(this.value ? [String(this.value)] : []);
396
+ }
397
+ _rebuildVisible() {
398
+ this._syncMode();
399
+ const q = String(this.searchQuery || "")
400
+ .trim()
401
+ .toLowerCase();
402
+ const selected = this._selectedSet();
403
+ const all = this.options || [];
404
+ const filtered = !q
405
+ ? all
406
+ : all.filter((o) => {
407
+ const label = String(o.label || "").toLowerCase();
408
+ const value = String(o.value || "").toLowerCase();
409
+ return label.includes(q) || value.includes(q);
410
+ });
411
+ this.visibleOptions = filtered.map((o) => ({
412
+ value: String(o.value),
413
+ label: String(o.label),
414
+ selected: selected.has(String(o.value)),
415
+ }));
416
+ this.showEmpty = this.visibleOptions.length === 0;
417
+ this._syncLabel();
418
+ }
419
+ _syncLabel() {
420
+ const opts = this.options || [];
421
+ if (this.isMultiple) {
422
+ const selected = this._selectedSet();
423
+ const labels = opts.filter((o) => selected.has(String(o.value))).map((o) => o.label);
424
+ if (!labels.length) this.displayLabel = this.placeholder || "";
425
+ else if (labels.length <= 2) this.displayLabel = labels.join(", ");
426
+ else this.displayLabel = `${labels[0]}, ${labels[1]} +${labels.length - 2}`;
427
+ return;
428
+ }
429
+ const hit = opts.find((o) => String(o.value) === String(this.value));
430
+ this.displayLabel = hit ? hit.label : this.value || this.placeholder || "";
431
+ }
432
+ searchLabel() {
433
+ return this.searchPlaceholder || this.label || "";
434
+ }
435
+ toggle() {
436
+ if (this.disabled) return;
437
+ if (this.open) this.close();
438
+ else this.openPanel();
439
+ }
440
+ openPanel() {
441
+ this.searchQuery = "";
442
+ this._rebuildVisible();
443
+ this.open = true;
444
+ }
445
+ close() {
446
+ this.open = false;
447
+ this.searchQuery = "";
448
+ this._rebuildVisible();
449
+ }
450
+ _emit(nextValue: string, nextValues: string[]) {
451
+ const payload = {
452
+ target: {
453
+ value: nextValue,
454
+ values: nextValues,
455
+ },
456
+ };
457
+ this.emit("change", payload);
458
+ if (typeof this.onChange === "function") {
459
+ this.onChange(payload);
460
+ }
461
+ }
462
+ /** Parent-owned prop writes (bindComponentProp) — keep derived label/options in sync. */
463
+ __vmzOnParentProp(name: string) {
464
+ if (name === "value" || name === "values" || name === "options" || name === "placeholder") {
465
+ this._rebuildVisible();
466
+ }
467
+ }
468
+ choose(next: string) {
469
+ if (!next && next !== "") return;
470
+ if (this.isMultiple) {
471
+ const cur = [...(this.values || [])].map(String);
472
+ const idx = cur.indexOf(String(next));
473
+ if (idx >= 0) cur.splice(idx, 1);
474
+ else cur.push(String(next));
475
+ this.values = cur;
476
+ this._rebuildVisible();
477
+ this._emit(cur.join(","), cur);
478
+ return;
479
+ }
480
+ const nextStr = String(next);
481
+ if (nextStr === String(this.value)) {
482
+ this.close();
483
+ return;
484
+ }
485
+ this.value = nextStr;
486
+ this._rebuildVisible();
487
+ this._emit(nextStr, nextStr ? [nextStr] : []);
488
+ this.close();
489
+ }
490
+ onSearchInput(ev: Event) {
491
+ const t = ev?.target as HTMLInputElement | null;
492
+ this.searchQuery = t ? String(t.value || "") : "";
493
+ this._rebuildVisible();
494
+ }
495
+ onSearchKeyDown(ev: KeyboardEvent) {
496
+ if (!ev) return;
497
+ if (ev.key === "Escape") {
498
+ ev.preventDefault();
499
+ this.close();
500
+ } else if (ev.key === "ArrowDown") {
501
+ ev.preventDefault();
502
+ const root = this.__vmzDomRoot;
503
+ const first =
504
+ root && typeof root.querySelector === "function"
505
+ ? root.querySelector('[data-vmz-overlay="select"] [role="option"]')
506
+ : null;
507
+ if (first instanceof HTMLElement) first.focus();
508
+ }
509
+ }
510
+ onOptionClick(ev: Event) {
511
+ const t = ev?.target as Element | null;
512
+ const el =
513
+ (t && typeof (t as HTMLElement).closest === "function"
514
+ ? ((t as HTMLElement).closest("[data-vmz-option]") as HTMLElement | null)
515
+ : null) || (ev?.currentTarget as HTMLElement | null);
516
+ const id =
517
+ el && typeof el.getAttribute === "function" ? el.getAttribute("data-vmz-option") : null;
518
+ if (id != null) this.choose(id);
519
+ }
520
+ onTriggerKeyDown(ev: KeyboardEvent) {
521
+ if (!ev || this.disabled) return;
522
+ if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
523
+ ev.preventDefault();
524
+ this.openPanel();
525
+ } else if (ev.key === "Escape" && this.open) {
526
+ ev.preventDefault();
527
+ this.close();
528
+ }
529
+ }
530
+ onListKeyDown(ev: KeyboardEvent) {
531
+ if (!ev) return;
532
+ if (ev.key === "Escape") {
533
+ ev.preventDefault();
534
+ this.close();
535
+ return;
536
+ }
537
+ const root = this.__vmzDomRoot;
538
+ const list =
539
+ root && typeof root.querySelector === "function"
540
+ ? root.querySelector(`[data-vmz-overlay="select"] [role="listbox"]`)
541
+ : null;
542
+ if (!list) return;
543
+ const nodes = [...list.querySelectorAll('[role="option"]')].filter(
544
+ (n) => n instanceof HTMLElement && n.offsetParent !== null,
545
+ );
546
+ if (!nodes.length) return;
547
+ const idx = nodes.indexOf(document.activeElement as Element);
548
+ if (ev.key === "ArrowDown") {
549
+ ev.preventDefault();
550
+ (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
551
+ } else if (ev.key === "ArrowUp") {
552
+ ev.preventDefault();
553
+ (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
554
+ } else if (ev.key === "Home") {
555
+ ev.preventDefault();
556
+ (nodes[0] as HTMLElement).focus();
557
+ } else if (ev.key === "End") {
558
+ ev.preventDefault();
559
+ (nodes[nodes.length - 1] as HTMLElement).focus();
560
+ } else if (ev.key === "Enter" || ev.key === " ") {
561
+ const active = document.activeElement as HTMLElement | null;
562
+ const id = active?.getAttribute?.("data-vmz-option");
563
+ if (id) {
564
+ ev.preventDefault();
565
+ this.choose(id);
566
+ }
567
+ }
568
+ }
569
+ _syncOverlay() {
570
+ const root = this.__vmzDomRoot;
571
+ const panel =
572
+ root && typeof root.querySelector === "function"
573
+ ? root.querySelector('[data-vmz-overlay="select"][data-vmz-focus="enter"]')
574
+ : null;
575
+ if (panel && this.open) this._bindDoc(panel as HTMLElement);
576
+ else if (!panel) this._teardownDoc();
577
+ this._rebuildVisible();
578
+ }
579
+ _bindDoc(panel: HTMLElement) {
580
+ if (typeof document === "undefined") return;
581
+ if (!this._docKeyHandler) {
582
+ this._docKeyHandler = (ev: KeyboardEvent) => {
583
+ if (ev.key === "Escape") {
584
+ ev.preventDefault();
585
+ this.close();
586
+ }
587
+ };
588
+ document.addEventListener("keydown", this._docKeyHandler);
589
+ }
590
+ if (!this._docPointerHandler) {
591
+ const root = this.__vmzDomRoot;
592
+ this._docPointerHandler = (ev: Event) => {
593
+ const t = ev.target;
594
+ if (!(t instanceof Node)) return;
595
+ if (root && typeof root.contains === "function" && root.contains(t)) return;
596
+ this.close();
597
+ };
598
+ document.addEventListener("pointerdown", this._docPointerHandler, true);
599
+ }
600
+ const search = panel.querySelector('[data-vmz-select="search"]');
601
+ if (search instanceof HTMLElement) {
602
+ search.focus();
603
+ return;
604
+ }
605
+ const first = panel.querySelector('[role="option"]');
606
+ if (first instanceof HTMLElement) first.focus();
607
+ else panel.focus();
608
+ }
609
+ _teardownDoc() {
610
+ if (typeof document !== "undefined") {
611
+ if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
612
+ if (this._docPointerHandler) {
613
+ document.removeEventListener("pointerdown", this._docPointerHandler, true);
614
+ }
615
+ }
616
+ this._docKeyHandler = null;
617
+ this._docPointerHandler = null;
618
+ }
619
+ }
620
+ </script>