@fusion.dev/helix-headless 0.143.0 → 0.144.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/custom-elements.json +8551 -375
- package/dist/area-chart/root/area-chart-root.d.ts +12 -4
- package/dist/area-chart/root/area-chart-root.js +24 -7
- package/dist/avatar/root/avatar-root.d.ts +14 -0
- package/dist/avatar/root/avatar-root.js +23 -9
- package/dist/badge/root/badge-root.d.ts +11 -7
- package/dist/badge/root/badge-root.js +1 -1
- package/dist/bar-chart/root/bar-chart-root.d.ts +12 -3
- package/dist/bar-chart/root/bar-chart-root.js +17 -10
- package/dist/button/root/button-root.d.ts +47 -1
- package/dist/button/root/button-root.js +38 -4
- package/dist/callout/root/callout-root.d.ts +18 -1
- package/dist/callout/root/callout-root.js +15 -4
- package/dist/card/root/card-root.d.ts +30 -1
- package/dist/card/root/card-root.js +38 -5
- package/dist/checkbox/root/checkbox-root.js +2 -0
- package/dist/checkbox-group/root/checkbox-group-root.d.ts +53 -3
- package/dist/checkbox-group/root/checkbox-group-root.js +71 -22
- package/dist/code-block/lines.d.ts +13 -0
- package/dist/code-block/lines.js +2 -0
- package/dist/code-block/root/code-block-root.d.ts +30 -0
- package/dist/code-block/root/code-block-root.js +23 -2
- package/dist/color-input/root/color-input-root.d.ts +34 -1
- package/dist/color-input/root/color-input-root.js +23 -4
- package/dist/color-picker/channels.d.ts +26 -0
- package/dist/color-picker/channels.js +84 -0
- package/dist/color-picker/root/color-picker-root.d.ts +81 -5
- package/dist/color-picker/root/color-picker-root.js +103 -10
- package/dist/combobox/root/combobox-root.d.ts +38 -3
- package/dist/combobox/root/combobox-root.js +64 -2
- package/dist/command/root/command-root.d.ts +30 -4
- package/dist/command/root/command-root.js +33 -3
- package/dist/copy-button/from.d.ts +22 -0
- package/dist/copy-button/from.js +28 -0
- package/dist/copy-button/root/copy-button-root.d.ts +27 -1
- package/dist/copy-button/root/copy-button-root.js +43 -7
- package/dist/date-input/root/date-input-root.d.ts +73 -4
- package/dist/date-input/root/date-input-root.js +93 -43
- package/dist/date-picker/root/date-picker-root.d.ts +145 -1
- package/dist/date-picker/root/date-picker-root.js +239 -48
- package/dist/dialog/description.d.ts +9 -0
- package/dist/dialog/description.js +14 -0
- package/dist/dialog/root/dialog-root.d.ts +64 -1
- package/dist/dialog/root/dialog-root.js +49 -23
- package/dist/donut-chart/root/donut-chart-root.d.ts +25 -4
- package/dist/donut-chart/root/donut-chart-root.js +21 -6
- package/dist/drawer/root/drawer-root.d.ts +63 -2
- package/dist/drawer/root/drawer-root.js +95 -19
- package/dist/drawer/swipe-start.d.ts +9 -0
- package/dist/drawer/swipe-start.js +12 -0
- package/dist/element-events.generated.js +8 -5
- package/dist/empty/root/empty-root.d.ts +1 -1
- package/dist/file-input/root/file-input-root.d.ts +47 -0
- package/dist/file-input/root/file-input-root.js +60 -22
- package/dist/file-input/screen.d.ts +35 -0
- package/dist/file-input/screen.js +49 -0
- package/dist/filter-input/root/filter-input-root.d.ts +38 -4
- package/dist/filter-input/root/filter-input-root.js +67 -3
- package/dist/filter-input/suggest.d.ts +17 -0
- package/dist/filter-input/suggest.js +23 -0
- package/dist/format-date/options.d.ts +33 -0
- package/dist/format-date/options.js +42 -0
- package/dist/format-date/root/format-date-root.d.ts +51 -0
- package/dist/format-date/root/format-date-root.js +58 -38
- package/dist/heatmap/root/heatmap-root.d.ts +23 -6
- package/dist/heatmap/root/heatmap-root.js +24 -13
- package/dist/icon/index.d.ts +2 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon/registry.d.ts +37 -0
- package/dist/icon/registry.js +83 -0
- package/dist/icon/root/icon-root.d.ts +30 -4
- package/dist/icon/root/icon-root.js +41 -6
- package/dist/icon/root/index.d.ts +2 -0
- package/dist/icon/root/index.js +2 -0
- package/dist/icon/set.d.ts +1 -0
- package/dist/icon/set.js +3 -0
- package/dist/image/root/image-root.d.ts +14 -0
- package/dist/image/root/image-root.js +15 -2
- package/dist/input/root/input-root.d.ts +11 -0
- package/dist/input/root/input-root.js +15 -0
- package/dist/kbd/root/kbd-root.d.ts +20 -13
- package/dist/kbd/root/kbd-root.js +35 -37
- package/dist/kbd/shortcut.d.ts +18 -0
- package/dist/kbd/shortcut.js +35 -0
- package/dist/legend/hidden-series.d.ts +8 -0
- package/dist/legend/hidden-series.js +13 -0
- package/dist/legend/root/legend-root.d.ts +56 -0
- package/dist/legend/root/legend-root.js +54 -5
- package/dist/lightbox/item/lightbox-item.d.ts +15 -0
- package/dist/lightbox/item/lightbox-item.js +10 -1
- package/dist/lightbox/root/lightbox-root.d.ts +61 -1
- package/dist/lightbox/root/lightbox-root.js +94 -4
- package/dist/line-chart/root/line-chart-root.d.ts +11 -3
- package/dist/line-chart/root/line-chart-root.js +19 -5
- package/dist/listbox/range.d.ts +17 -0
- package/dist/listbox/range.js +5 -0
- package/dist/listbox/root/listbox-root.d.ts +74 -3
- package/dist/listbox/root/listbox-root.js +87 -9
- package/dist/loader/busy.d.ts +17 -0
- package/dist/loader/busy.js +24 -0
- package/dist/loader/root/loader-root.d.ts +37 -0
- package/dist/loader/root/loader-root.js +52 -3
- package/dist/meter/root/meter-root.d.ts +7 -0
- package/dist/meter/root/meter-root.js +6 -3
- package/dist/notifications/feed-keys.d.ts +10 -0
- package/dist/notifications/feed-keys.js +14 -0
- package/dist/notifications/notification/notification.d.ts +17 -0
- package/dist/notifications/notification/notification.js +21 -2
- package/dist/notifications/root/notifications-root.d.ts +42 -0
- package/dist/notifications/root/notifications-root.js +60 -1
- package/dist/number-input/root/number-input-root.d.ts +47 -0
- package/dist/number-input/root/number-input-root.js +58 -22
- package/dist/object-inspector/root/object-inspector-root.d.ts +1 -0
- package/dist/object-inspector/root/object-inspector-root.js +1 -1
- package/dist/option/root/option-root.d.ts +54 -5
- package/dist/option/root/option-root.js +34 -5
- package/dist/pin-input/format.d.ts +7 -0
- package/dist/pin-input/format.js +16 -0
- package/dist/pin-input/root/pin-input-root.d.ts +21 -0
- package/dist/pin-input/root/pin-input-root.js +29 -2
- package/dist/popover/root/popover-root.d.ts +62 -4
- package/dist/popover/root/popover-root.js +53 -5
- package/dist/progress/root/progress-root.d.ts +7 -0
- package/dist/progress/root/progress-root.js +9 -3
- package/dist/qr-code/export.d.ts +31 -0
- package/dist/qr-code/export.js +10 -0
- package/dist/qr-code/root/qr-code-root.d.ts +26 -0
- package/dist/qr-code/root/qr-code-root.js +49 -2
- package/dist/radio/group/radio-group.d.ts +23 -0
- package/dist/radio/group/radio-group.js +18 -0
- package/dist/rating/root/rating-root.d.ts +25 -0
- package/dist/rating/root/rating-root.js +45 -3
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
- package/dist/scatter-chart/root/scatter-chart-root.js +25 -7
- package/dist/select/root/select-root.d.ts +24 -0
- package/dist/select/root/select-root.js +20 -6
- package/dist/skeleton/root/skeleton-root.d.ts +25 -0
- package/dist/skeleton/root/skeleton-root.js +17 -2
- package/dist/slider/root/slider-root.d.ts +39 -0
- package/dist/slider/root/slider-root.js +45 -9
- package/dist/stat/root/stat-root.d.ts +2 -0
- package/dist/stat/root/stat-root.js +1 -1
- package/dist/steps/step/step.d.ts +1 -0
- package/dist/steps/step/step.js +1 -1
- package/dist/switch/root/switch-root.d.ts +12 -2
- package/dist/switch/root/switch-root.js +6 -1
- package/dist/tag/group/tag-group.d.ts +107 -0
- package/dist/tag/group/tag-group.js +157 -0
- package/dist/tag/index.d.ts +1 -0
- package/dist/tag/index.js +1 -0
- package/dist/tag/root/tag-root.d.ts +60 -0
- package/dist/tag/root/tag-root.js +74 -18
- package/dist/tag/selection.d.ts +10 -0
- package/dist/tag/selection.js +12 -0
- package/dist/textarea/grow.d.ts +22 -0
- package/dist/textarea/grow.js +20 -0
- package/dist/textarea/root/textarea-root.d.ts +9 -0
- package/dist/textarea/root/textarea-root.js +9 -1
- package/dist/time-input/root/time-input-root.d.ts +44 -5
- package/dist/time-input/root/time-input-root.js +69 -39
- package/dist/time-picker/root/time-picker-root.d.ts +54 -8
- package/dist/time-picker/root/time-picker-root.js +121 -91
- package/dist/toast/hotkey.d.ts +22 -0
- package/dist/toast/hotkey.js +23 -0
- package/dist/toast/item/toast-item.d.ts +16 -1
- package/dist/toast/item/toast-item.js +41 -13
- package/dist/toast/root/toast-root.d.ts +42 -8
- package/dist/toast/root/toast-root.js +64 -4
- package/dist/toggle/group/toggle-group.d.ts +57 -15
- package/dist/toggle/group/toggle-group.js +69 -9
- package/dist/toggle/root/toggle-root.d.ts +12 -1
- package/dist/toggle/root/toggle-root.js +11 -3
- package/dist/tooltip/focus-target.d.ts +6 -0
- package/dist/tooltip/focus-target.js +9 -0
- package/dist/tooltip/root/tooltip-root.d.ts +42 -17
- package/dist/tooltip/root/tooltip-root.js +50 -9
- package/dist/tour/naming.d.ts +30 -0
- package/dist/tour/naming.js +10 -0
- package/dist/tour/root/tour-root.d.ts +35 -0
- package/dist/tour/root/tour-root.js +56 -5
- package/dist/tour/stop/tour-stop.d.ts +53 -8
- package/dist/tour/stop/tour-stop.js +45 -15
- package/dist/tour/wait.d.ts +11 -0
- package/dist/tour/wait.js +19 -0
- package/dist/trend/root/trend-root.d.ts +13 -0
- package/dist/trend/root/trend-root.js +14 -9
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +4862 -254
- package/types/svelte.d.ts +1 -0
- package/types/vue.d.ts +1149 -67
|
@@ -16,6 +16,12 @@ declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fus
|
|
|
16
16
|
* visible trigger is a `<label for>`. The label is not focusable. The input is,
|
|
17
17
|
* and Enter and Space on it open the picker.
|
|
18
18
|
*
|
|
19
|
+
* Files arrive from the picker, a drop, or a paste onto the field, and each one
|
|
20
|
+
* is screened: `accept`, `max-file-size`, `max-files` and your own `validate`
|
|
21
|
+
* can refuse it. A refused file is left out, `helix-reject` lists each one with
|
|
22
|
+
* its reason, and a reader hears `rejected-text` with the names filled in.
|
|
23
|
+
* `directory` asks the picker for a folder.
|
|
24
|
+
*
|
|
19
25
|
* @since 0.1.0
|
|
20
26
|
* @status stable
|
|
21
27
|
* @category forms
|
|
@@ -29,6 +35,7 @@ declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fus
|
|
|
29
35
|
*
|
|
30
36
|
* @event {Event} change - Fired after the value changes.
|
|
31
37
|
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before the chosen files change, with their names, and cancelable. Canceling it keeps the files the field had, even when the browser has already written a new pick.
|
|
38
|
+
* @event {HelixRejectEvent} helix-reject - Fired when files arrive and some are left out. `detail.files` lists each with its `reason`: `type`, `size`, `count` or `invalid`, and your `validate` message.
|
|
32
39
|
* @event {Event} input - Fired after the value changes.
|
|
33
40
|
*
|
|
34
41
|
* @csspart control - The file input itself, which carries the label and validity.
|
|
@@ -38,19 +45,24 @@ declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fus
|
|
|
38
45
|
* @csspart file - One chosen file.
|
|
39
46
|
* @csspart hint - Help under the field, while nothing is wrong.
|
|
40
47
|
* @csspart kind - A file's type.
|
|
48
|
+
* @csspart kind-cell - The grid cell around the tag, rendered only when the tag sits in a `Tag.Group`.
|
|
41
49
|
* @csspart kind-dismiss - The remove button, when `dismissible`.
|
|
42
50
|
* @csspart kind-dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
43
51
|
* @csspart kind-label - The label, trimmed to its capitals and baseline.
|
|
52
|
+
* @csspart kind-link - The link around the label, when the tag has an `href`.
|
|
44
53
|
* @csspart kind-start - The category before the label.
|
|
45
54
|
* @csspart label - The label above the field.
|
|
46
55
|
* @csspart list - The chosen files.
|
|
56
|
+
* @csspart live - The live region that says `rejected-text` when files are refused. It is rendered from the first refusal.
|
|
47
57
|
* @csspart meta - The row that holds a file's size and type.
|
|
48
58
|
* @csspart name - A file's name.
|
|
49
59
|
* @csspart remove - The control that removes a file.
|
|
50
60
|
* @csspart size - A file's size.
|
|
61
|
+
* @csspart size-cell - The grid cell around the tag, rendered only when the tag sits in a `Tag.Group`.
|
|
51
62
|
* @csspart size-dismiss - The remove button, when `dismissible`.
|
|
52
63
|
* @csspart size-dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
53
64
|
* @csspart size-label - The label, trimmed to its capitals and baseline.
|
|
65
|
+
* @csspart size-link - The link around the label, when the tag has an `href`.
|
|
54
66
|
* @csspart size-start - The category before the label.
|
|
55
67
|
* @csspart trigger - The label that opens the picker.
|
|
56
68
|
*
|
|
@@ -69,16 +81,35 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
|
|
|
69
81
|
* Which camera or microphone a mobile picker should offer, if any.
|
|
70
82
|
*/
|
|
71
83
|
capture?: 'environment' | 'user';
|
|
84
|
+
/**
|
|
85
|
+
* Whether the picker chooses a folder, and the field takes every file in
|
|
86
|
+
* it. The platform's `webkitdirectory`.
|
|
87
|
+
*/
|
|
88
|
+
directory: boolean;
|
|
72
89
|
/**
|
|
73
90
|
* Language tag the sizes are written in.
|
|
74
91
|
*
|
|
75
92
|
* Absent uses the runtime's own.
|
|
76
93
|
*/
|
|
77
94
|
locale: string;
|
|
95
|
+
/**
|
|
96
|
+
* The most files the field holds, under `multiple`. Files past it are
|
|
97
|
+
* refused.
|
|
98
|
+
*/
|
|
99
|
+
maxFiles?: number;
|
|
100
|
+
/**
|
|
101
|
+
* The most bytes one file may have. A larger one is refused.
|
|
102
|
+
*/
|
|
103
|
+
maxFileSize?: number;
|
|
78
104
|
/**
|
|
79
105
|
* Whether more than one file can be chosen.
|
|
80
106
|
*/
|
|
81
107
|
multiple: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* What a reader hears when files are refused, with `{count}` and `{names}`
|
|
110
|
+
* filled in.
|
|
111
|
+
*/
|
|
112
|
+
rejectedText: string;
|
|
82
113
|
/**
|
|
83
114
|
* The accessible name of the control that removes a file. `{name}` is
|
|
84
115
|
* replaced with the file's name.
|
|
@@ -92,6 +123,22 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
|
|
|
92
123
|
* The trigger's text when nothing is slotted into it.
|
|
93
124
|
*/
|
|
94
125
|
triggerLabel: string;
|
|
126
|
+
/**
|
|
127
|
+
* Your own check on each arriving file, which returns why it is refused, or
|
|
128
|
+
* nothing to take it. A property, since a function cannot be an attribute.
|
|
129
|
+
*/
|
|
130
|
+
validate?: (file: File) => string | null | undefined;
|
|
131
|
+
/** The live region, rendered from the first refusal. */
|
|
132
|
+
private readonly status;
|
|
133
|
+
/**
|
|
134
|
+
* Screens arriving files and commits what is taken, after the files the
|
|
135
|
+
* field keeps. Refused files are announced and said.
|
|
136
|
+
*/
|
|
137
|
+
private take;
|
|
138
|
+
/** Sends `helix-reject` and says which files were left out. */
|
|
139
|
+
private refuse;
|
|
140
|
+
/** Takes files pasted onto the field, as a drop takes them. */
|
|
141
|
+
private readonly handlePaste;
|
|
95
142
|
/**
|
|
96
143
|
* Whether something is being dragged over the field.
|
|
97
144
|
*
|
|
@@ -4,11 +4,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, customElement, drawIcon, fillTemplate, HelixBeforeChangeEvent, HelixFormElement, MixinField, requestChange, restoreValue, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, customElement, drawIcon, drawStatus, fillTemplate, formatNumbers, HelixBeforeChangeEvent, HelixFormElement, HelixRejectEvent, MixinField, requestChange, restoreValue, StatusController, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { trash } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
|
+
import { screenFiles } from '../screen.js';
|
|
12
13
|
import '../../tag/index.js';
|
|
13
14
|
const named = (label, name) => label.includes('{name}') ? fillTemplate(label, { name }) : `${label} ${name}`;
|
|
14
15
|
const size = (bytes, locale) => {
|
|
@@ -33,22 +34,6 @@ const kind = (file) => {
|
|
|
33
34
|
const subtype = file.type.split('/')[1]?.replace(/^x-/, '').split('+')[0] ?? '';
|
|
34
35
|
return /^[a-z0-9]{1,6}$/i.test(subtype) ? subtype.toUpperCase() : 'File';
|
|
35
36
|
};
|
|
36
|
-
const accepts = (file, accept) => {
|
|
37
|
-
const tokens = accept
|
|
38
|
-
.split(',')
|
|
39
|
-
.map((token) => token.trim().toLowerCase())
|
|
40
|
-
.filter(Boolean);
|
|
41
|
-
if (tokens.length === 0) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
const name = file.name.toLowerCase();
|
|
45
|
-
const type = file.type.toLowerCase();
|
|
46
|
-
return tokens.some((token) => token.startsWith('.')
|
|
47
|
-
? name.endsWith(token)
|
|
48
|
-
: token.endsWith('/*')
|
|
49
|
-
? type.startsWith(token.slice(0, -1))
|
|
50
|
-
: type === token);
|
|
51
|
-
};
|
|
52
37
|
let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(HelixFormElement) {
|
|
53
38
|
constructor() {
|
|
54
39
|
super(...arguments);
|
|
@@ -57,15 +42,26 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
57
42
|
target: () => this.partNamed('control'),
|
|
58
43
|
});
|
|
59
44
|
this.accept = '';
|
|
45
|
+
this.directory = false;
|
|
60
46
|
this.locale = '';
|
|
61
47
|
this.multiple = false;
|
|
48
|
+
this.rejectedText = 'Not added: {names}';
|
|
62
49
|
this.removeLabel = 'Remove {name}';
|
|
63
50
|
this.triggerLabel = 'Choose files';
|
|
51
|
+
this.status = new StatusController(this);
|
|
52
|
+
this.handlePaste = (event) => {
|
|
53
|
+
const pasted = [...(event.clipboardData?.files ?? [])];
|
|
54
|
+
if (this.isDisabled || pasted.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
this.take(pasted, this.multiple ? this.files : []);
|
|
59
|
+
};
|
|
64
60
|
this.dropping = false;
|
|
65
61
|
this.committed = [];
|
|
66
62
|
this.handleChange = (event) => {
|
|
67
63
|
event.stopPropagation();
|
|
68
|
-
this.
|
|
64
|
+
this.take(this.files, [], true);
|
|
69
65
|
};
|
|
70
66
|
this.handleDragOver = (event) => {
|
|
71
67
|
if (this.isDisabled) {
|
|
@@ -84,11 +80,11 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
84
80
|
return;
|
|
85
81
|
}
|
|
86
82
|
event.preventDefault();
|
|
87
|
-
const dropped = [...event.dataTransfer.files]
|
|
83
|
+
const dropped = [...event.dataTransfer.files];
|
|
88
84
|
if (dropped.length === 0) {
|
|
89
85
|
return;
|
|
90
86
|
}
|
|
91
|
-
this.
|
|
87
|
+
this.take(dropped, this.multiple ? this.files : []);
|
|
92
88
|
};
|
|
93
89
|
this.handleRemove = (event) => {
|
|
94
90
|
const at = Number(event.currentTarget.dataset.at);
|
|
@@ -102,6 +98,30 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
102
98
|
});
|
|
103
99
|
};
|
|
104
100
|
}
|
|
101
|
+
take(arriving, keeping, picked = false) {
|
|
102
|
+
const { kept, refused } = screenFiles(arriving, keeping, {
|
|
103
|
+
accept: this.accept,
|
|
104
|
+
maxFileSize: this.maxFileSize,
|
|
105
|
+
maxFiles: this.maxFiles,
|
|
106
|
+
multiple: this.multiple || picked,
|
|
107
|
+
validate: this.validate,
|
|
108
|
+
});
|
|
109
|
+
const same = (one, two) => one.length === two.length && one.every((file, at) => file === two[at]);
|
|
110
|
+
if (picked || !(same(kept, this.committed) && same(kept, this.files))) {
|
|
111
|
+
this.commit(kept);
|
|
112
|
+
}
|
|
113
|
+
if (refused.length > 0) {
|
|
114
|
+
this.refuse(refused);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
refuse(refused) {
|
|
118
|
+
this.dispatchEvent(new HelixRejectEvent({ files: refused }));
|
|
119
|
+
const words = fillTemplate(this.rejectedText, {
|
|
120
|
+
count: formatNumbers(this.lang)(refused.length),
|
|
121
|
+
names: new Intl.ListFormat(this.lang || undefined, { style: 'short', type: 'unit' }).format(refused.map((one) => one.file.name)),
|
|
122
|
+
});
|
|
123
|
+
this.status.say(words);
|
|
124
|
+
}
|
|
105
125
|
get files() {
|
|
106
126
|
return [...(this.control?.files ?? [])];
|
|
107
127
|
}
|
|
@@ -190,10 +210,10 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
190
210
|
}
|
|
191
211
|
renderMeta(file) {
|
|
192
212
|
return html `<${this.tag('tag')} part="size"
|
|
193
|
-
exportparts="dismiss: size-dismiss, dismiss-mark: size-dismiss-mark, label: size-label, start: size-start">${size(file.size, this.locale)}</${this.tag('tag')}>
|
|
213
|
+
exportparts="cell: size-cell, dismiss: size-dismiss, dismiss-mark: size-dismiss-mark, label: size-label, link: size-link, start: size-start">${size(file.size, this.locale)}</${this.tag('tag')}>
|
|
194
214
|
<${this.tag('tag')}
|
|
195
215
|
part="kind"
|
|
196
|
-
exportparts="dismiss: kind-dismiss, dismiss-mark: kind-dismiss-mark, label: kind-label, start: kind-start">${kind(file)}</${this.tag('tag')}>`;
|
|
216
|
+
exportparts="cell: kind-cell, dismiss: kind-dismiss, dismiss-mark: kind-dismiss-mark, label: kind-label, link: kind-link, start: kind-start">${kind(file)}</${this.tag('tag')}>`;
|
|
197
217
|
}
|
|
198
218
|
render() {
|
|
199
219
|
const files = this.files;
|
|
@@ -204,6 +224,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
204
224
|
@dragleave=${this.handleDragLeave}
|
|
205
225
|
@dragover=${this.handleDragOver}
|
|
206
226
|
@drop=${this.handleDrop}
|
|
227
|
+
@paste=${this.handlePaste}
|
|
207
228
|
>
|
|
208
229
|
<input
|
|
209
230
|
part="control"
|
|
@@ -217,6 +238,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
217
238
|
?disabled=${this.isDisabled}
|
|
218
239
|
?multiple=${this.multiple}
|
|
219
240
|
?required=${this.required}
|
|
241
|
+
?webkitdirectory=${this.directory}
|
|
220
242
|
@change=${this.handleChange}
|
|
221
243
|
@input=${this.stopInner}
|
|
222
244
|
/>
|
|
@@ -228,6 +250,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
228
250
|
one ordering decision this component makes differently from the way it
|
|
229
251
|
reads. Reasoning above renderMeta. -->
|
|
230
252
|
${this.renderHint()}${this.renderFeedback()}
|
|
253
|
+
${drawStatus(this.status)}
|
|
231
254
|
${files.length === 0
|
|
232
255
|
? nothing
|
|
233
256
|
: html `<ul part="list">
|
|
@@ -258,18 +281,33 @@ __decorate([
|
|
|
258
281
|
__decorate([
|
|
259
282
|
property()
|
|
260
283
|
], HelixHeadlessFileInput.prototype, "capture", void 0);
|
|
284
|
+
__decorate([
|
|
285
|
+
property({ type: Boolean })
|
|
286
|
+
], HelixHeadlessFileInput.prototype, "directory", void 0);
|
|
261
287
|
__decorate([
|
|
262
288
|
property()
|
|
263
289
|
], HelixHeadlessFileInput.prototype, "locale", void 0);
|
|
290
|
+
__decorate([
|
|
291
|
+
property({ attribute: 'max-files', type: Number })
|
|
292
|
+
], HelixHeadlessFileInput.prototype, "maxFiles", void 0);
|
|
293
|
+
__decorate([
|
|
294
|
+
property({ attribute: 'max-file-size', type: Number })
|
|
295
|
+
], HelixHeadlessFileInput.prototype, "maxFileSize", void 0);
|
|
264
296
|
__decorate([
|
|
265
297
|
property({ reflect: true, type: Boolean })
|
|
266
298
|
], HelixHeadlessFileInput.prototype, "multiple", void 0);
|
|
299
|
+
__decorate([
|
|
300
|
+
property({ attribute: 'rejected-text' })
|
|
301
|
+
], HelixHeadlessFileInput.prototype, "rejectedText", void 0);
|
|
267
302
|
__decorate([
|
|
268
303
|
property({ attribute: 'remove-label' })
|
|
269
304
|
], HelixHeadlessFileInput.prototype, "removeLabel", void 0);
|
|
270
305
|
__decorate([
|
|
271
306
|
property({ attribute: 'trigger-label' })
|
|
272
307
|
], HelixHeadlessFileInput.prototype, "triggerLabel", void 0);
|
|
308
|
+
__decorate([
|
|
309
|
+
property({ attribute: false })
|
|
310
|
+
], HelixHeadlessFileInput.prototype, "validate", void 0);
|
|
273
311
|
__decorate([
|
|
274
312
|
state()
|
|
275
313
|
], HelixHeadlessFileInput.prototype, "dropping", void 0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RefusedFile } from '@fusion.dev/helix-core';
|
|
2
|
+
/**
|
|
3
|
+
* Which of the files arriving at a file field it takes, and why it refuses the
|
|
4
|
+
* rest: the rules `accept`, `max-file-size`, `max-files` and `validate` set.
|
|
5
|
+
* Zag's file upload screens a drop the same way and reports each refused file
|
|
6
|
+
* with its reason.
|
|
7
|
+
*/
|
|
8
|
+
export type { Refusal, RefusedFile } from '@fusion.dev/helix-core';
|
|
9
|
+
/** The rules a field screens arriving files by. */
|
|
10
|
+
export interface ScreenRules {
|
|
11
|
+
/** A native `accept` list. Empty takes every type. */
|
|
12
|
+
readonly accept: string;
|
|
13
|
+
/** The most bytes one file may have. */
|
|
14
|
+
readonly maxFileSize?: number | undefined;
|
|
15
|
+
/** The most files the field holds, counting the ones it has. */
|
|
16
|
+
readonly maxFiles?: number | undefined;
|
|
17
|
+
/** Whether the field holds more than one file. */
|
|
18
|
+
readonly multiple: boolean;
|
|
19
|
+
/** Your own check, which returns why a file is refused, or nothing. */
|
|
20
|
+
readonly validate?: ((file: File) => string | null | undefined) | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a file is one `accept` lists: an extension, a type, or a family
|
|
24
|
+
* such as `image/*`, as the platform reads it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const accepts: (file: File, accept: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Screens arriving files against the rules, given the files the field keeps.
|
|
29
|
+
* `kept` is what the field holds afterward: the files it keeps and the
|
|
30
|
+
* arrivals it took, or for a single-file field the last arrival it took.
|
|
31
|
+
*/
|
|
32
|
+
export declare const screenFiles: (arriving: File[], keeping: File[], rules: ScreenRules) => {
|
|
33
|
+
kept: File[];
|
|
34
|
+
refused: RefusedFile[];
|
|
35
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { commaList } from '@fusion.dev/helix-core';
|
|
2
|
+
export const accepts = (file, accept) => {
|
|
3
|
+
const tokens = commaList(accept).map((token) => token.toLowerCase());
|
|
4
|
+
if (tokens.length === 0) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
const name = file.name.toLowerCase();
|
|
8
|
+
const type = file.type.toLowerCase();
|
|
9
|
+
return tokens.some((token) => token.startsWith('.')
|
|
10
|
+
? name.endsWith(token)
|
|
11
|
+
: token.endsWith('/*')
|
|
12
|
+
? type.startsWith(token.slice(0, -1))
|
|
13
|
+
: type === token);
|
|
14
|
+
};
|
|
15
|
+
const refusal = (file, rules) => {
|
|
16
|
+
if (!accepts(file, rules.accept)) {
|
|
17
|
+
return { file, reason: 'type' };
|
|
18
|
+
}
|
|
19
|
+
if (rules.maxFileSize !== undefined && file.size > rules.maxFileSize) {
|
|
20
|
+
return { file, reason: 'size' };
|
|
21
|
+
}
|
|
22
|
+
const message = rules.validate?.(file);
|
|
23
|
+
return message ? { file, message, reason: 'invalid' } : undefined;
|
|
24
|
+
};
|
|
25
|
+
export const screenFiles = (arriving, keeping, rules) => {
|
|
26
|
+
const refused = [];
|
|
27
|
+
const taken = [];
|
|
28
|
+
for (const file of arriving) {
|
|
29
|
+
const refusedFor = refusal(file, rules);
|
|
30
|
+
if (refusedFor) {
|
|
31
|
+
refused.push(refusedFor);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
taken.push(file);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!rules.multiple) {
|
|
38
|
+
const last = taken.at(-1);
|
|
39
|
+
for (const file of taken.slice(0, -1)) {
|
|
40
|
+
refused.push({ file, reason: 'count' });
|
|
41
|
+
}
|
|
42
|
+
return { kept: last ? [last] : keeping.slice(0, 1), refused };
|
|
43
|
+
}
|
|
44
|
+
const room = rules.maxFiles === undefined ? taken.length : Math.max(0, rules.maxFiles - keeping.length);
|
|
45
|
+
for (const file of taken.slice(room)) {
|
|
46
|
+
refused.push({ file, reason: 'count' });
|
|
47
|
+
}
|
|
48
|
+
return { kept: [...keeping, ...taken.slice(0, room)], refused };
|
|
49
|
+
};
|
|
@@ -13,8 +13,10 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
13
13
|
* its tokens show a selection it owns, drawn from the options it was given.
|
|
14
14
|
*
|
|
15
15
|
* `value` is the typed text. Every keystroke fires `input` with the text on the
|
|
16
|
-
* element, so a table can narrow as the user types.
|
|
17
|
-
*
|
|
16
|
+
* element, so a table can narrow as the user types. A form receives the text
|
|
17
|
+
* under `name`, and then one more entry under `name` for each token: its
|
|
18
|
+
* `value` attribute, or for a `FilterChip`, its field and values written
|
|
19
|
+
* `field:values` as the field reads them typed. Backspace in an empty field fires
|
|
18
20
|
* `helix-dismiss` on the last token, the same event its remove control fires,
|
|
19
21
|
* so one handler covers the pointer and the keyboard.
|
|
20
22
|
*
|
|
@@ -38,8 +40,11 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
38
40
|
* moves through them with the arrow keys, and presses one on Enter. The option
|
|
39
41
|
* fires its own `helix-select`, and you decide what choosing it means. Keys
|
|
40
42
|
* before a colon and values after it use the same panel with different
|
|
41
|
-
* contents. `
|
|
42
|
-
*
|
|
43
|
+
* contents. `filter-suggestions` narrows the panel to the options whose words
|
|
44
|
+
* hold the term at the caret, after its colon, ignoring case and accents, and
|
|
45
|
+
* hides the rest. `readFilterText` is exported so you can narrow it yourself
|
|
46
|
+
* against partly typed text. While the panel is open, a reader hears how many
|
|
47
|
+
* suggestions it offers. The panel opens when it has options and closes when it has
|
|
43
48
|
* none, and a choice does not close it, so after picking a key you see its
|
|
44
49
|
* values next. Use `selectionStart` and `setSelectionRange` to write a
|
|
45
50
|
* completion back and put the caret after it, or the next parse reads the wrong
|
|
@@ -93,6 +98,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
93
98
|
* @csspart hint - The help text under the field, shown while there is no error.
|
|
94
99
|
* @csspart label - The label above the field.
|
|
95
100
|
* @csspart lead - The box before the text that holds the `start` slot.
|
|
101
|
+
* @csspart live - The live region that says how many suggestions the open panel offers. It is rendered from the first time the panel opens with suggestions.
|
|
96
102
|
* @csspart rest - The count of tokens that did not fit. It is a `<button>` with `expandable` and a `<span>` without it.
|
|
97
103
|
* @csspart surface - The floating panel the suggestions sit in.
|
|
98
104
|
* @csspart tags - The row the tokens sit in.
|
|
@@ -118,12 +124,40 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
118
124
|
* an icon.
|
|
119
125
|
*/
|
|
120
126
|
clearLabel: string;
|
|
127
|
+
/**
|
|
128
|
+
* Whether the panel narrows itself to the suggestions that hold the term
|
|
129
|
+
* being typed, as React Aria's Autocomplete filters. Off, the panel shows
|
|
130
|
+
* what you slot and you narrow it.
|
|
131
|
+
*/
|
|
132
|
+
filterSuggestions: boolean;
|
|
121
133
|
/**
|
|
122
134
|
* The text shown while the field is empty.
|
|
123
135
|
*
|
|
124
136
|
* Not a label: it goes when typing starts.
|
|
125
137
|
*/
|
|
126
138
|
placeholder: string;
|
|
139
|
+
/** What a reader hears for one suggestion, with `{count}` filled in. */
|
|
140
|
+
suggestionLabel: string;
|
|
141
|
+
/** What a reader hears for any other number of suggestions, with `{count}` filled in. */
|
|
142
|
+
suggestionsLabel: string;
|
|
143
|
+
/** The live region, rendered from the first count said. */
|
|
144
|
+
private readonly status;
|
|
145
|
+
/** The count last said, so the same panel is said once. */
|
|
146
|
+
private saidCount;
|
|
147
|
+
/** The options this element hid while narrowing, so only those are shown again. */
|
|
148
|
+
private readonly narrowed;
|
|
149
|
+
/**
|
|
150
|
+
* Hides the slotted options whose words do not hold the term at the caret,
|
|
151
|
+
* under `filter-suggestions`, and shows again the ones it hid before.
|
|
152
|
+
*/
|
|
153
|
+
private narrow;
|
|
154
|
+
/** Says how many suggestions the open panel offers, when that changes. */
|
|
155
|
+
private sayCount;
|
|
156
|
+
/**
|
|
157
|
+
* What the form receives: the text, then one entry for each token that
|
|
158
|
+
* says what it filters. See the class description.
|
|
159
|
+
*/
|
|
160
|
+
private formValue;
|
|
127
161
|
/** How many tokens did not fit, which is what the row's count renders. */
|
|
128
162
|
private over;
|
|
129
163
|
/**
|
|
@@ -4,13 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, customElement, drawIcon, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, scrollItemIntoView, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, countLabel, customElement, drawIcon, drawStatus, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, StatusController, scrollItemIntoView, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { x } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { HelixHeadlessFilterField } from '../field/filter-field.js';
|
|
12
12
|
import { readFilterText } from '../grammar.js';
|
|
13
13
|
import { HelixHeadlessFilterMenu } from '../menu/filter-menu.js';
|
|
14
|
+
import { currentTerm, suggests, tokenEntry } from '../suggest.js';
|
|
14
15
|
const asksToGo = (node) => typeof node?.dismiss === 'function';
|
|
15
16
|
const isOption = (node) => 'active' in node && 'value' in node;
|
|
16
17
|
let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
|
|
@@ -22,7 +23,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
22
23
|
this.announcesInput = true;
|
|
23
24
|
this.clearable = false;
|
|
24
25
|
this.clearLabel = 'Clear';
|
|
26
|
+
this.filterSuggestions = false;
|
|
25
27
|
this.placeholder = '';
|
|
28
|
+
this.suggestionLabel = '{count} suggestion';
|
|
29
|
+
this.suggestionsLabel = '{count} suggestions';
|
|
30
|
+
this.status = new StatusController(this);
|
|
31
|
+
this.saidCount = -1;
|
|
32
|
+
this.narrowed = new Set();
|
|
26
33
|
this.over = 0;
|
|
27
34
|
this.activeAt = -1;
|
|
28
35
|
this.suggesting = false;
|
|
@@ -154,6 +161,51 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
154
161
|
this.over = this.hideOverflow(this.tokens);
|
|
155
162
|
};
|
|
156
163
|
}
|
|
164
|
+
narrow() {
|
|
165
|
+
const slot = this.renderRoot?.querySelector('slot[name="suggestions"]');
|
|
166
|
+
const term = this.filterSuggestions
|
|
167
|
+
? currentTerm(this.value, this.control?.selectionStart ?? this.value.length)
|
|
168
|
+
: '';
|
|
169
|
+
for (const node of slot?.assignedElements({ flatten: true }) ?? []) {
|
|
170
|
+
const options = isOption(node) ? [node] : [...node.children].filter(isOption);
|
|
171
|
+
for (const option of options) {
|
|
172
|
+
const hide = !suggests(option.textContent ?? '', term);
|
|
173
|
+
if (hide && !option.hidden) {
|
|
174
|
+
option.hidden = true;
|
|
175
|
+
this.narrowed.add(option);
|
|
176
|
+
}
|
|
177
|
+
else if (!hide && this.narrowed.has(option)) {
|
|
178
|
+
option.hidden = false;
|
|
179
|
+
this.narrowed.delete(option);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
sayCount() {
|
|
185
|
+
const count = this.open && this.suggesting ? this.suggestions.length : -1;
|
|
186
|
+
if (count === this.saidCount) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.saidCount = count;
|
|
190
|
+
if (count < 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.status.sayNow(countLabel(count, this.suggestionLabel, this.suggestionsLabel, this.lang));
|
|
194
|
+
}
|
|
195
|
+
formValue() {
|
|
196
|
+
const entries = (this.tokens?.assignedElements({ flatten: true }) ?? [])
|
|
197
|
+
.map(tokenEntry)
|
|
198
|
+
.filter((entry) => entry !== null);
|
|
199
|
+
if (this.name === '' || entries.length === 0) {
|
|
200
|
+
return this.value;
|
|
201
|
+
}
|
|
202
|
+
const data = new FormData();
|
|
203
|
+
data.append(this.name, this.value);
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
data.append(this.name, entry);
|
|
206
|
+
}
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
157
209
|
static { this.shadowRootOptions = {
|
|
158
210
|
...HelixFormElement.shadowRootOptions,
|
|
159
211
|
delegatesFocus: true,
|
|
@@ -181,7 +233,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
181
233
|
}
|
|
182
234
|
}
|
|
183
235
|
}
|
|
184
|
-
return found.filter((option) => !option.disabled);
|
|
236
|
+
return found.filter((option) => !option.disabled && !option.hidden);
|
|
185
237
|
}
|
|
186
238
|
get selectionDirection() {
|
|
187
239
|
return this.control?.selectionDirection ?? 'none';
|
|
@@ -264,6 +316,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
264
316
|
from < 0 ? (by > 0 ? 0 : options.length - 1) : (from + by + options.length) % options.length;
|
|
265
317
|
}
|
|
266
318
|
refreshSuggesting() {
|
|
319
|
+
this.narrow();
|
|
267
320
|
const options = this.suggestions;
|
|
268
321
|
this.suggesting = options.length > 0;
|
|
269
322
|
this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
|
|
@@ -311,7 +364,8 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
311
364
|
super.updated(changed);
|
|
312
365
|
this.releaseCaret?.();
|
|
313
366
|
this.releaseCaret = undefined;
|
|
314
|
-
this.internals.setFormValue(this.
|
|
367
|
+
this.internals.setFormValue(this.formValue());
|
|
368
|
+
this.sayCount();
|
|
315
369
|
const control = this.control;
|
|
316
370
|
if (control) {
|
|
317
371
|
this.copyValidity(control);
|
|
@@ -391,6 +445,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
391
445
|
>
|
|
392
446
|
<slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
|
|
393
447
|
</div>
|
|
448
|
+
${drawStatus(this.status)}
|
|
394
449
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
395
450
|
}
|
|
396
451
|
};
|
|
@@ -400,9 +455,18 @@ __decorate([
|
|
|
400
455
|
__decorate([
|
|
401
456
|
property({ attribute: 'clear-label' })
|
|
402
457
|
], HelixHeadlessFilterInput.prototype, "clearLabel", void 0);
|
|
458
|
+
__decorate([
|
|
459
|
+
property({ attribute: 'filter-suggestions', type: Boolean })
|
|
460
|
+
], HelixHeadlessFilterInput.prototype, "filterSuggestions", void 0);
|
|
403
461
|
__decorate([
|
|
404
462
|
property()
|
|
405
463
|
], HelixHeadlessFilterInput.prototype, "placeholder", void 0);
|
|
464
|
+
__decorate([
|
|
465
|
+
property({ attribute: 'suggestion-label' })
|
|
466
|
+
], HelixHeadlessFilterInput.prototype, "suggestionLabel", void 0);
|
|
467
|
+
__decorate([
|
|
468
|
+
property({ attribute: 'suggestions-label' })
|
|
469
|
+
], HelixHeadlessFilterInput.prototype, "suggestionsLabel", void 0);
|
|
406
470
|
__decorate([
|
|
407
471
|
state()
|
|
408
472
|
], HelixHeadlessFilterInput.prototype, "over", void 0);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a filter field narrows its suggestions by, and what it sends a form,
|
|
3
|
+
* kept apart from the element so each can be read and tested alone.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The term being typed at the caret: the run of text since the last space,
|
|
7
|
+
* and after its colon where it has one, so `status:ope` narrows by `ope`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const currentTerm: (text: string, caret?: number) => string;
|
|
10
|
+
/** Whether a suggestion's words hold the term, ignoring case and accents. */
|
|
11
|
+
export declare const suggests: (words: string, term: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* What a token sends a form: its `value` attribute, or for a token naming a
|
|
14
|
+
* field, the field and the values it shows, written `field:values` the way the
|
|
15
|
+
* field reads them typed. Nothing for a token that says neither.
|
|
16
|
+
*/
|
|
17
|
+
export declare const tokenEntry: (token: Element) => string | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const currentTerm = (text, caret = text.length) => {
|
|
2
|
+
const before = text.slice(0, caret);
|
|
3
|
+
const word = before.slice(before.search(/\S*$/u));
|
|
4
|
+
const colon = word.indexOf(':');
|
|
5
|
+
return (colon === -1 ? word : word.slice(colon + 1)).replace(/^-/u, '');
|
|
6
|
+
};
|
|
7
|
+
export const suggests = (words, term) => {
|
|
8
|
+
const plain = (text) => text.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
|
|
9
|
+
return term === '' || plain(words).includes(plain(term));
|
|
10
|
+
};
|
|
11
|
+
export const tokenEntry = (token) => {
|
|
12
|
+
const written = token.getAttribute('value');
|
|
13
|
+
if (written !== null) {
|
|
14
|
+
return written;
|
|
15
|
+
}
|
|
16
|
+
const field = token.getAttribute('name');
|
|
17
|
+
const values = (token.textContent ?? '').replace(/\s+/gu, ' ').trim();
|
|
18
|
+
if (!field || values === '') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const negated = token.getAttribute('operator')?.startsWith('not') ? '-' : '';
|
|
22
|
+
return `${negated}${field}:${values.replace(/\s*,\s*/gu, ',')}`;
|
|
23
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Format Date was asked for, in its own spelling: the `Intl` options a
|
|
3
|
+
* consumer set, and whether the timestamp is a day with no time of day.
|
|
4
|
+
*/
|
|
5
|
+
export interface Asked {
|
|
6
|
+
dateOnly: boolean;
|
|
7
|
+
dateStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
|
|
8
|
+
day?: 'numeric' | '2-digit' | undefined;
|
|
9
|
+
era?: 'long' | 'narrow' | 'short' | undefined;
|
|
10
|
+
hour?: 'numeric' | '2-digit' | undefined;
|
|
11
|
+
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24' | undefined;
|
|
12
|
+
minute?: 'numeric' | '2-digit' | undefined;
|
|
13
|
+
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' | undefined;
|
|
14
|
+
second?: 'numeric' | '2-digit' | undefined;
|
|
15
|
+
timeStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
|
|
16
|
+
timeZone?: string | undefined;
|
|
17
|
+
timeZoneName?: 'long' | 'short' | undefined;
|
|
18
|
+
weekday?: 'long' | 'short' | 'narrow' | undefined;
|
|
19
|
+
year?: 'numeric' | '2-digit' | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The `Intl.DateTimeFormat` options for what was asked, and none that was not.
|
|
23
|
+
*
|
|
24
|
+
* **A style wins over the fields.** `dateStyle` and `timeStyle` are each
|
|
25
|
+
* locale's own whole pattern, and `Intl` throws a `TypeError` when one is
|
|
26
|
+
* given beside `month` or `hour`, so the fields are left out wherever a style
|
|
27
|
+
* is set. `hourCycle` and `timeZone` go with either.
|
|
28
|
+
*
|
|
29
|
+
* **A day with no time is read in UTC** unless a zone is given. Parsed, it is
|
|
30
|
+
* midnight UTC, and read in a zone west of it that is the day before: measured
|
|
31
|
+
* before this, `2026-08-04` read "August 3, 2026" in Los Angeles.
|
|
32
|
+
*/
|
|
33
|
+
export declare const formatOptions: (asked: Asked) => Intl.DateTimeFormatOptions;
|