@fusion.dev/helix-headless 0.23.0 → 0.24.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 +48376 -26638
- package/dist/announcement/root/announcement-root.d.ts +19 -5
- package/dist/area-chart/index.d.ts +1 -0
- package/dist/area-chart/index.js +1 -0
- package/dist/area-chart/root/area-chart-root.d.ts +182 -0
- package/dist/area-chart/root/area-chart-root.js +205 -0
- package/dist/carousel/root/carousel-root.d.ts +1 -0
- package/dist/carousel/root/carousel-root.js +18 -2
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/message/chat-message.d.ts +7 -0
- package/dist/chat/message/chat-message.js +2 -1
- package/dist/chat/root/chat-root.d.ts +195 -4
- package/dist/chat/root/chat-root.js +316 -23
- package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
- package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
- package/dist/code-block/index.d.ts +1 -0
- package/dist/code-block/index.js +1 -0
- package/dist/code-block/patch.d.ts +62 -0
- package/dist/code-block/patch.js +93 -0
- package/dist/code-block/root/code-block-root.d.ts +82 -17
- package/dist/code-block/root/code-block-root.js +83 -6
- package/dist/color-picker/root/color-picker-root.d.ts +4 -3
- package/dist/color-picker/root/color-picker-root.js +4 -1
- package/dist/data-table/root/data-table-root.d.ts +195 -23
- package/dist/data-table/root/data-table-root.js +308 -27
- package/dist/description-list/index.d.ts +2 -0
- package/dist/description-list/index.js +2 -0
- package/dist/description-list/item/description-list-item.d.ts +39 -0
- package/dist/description-list/item/description-list-item.js +26 -0
- package/dist/description-list/root/description-list-root.d.ts +56 -0
- package/dist/description-list/root/description-list-root.js +21 -0
- package/dist/element-events.generated.js +27 -6
- package/dist/filter-input/chip/filter-chip.d.ts +114 -0
- package/dist/filter-input/chip/filter-chip.js +61 -0
- package/dist/filter-input/field/filter-field.d.ts +128 -0
- package/dist/filter-input/field/filter-field.js +70 -0
- package/dist/filter-input/grammar.d.ts +96 -0
- package/dist/filter-input/grammar.js +133 -0
- package/dist/filter-input/index.d.ts +5 -0
- package/dist/filter-input/index.js +5 -0
- package/dist/filter-input/menu/filter-menu.d.ts +153 -0
- package/dist/filter-input/menu/filter-menu.js +164 -0
- package/dist/filter-input/operator/filter-operator.d.ts +105 -0
- package/dist/filter-input/operator/filter-operator.js +39 -0
- package/dist/filter-input/root/filter-input-root.d.ts +118 -9
- package/dist/filter-input/root/filter-input-root.js +96 -2
- package/dist/format-date/root/format-date-root.js +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/line-chart/root/line-chart-root.d.ts +9 -2
- package/dist/listbox/root/listbox-root.d.ts +8 -7
- package/dist/listbox/root/listbox-root.js +7 -2
- package/dist/menu/item/menu-item.d.ts +31 -6
- package/dist/menu/item/menu-item.js +18 -5
- package/dist/menu/root/menu-root.js +7 -2
- package/dist/notifications/group/notification-group.d.ts +129 -0
- package/dist/notifications/group/notification-group.js +106 -0
- package/dist/notifications/index.d.ts +1 -0
- package/dist/notifications/index.js +1 -0
- package/dist/notifications/notification/notification.d.ts +39 -13
- package/dist/notifications/notification/notification.js +11 -8
- package/dist/notifications/root/notifications-root.d.ts +57 -1
- package/dist/notifications/root/notifications-root.js +35 -4
- package/dist/number-input/root/number-input-root.d.ts +21 -1
- package/dist/number-input/root/number-input-root.js +18 -5
- package/dist/object-inspector/index.d.ts +1 -0
- package/dist/object-inspector/index.js +1 -0
- package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
- package/dist/object-inspector/root/object-inspector-root.js +348 -0
- package/dist/outline/root/outline-root.d.ts +16 -1
- package/dist/outline/root/outline-root.js +27 -9
- package/dist/radio/group/radio-group.d.ts +1 -0
- package/dist/scatter-chart/index.d.ts +1 -0
- package/dist/scatter-chart/index.js +1 -0
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
- package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
- package/dist/skip-link/index.d.ts +1 -0
- package/dist/skip-link/index.js +1 -0
- package/dist/skip-link/root/skip-link-root.d.ts +81 -0
- package/dist/skip-link/root/skip-link-root.js +58 -0
- package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
- package/dist/speed-dial/root/speed-dial-root.js +6 -2
- package/dist/steps/root/steps-root.d.ts +1 -0
- package/dist/table/column/table-column.d.ts +79 -0
- package/dist/table/column/table-column.js +125 -5
- package/dist/table/root/table-root.d.ts +2 -0
- package/dist/table/root/table-root.js +12 -3
- package/dist/time-picker/root/time-picker-root.d.ts +9 -0
- package/dist/time-picker/root/time-picker-root.js +5 -0
- package/dist/toast/root/toast-root.d.ts +2 -0
- package/dist/tooltip/root/tooltip-root.js +1 -1
- package/dist/tour/root/tour-root.d.ts +1 -0
- package/dist/tree/item/tree-item.d.ts +7 -4
- package/dist/tree/item/tree-item.js +4 -1
- package/dist/video/root/video-root.d.ts +23 -1
- package/dist/video/root/video-root.js +42 -22
- package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
- package/dist/virtual-list/root/virtual-list-root.js +10 -4
- package/package.json +5 -5
- package/types/jsx.d.ts +4535 -346
- package/types/svelte.d.ts +11 -0
- package/types/vue.d.ts +737 -39
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export const DEFAULT_FILTER_OPERATORS = [
|
|
2
|
+
{ arity: 'many', default: true, label: 'is any of', name: 'any' },
|
|
3
|
+
{ arity: 'many', default: false, label: 'is none of', name: 'none' },
|
|
4
|
+
];
|
|
5
|
+
const NEGATION = 'none';
|
|
6
|
+
const same = (written, declared) => declared !== '' && written.toLowerCase() === declared.trim().toLowerCase();
|
|
7
|
+
const unquote = (piece) => piece.length > 1 && piece.startsWith('"') && piece.endsWith('"') ? piece.slice(1, -1) : piece;
|
|
8
|
+
function split(text, separator) {
|
|
9
|
+
const parts = [];
|
|
10
|
+
let current = '';
|
|
11
|
+
let quoted = false;
|
|
12
|
+
for (const char of text) {
|
|
13
|
+
if (char === '"') {
|
|
14
|
+
quoted = !quoted;
|
|
15
|
+
current += char;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!quoted && (separator === ' ' ? /\s/.test(char) : char === separator)) {
|
|
19
|
+
if (current !== '') {
|
|
20
|
+
parts.push(current);
|
|
21
|
+
}
|
|
22
|
+
current = '';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
current += char;
|
|
26
|
+
}
|
|
27
|
+
if (current !== '') {
|
|
28
|
+
parts.push(current);
|
|
29
|
+
}
|
|
30
|
+
return parts;
|
|
31
|
+
}
|
|
32
|
+
function atColon(term) {
|
|
33
|
+
let quoted = false;
|
|
34
|
+
for (let at = 0; at < term.length; at += 1) {
|
|
35
|
+
const char = term[at];
|
|
36
|
+
if (char === '"') {
|
|
37
|
+
quoted = !quoted;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!quoted && char === ':') {
|
|
41
|
+
return [term.slice(0, at), term.slice(at + 1)];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function readPiece(written, fields) {
|
|
47
|
+
const negated = written.startsWith('-');
|
|
48
|
+
const parts = atColon(negated ? written.slice(1) : written);
|
|
49
|
+
if (!parts) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const key = unquote(parts[0]);
|
|
53
|
+
const field = fields.find((one) => same(key, one.name) || same(key, one.label));
|
|
54
|
+
if (!field) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const operator = negated
|
|
58
|
+
? field.operators.find((one) => one.name === NEGATION)
|
|
59
|
+
: (field.operators.find((one) => one.default) ?? field.operators[0]);
|
|
60
|
+
if (!operator || operator.arity === 'none') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const values = [];
|
|
64
|
+
for (const raw of split(parts[1], ',').map(unquote)) {
|
|
65
|
+
if (raw === '') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (field.values.length === 0) {
|
|
69
|
+
values.push(raw);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const found = field.values.find((one) => same(raw, one.value) || same(raw, one.label));
|
|
73
|
+
if (!found) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
values.push(found.value);
|
|
77
|
+
}
|
|
78
|
+
if (values.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (operator.arity === 'one' && values.length > 1) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return { field, operator, values };
|
|
85
|
+
}
|
|
86
|
+
export function readFilterText(text, fields) {
|
|
87
|
+
const order = [];
|
|
88
|
+
const held = new Map();
|
|
89
|
+
const spare = [];
|
|
90
|
+
for (const written of split(text, ' ')) {
|
|
91
|
+
const read = readPiece(written, fields);
|
|
92
|
+
if (!read) {
|
|
93
|
+
spare.push(written);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const standing = held.get(read.field.name);
|
|
97
|
+
if (standing && standing.operator.name !== read.operator.name) {
|
|
98
|
+
spare.push(written);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (standing) {
|
|
102
|
+
for (const value of read.values) {
|
|
103
|
+
if (!standing.values.includes(value)) {
|
|
104
|
+
standing.values.push(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
standing.written.push(written);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const term = {
|
|
111
|
+
field: read.field,
|
|
112
|
+
operator: read.operator,
|
|
113
|
+
values: [...new Set(read.values)],
|
|
114
|
+
written: [written],
|
|
115
|
+
};
|
|
116
|
+
order.push(term);
|
|
117
|
+
held.set(read.field.name, term);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
terms: order.map((term) => ({
|
|
121
|
+
facet: {
|
|
122
|
+
arity: term.operator.arity,
|
|
123
|
+
field: term.field.name,
|
|
124
|
+
label: term.field.label,
|
|
125
|
+
operator: term.operator.name,
|
|
126
|
+
operatorLabel: term.operator.default ? '' : term.operator.label,
|
|
127
|
+
values: term.values,
|
|
128
|
+
},
|
|
129
|
+
written: term.written,
|
|
130
|
+
})),
|
|
131
|
+
text: spare.join(' '),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
+
import type { HelixHeadlessFilterField } from '../field/filter-field.js';
|
|
4
|
+
import type { FilterGrammarOperator } from '../grammar.js';
|
|
5
|
+
import '../../divider/index.js';
|
|
6
|
+
import '../../menu/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* @summary The control that builds a filter: pick a field, tick its values.
|
|
9
|
+
*
|
|
10
|
+
* @description
|
|
11
|
+
* **It reads the declarations and renders a menu.** A consumer writes their
|
|
12
|
+
* fields as `FilterField`s inside this, and what comes out is a two level menu:
|
|
13
|
+
* the fields, and under each one its comparisons and its values. Nothing here
|
|
14
|
+
* knows what a field means, which is the line `FilterField` already draws.
|
|
15
|
+
*
|
|
16
|
+
* ```html
|
|
17
|
+
* <helix-headless-filter-menu label="Filter">
|
|
18
|
+
* <helix-headless-filter-field name="disposition" label="Disposition">
|
|
19
|
+
* <helix-headless-option value="refunded">Refunded</helix-headless-option>
|
|
20
|
+
* </helix-headless-filter-field>
|
|
21
|
+
* </helix-headless-filter-menu>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* That field declares no comparisons and so is offered `is any of` and `is none
|
|
25
|
+
* of`, which are the two the typed grammar can say. The menu and the text end up
|
|
26
|
+
* offering the same filters because both read `FilterField.vocabulary`, and a
|
|
27
|
+
* consumer whose field needs other words declares them there.
|
|
28
|
+
*
|
|
29
|
+
* **A tick is a filter, immediately.** There is no Apply: ticking a value
|
|
30
|
+
* announces the field's whole set as it now stands, so a queue narrows as the
|
|
31
|
+
* reader works rather than after they have finished. Unticking the last value
|
|
32
|
+
* announces an empty set, which is a consumer's cue to drop the chip.
|
|
33
|
+
*
|
|
34
|
+
* **The operator is chosen here and written nowhere else.** `helix-facet` carries
|
|
35
|
+
* `operatorLabel` empty when the comparison is the field's default, which is
|
|
36
|
+
* exactly what a chip's `operator-label` wants: the segment appears only where
|
|
37
|
+
* somebody picked something worth noticing. Deciding it here is the point, since
|
|
38
|
+
* this is the only thing that has read both the field and its operators.
|
|
39
|
+
*
|
|
40
|
+
* **How many values a comparison takes decides what is under it.** A field
|
|
41
|
+
* whose comparison is `arity="none"`, `is empty` or `is set`, is offered no
|
|
42
|
+
* values at all and gives back any that were ticked: the comparison is the whole
|
|
43
|
+
* filter. One at `arity="one"` offers its values as a single choice, so pressing
|
|
44
|
+
* another replaces it and pressing the one already on clears it. Both announce
|
|
45
|
+
* `arity` in the facet, which is what lets a consumer tell an empty filter from
|
|
46
|
+
* no filter at all.
|
|
47
|
+
*
|
|
48
|
+
* **It applies nothing.** The chips are the consumer's markup and the rows are
|
|
49
|
+
* their data, so this announces and stops, the way `Notification` reports a read
|
|
50
|
+
* rather than inventing one. Refusing `helix-facet` refuses the change.
|
|
51
|
+
*
|
|
52
|
+
* @since 0.25.0
|
|
53
|
+
* @status experimental
|
|
54
|
+
* @category forms
|
|
55
|
+
* @aliases FacetPicker, FilterBuilder
|
|
56
|
+
* @documentation https://helix.fusion.dev/components/filter-input
|
|
57
|
+
*
|
|
58
|
+
* @slot - The fields this can filter by, as `FilterField`s. Read rather than rendered.
|
|
59
|
+
* @slot trigger - The control that opens the menu. A `Button` is the usual one.
|
|
60
|
+
*
|
|
61
|
+
* @event {HelixFacetEvent} helix-facet - Fired when a field's values change. Cancelable: refusing it takes the decision and nothing moves.
|
|
62
|
+
*/
|
|
63
|
+
export declare class HelixHeadlessFilterMenu extends HelixElement {
|
|
64
|
+
/**
|
|
65
|
+
* Accessible name for the menu.
|
|
66
|
+
*/
|
|
67
|
+
label: string;
|
|
68
|
+
/**
|
|
69
|
+
* What is ticked, per field.
|
|
70
|
+
*
|
|
71
|
+
* **Held here rather than read back off the menu**, because the menu's rows
|
|
72
|
+
* are rebuilt every time a page changes and a tick read from a row that has
|
|
73
|
+
* been re-rendered is a tick nobody made. This is the answer; the rows are a
|
|
74
|
+
* view of it.
|
|
75
|
+
*/
|
|
76
|
+
private readonly picked;
|
|
77
|
+
/** Which comparison each field is using, where the reader chose one. */
|
|
78
|
+
private readonly comparisons;
|
|
79
|
+
/**
|
|
80
|
+
* The fields declared inside this, in the order they were written.
|
|
81
|
+
*
|
|
82
|
+
* What a render may know of them: none on a server, whose shim has no tree
|
|
83
|
+
* and threw here, and none while hydrating what one sent.
|
|
84
|
+
*/
|
|
85
|
+
get fields(): HelixHeadlessFilterField[];
|
|
86
|
+
protected firstUpdated(changed: PropertyValues): void;
|
|
87
|
+
/** What is ticked for one field, as the consumer's own values. */
|
|
88
|
+
valuesFor(field: string): readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Which comparison a field is using: the reader's choice, then the one the
|
|
91
|
+
* field marked, then the first one it supports.
|
|
92
|
+
*
|
|
93
|
+
* **The last of those is not a fallback for nothing, it is the scalar case.**
|
|
94
|
+
* A date supports `after` and nothing else and marks no default, because there
|
|
95
|
+
* is nothing for a default to distinguish it from. Answering `null` there left
|
|
96
|
+
* a chip built from it with no comparison at all: `Created` and a date, with
|
|
97
|
+
* the word that made it mean anything missing. It is not marked `default`, so
|
|
98
|
+
* it is not the unwritten one and the chip says it, which is right.
|
|
99
|
+
*
|
|
100
|
+
* Read off `vocabulary` rather than off the operator elements, so a field that
|
|
101
|
+
* declared none is offered the defaults here and parses them in the text, from
|
|
102
|
+
* one answer rather than two that can drift.
|
|
103
|
+
*/
|
|
104
|
+
operatorFor(field: HelixHeadlessFilterField): FilterGrammarOperator | null;
|
|
105
|
+
/**
|
|
106
|
+
* Announces a field's set as it now stands, and reports whether it stood.
|
|
107
|
+
*
|
|
108
|
+
* The whole set rather than the one value that moved, because a filter is the
|
|
109
|
+
* set: a consumer redrawing a chip from a single tick would have to keep their
|
|
110
|
+
* own copy of everything else, which is the second source of truth this exists
|
|
111
|
+
* to avoid.
|
|
112
|
+
*/
|
|
113
|
+
private announce;
|
|
114
|
+
/**
|
|
115
|
+
* Filters a field by these values, announcing it the way a tick does.
|
|
116
|
+
*
|
|
117
|
+
* **The one way this element's state changes**, whether the move came from a
|
|
118
|
+
* row here, from a filter typed into the field around it, or from a chip
|
|
119
|
+
* somebody dismissed. Before this existed the ticks were only ever written by
|
|
120
|
+
* a press, so a chip removed anywhere else left the menu still showing it
|
|
121
|
+
* ticked, and the reader's next press on that value read as *unticking* it:
|
|
122
|
+
* they pressed `Refunded`, an empty set was announced, and nothing appeared.
|
|
123
|
+
*
|
|
124
|
+
* Refusing the `helix-facet` puts everything back, so a consumer who declines
|
|
125
|
+
* a filter is never left looking at a menu that says it took.
|
|
126
|
+
*/
|
|
127
|
+
apply(field: string, values: readonly string[], operator?: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* A row was chosen: a value to tick, or a comparison to use.
|
|
130
|
+
*
|
|
131
|
+
* **Read off the row rather than encoded into its `value`.** Packing the three
|
|
132
|
+
* facts into one string and splitting them apart again was the first idea, and
|
|
133
|
+
* it does not survive the template: a nested template literal inside a
|
|
134
|
+
* `static-html` template comes back carrying Lit's own binding marker where
|
|
135
|
+
* the separator was, so `field disposition` arrived with a null byte in the
|
|
136
|
+
* middle of it and every split produced nonsense. Three data attributes cost
|
|
137
|
+
* nothing and there is nothing to parse.
|
|
138
|
+
*
|
|
139
|
+
* Stopped here rather than let past, because a consumer listening for
|
|
140
|
+
* `helix-select` on their own page should not receive one for a row inside a
|
|
141
|
+
* control they did not write. Only the two rows that choose something need it:
|
|
142
|
+
* a row that opens a page is stopped by `Menu` itself, in a capture-phase
|
|
143
|
+
* handler, on the ground that looking into a submenu is not a choice. Measured
|
|
144
|
+
* both ways, and widening this to every `data-facet` row changed nothing.
|
|
145
|
+
*/
|
|
146
|
+
private readonly handleSelect;
|
|
147
|
+
protected render(): TemplateResult;
|
|
148
|
+
}
|
|
149
|
+
declare global {
|
|
150
|
+
interface HTMLElementTagNameMap {
|
|
151
|
+
'helix-headless-filter-menu': HelixHeadlessFilterMenu;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { customElement, HelixElement, HelixFacetEvent } from '@fusion.dev/helix-core';
|
|
8
|
+
import { nothing } from 'lit';
|
|
9
|
+
import { property } from 'lit/decorators.js';
|
|
10
|
+
import { html } from 'lit/static-html.js';
|
|
11
|
+
import '../../divider/index.js';
|
|
12
|
+
import '../../menu/index.js';
|
|
13
|
+
let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.label = 'Filter';
|
|
17
|
+
this.picked = new Map();
|
|
18
|
+
this.comparisons = new Map();
|
|
19
|
+
this.handleSelect = (event) => {
|
|
20
|
+
const row = event.composedPath()[0];
|
|
21
|
+
if (!(row instanceof HTMLElement)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const kind = row.dataset.facet;
|
|
25
|
+
if (kind !== 'value' && kind !== 'operator') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
const field = row.dataset.field ?? '';
|
|
30
|
+
const value = row.dataset.value ?? '';
|
|
31
|
+
if (kind === 'operator') {
|
|
32
|
+
this.apply(field, this.valuesFor(field), value);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const declared = this.fields.find((one) => (one.name ?? '') === field);
|
|
36
|
+
const held = this.picked.get(field) ?? new Set();
|
|
37
|
+
if (declared && this.operatorFor(declared)?.arity === 'one') {
|
|
38
|
+
this.apply(field, held.has(value) && held.size === 1 ? [] : [value]);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const ticked = new Set(held);
|
|
42
|
+
if (ticked.has(value)) {
|
|
43
|
+
ticked.delete(value);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
ticked.add(value);
|
|
47
|
+
}
|
|
48
|
+
this.apply(field, [...ticked]);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
get fields() {
|
|
52
|
+
return this.knownChildren.filter((child) => ['helix-headless-filter-field', 'helix-filter-field'].includes(child.localName));
|
|
53
|
+
}
|
|
54
|
+
firstUpdated(changed) {
|
|
55
|
+
super.firstUpdated(changed);
|
|
56
|
+
this.finishHydration();
|
|
57
|
+
}
|
|
58
|
+
valuesFor(field) {
|
|
59
|
+
return [...(this.picked.get(field) ?? [])];
|
|
60
|
+
}
|
|
61
|
+
operatorFor(field) {
|
|
62
|
+
const { operators } = field.vocabulary;
|
|
63
|
+
const chosen = this.comparisons.get(field.name ?? '');
|
|
64
|
+
const declared = operators.find((operator) => operator.default) ?? operators[0] ?? null;
|
|
65
|
+
if (chosen === undefined) {
|
|
66
|
+
return declared;
|
|
67
|
+
}
|
|
68
|
+
return operators.find((operator) => operator.name === chosen) ?? declared;
|
|
69
|
+
}
|
|
70
|
+
announce(field) {
|
|
71
|
+
const operator = this.operatorFor(field);
|
|
72
|
+
return this.dispatchEvent(new HelixFacetEvent({
|
|
73
|
+
arity: operator?.arity ?? 'many',
|
|
74
|
+
field: field.name ?? '',
|
|
75
|
+
label: field.label,
|
|
76
|
+
operator: operator?.name ?? '',
|
|
77
|
+
operatorLabel: operator?.default ? '' : (operator?.label ?? ''),
|
|
78
|
+
values: this.valuesFor(field.name ?? ''),
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
apply(field, values, operator) {
|
|
82
|
+
const declared = this.fields.find((one) => (one.name ?? '') === field);
|
|
83
|
+
if (!declared) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const heldValues = this.picked.get(field);
|
|
87
|
+
const heldOperator = this.comparisons.get(field);
|
|
88
|
+
if (operator !== undefined) {
|
|
89
|
+
this.comparisons.set(field, operator);
|
|
90
|
+
}
|
|
91
|
+
else if (values.length === 0) {
|
|
92
|
+
this.comparisons.delete(field);
|
|
93
|
+
}
|
|
94
|
+
const arity = this.operatorFor(declared)?.arity ?? 'many';
|
|
95
|
+
this.picked.set(field, new Set(arity === 'none' ? [] : values));
|
|
96
|
+
const stood = this.announce(declared);
|
|
97
|
+
if (!stood) {
|
|
98
|
+
if (heldValues === undefined) {
|
|
99
|
+
this.picked.delete(field);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.picked.set(field, heldValues);
|
|
103
|
+
}
|
|
104
|
+
if (heldOperator === undefined) {
|
|
105
|
+
this.comparisons.delete(field);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.comparisons.set(field, heldOperator);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.requestUpdate();
|
|
112
|
+
return stood;
|
|
113
|
+
}
|
|
114
|
+
render() {
|
|
115
|
+
const divider = this.tag('divider');
|
|
116
|
+
const menu = this.tag('menu');
|
|
117
|
+
const item = this.tag('menu-item');
|
|
118
|
+
return html `<${menu} label=${this.label} @helix-select=${this.handleSelect}>
|
|
119
|
+
<slot name="trigger" slot="trigger"></slot>
|
|
120
|
+
${this.fields.map((field) => {
|
|
121
|
+
const { label, name, operators, values } = field.vocabulary;
|
|
122
|
+
const ticked = this.picked.get(name) ?? new Set();
|
|
123
|
+
const using = this.operatorFor(field);
|
|
124
|
+
const offered = using?.arity === 'none' ? [] : values;
|
|
125
|
+
return html `<${item} data-facet="field" data-field=${name}>
|
|
126
|
+
${label}
|
|
127
|
+
${operators.length > 1
|
|
128
|
+
? html `<${item} slot="submenu" data-facet="comparison" data-field=${name}>
|
|
129
|
+
${using?.label ?? ''}
|
|
130
|
+
${operators.map((operator) => html `<${item}
|
|
131
|
+
slot="submenu"
|
|
132
|
+
checkable
|
|
133
|
+
exclusive
|
|
134
|
+
?checked=${operator.name === using?.name}
|
|
135
|
+
data-facet="operator"
|
|
136
|
+
data-field=${name}
|
|
137
|
+
data-value=${operator.name}
|
|
138
|
+
>${operator.label}</${item}>`)}
|
|
139
|
+
</${item}>`
|
|
140
|
+
: nothing}
|
|
141
|
+
${operators.length > 1 && offered.length > 0
|
|
142
|
+
? html `<${divider} slot="submenu" announced></${divider}>`
|
|
143
|
+
: nothing}
|
|
144
|
+
${offered.map((option) => html `<${item}
|
|
145
|
+
slot="submenu"
|
|
146
|
+
checkable
|
|
147
|
+
?exclusive=${using?.arity === 'one'}
|
|
148
|
+
?checked=${ticked.has(option.value)}
|
|
149
|
+
data-facet="value"
|
|
150
|
+
data-field=${name}
|
|
151
|
+
data-value=${option.value}
|
|
152
|
+
>${option.label}</${item}>`)}
|
|
153
|
+
</${item}>`;
|
|
154
|
+
})}
|
|
155
|
+
</${menu}>`;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
__decorate([
|
|
159
|
+
property()
|
|
160
|
+
], HelixHeadlessFilterMenu.prototype, "label", void 0);
|
|
161
|
+
HelixHeadlessFilterMenu = __decorate([
|
|
162
|
+
customElement('helix-headless-filter-menu')
|
|
163
|
+
], HelixHeadlessFilterMenu);
|
|
164
|
+
export { HelixHeadlessFilterMenu };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { nothing, type PropertyValues } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* @summary One way a filter can compare a field to its values.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* **A declaration, not a control.** It renders nothing and is never seen: it is
|
|
8
|
+
* how a consumer says which comparisons their field supports, in markup, beside
|
|
9
|
+
* the values it supports them on. `Field` reads them; the chip shows the one in
|
|
10
|
+
* force; the menu offers the rest.
|
|
11
|
+
*
|
|
12
|
+
* **The vocabulary is the consumer's and this element never learns it.** What
|
|
13
|
+
* `is none of` means against a queue of refunds is a question about refunds, so
|
|
14
|
+
* nothing here compares anything or knows what `name` will be handed to. Helix
|
|
15
|
+
* owns the shape a filter has, which is a field, an operator and some values,
|
|
16
|
+
* and owns the interaction that builds one. What the words mean is the domain's,
|
|
17
|
+
* which is the same line `FilterInput` already draws when it declines to learn a
|
|
18
|
+
* grammar.
|
|
19
|
+
*
|
|
20
|
+
* **`default` is what makes a chip readable.** The operator a field is usually
|
|
21
|
+
* filtered by is the one nobody needs told, so a chip drawn with it says only
|
|
22
|
+
* the field and the values. Naming a second operator `default` on one field is a
|
|
23
|
+
* consumer bug and the field reports it rather than guessing.
|
|
24
|
+
*
|
|
25
|
+
* @since 0.23.0
|
|
26
|
+
* @status experimental
|
|
27
|
+
* @category forms
|
|
28
|
+
* @aliases Comparator, Predicate
|
|
29
|
+
* @documentation https://helix.fusion.dev/components/filter-input
|
|
30
|
+
*/
|
|
31
|
+
export declare class HelixHeadlessFilterOperator extends HelixElement {
|
|
32
|
+
/**
|
|
33
|
+
* How many values this comparison takes.
|
|
34
|
+
*
|
|
35
|
+
* `none` is a comparison that is complete on its own, which is what `is empty`
|
|
36
|
+
* and `is set` are: a chip drawn for one shows no values because there are
|
|
37
|
+
* none to show, and the menu that builds it offers nothing to pick.
|
|
38
|
+
*
|
|
39
|
+
* `one` takes a single value, which is every scalar comparison: `is after` a
|
|
40
|
+
* date, `is above` a number. `many` takes a set, which is what `is any of` and
|
|
41
|
+
* `is none of` are, and is the only arity a multi-select panel makes sense
|
|
42
|
+
* for.
|
|
43
|
+
*
|
|
44
|
+
* Read rather than enforced. This element applies nothing, so the arity is a
|
|
45
|
+
* fact the picker uses to decide what to offer, not a rule it polices. What
|
|
46
|
+
* the picker does with it: `none` offers no values at all and takes back any
|
|
47
|
+
* that were ticked, since the comparison is the whole filter; `one` replaces
|
|
48
|
+
* rather than adds, and its rows announce as one of a set. The typed grammar
|
|
49
|
+
* reads it too, refusing a list where a scalar was declared and never writing
|
|
50
|
+
* a comparison that takes nothing.
|
|
51
|
+
*
|
|
52
|
+
* The arity reaches a consumer in `helix-facet`, which is what tells an empty
|
|
53
|
+
* filter from no filter: `values` empty means the field is no longer narrowing
|
|
54
|
+
* anything, unless the comparison is one that never had values to give.
|
|
55
|
+
*
|
|
56
|
+
* **Undefined reads as `many`, and takes no default**, which is the rule every
|
|
57
|
+
* reflected property here answers to: a default writes an attribute on upgrade
|
|
58
|
+
* that the server never wrote, and the two markups differ before a reader has
|
|
59
|
+
* touched anything.
|
|
60
|
+
*/
|
|
61
|
+
arity?: 'many' | 'none' | 'one';
|
|
62
|
+
/**
|
|
63
|
+
* Whether this is the comparison the field is filtered by unless asked
|
|
64
|
+
* otherwise.
|
|
65
|
+
*
|
|
66
|
+
* The chip leaves a default operator unwritten, so the common filter reads as
|
|
67
|
+
* a field and its values and nothing else, and the word only appears once
|
|
68
|
+
* somebody has chosen a comparison worth noticing.
|
|
69
|
+
*/
|
|
70
|
+
default: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* What a reader is shown, in the chip and in the menu that changes it.
|
|
73
|
+
*
|
|
74
|
+
* Words rather than symbols, and the reason is that a set has more than one
|
|
75
|
+
* negation: `!=` cannot tell "is none of" from "is not all of", and both are
|
|
76
|
+
* filters somebody wants. A chip is read, so it takes the reading. The text
|
|
77
|
+
* grammar a consumer types is theirs and may use whatever symbols it likes.
|
|
78
|
+
*/
|
|
79
|
+
label: string;
|
|
80
|
+
/**
|
|
81
|
+
* What the consumer's own model calls this comparison.
|
|
82
|
+
*
|
|
83
|
+
* Never shown. It is what arrives in the filter this element helps build, so
|
|
84
|
+
* a consumer matches on it rather than on the words a reader sees, and
|
|
85
|
+
* translating the label breaks nothing.
|
|
86
|
+
*
|
|
87
|
+
* Reflected and so undefaulted, for the reason `arity` gives above.
|
|
88
|
+
*/
|
|
89
|
+
name?: string;
|
|
90
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
91
|
+
/**
|
|
92
|
+
* Nothing, which is what makes this a declaration.
|
|
93
|
+
*
|
|
94
|
+
* **No `<slot>`, deliberately.** An element that renders no slot renders none
|
|
95
|
+
* of its children, so the words a consumer writes inside one are held for
|
|
96
|
+
* whoever reads them and never painted. That is the whole mechanism, and it
|
|
97
|
+
* costs no `display: none` in a tier that has no stylesheet to put one in.
|
|
98
|
+
*/
|
|
99
|
+
protected render(): typeof nothing;
|
|
100
|
+
}
|
|
101
|
+
declare global {
|
|
102
|
+
interface HTMLElementTagNameMap {
|
|
103
|
+
'helix-headless-filter-operator': HelixHeadlessFilterOperator;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { customElement, HelixElement } from '@fusion.dev/helix-core';
|
|
8
|
+
import { nothing } from 'lit';
|
|
9
|
+
import { property } from 'lit/decorators.js';
|
|
10
|
+
let HelixHeadlessFilterOperator = class HelixHeadlessFilterOperator extends HelixElement {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.default = false;
|
|
14
|
+
this.label = '';
|
|
15
|
+
}
|
|
16
|
+
willUpdate(changed) {
|
|
17
|
+
super.willUpdate(changed);
|
|
18
|
+
this.internals.role = 'none';
|
|
19
|
+
}
|
|
20
|
+
render() {
|
|
21
|
+
return nothing;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
__decorate([
|
|
25
|
+
property({ reflect: true })
|
|
26
|
+
], HelixHeadlessFilterOperator.prototype, "arity", void 0);
|
|
27
|
+
__decorate([
|
|
28
|
+
property({ reflect: true, type: Boolean })
|
|
29
|
+
], HelixHeadlessFilterOperator.prototype, "default", void 0);
|
|
30
|
+
__decorate([
|
|
31
|
+
property()
|
|
32
|
+
], HelixHeadlessFilterOperator.prototype, "label", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
property({ reflect: true })
|
|
35
|
+
], HelixHeadlessFilterOperator.prototype, "name", void 0);
|
|
36
|
+
HelixHeadlessFilterOperator = __decorate([
|
|
37
|
+
customElement('helix-headless-filter-operator')
|
|
38
|
+
], HelixHeadlessFilterOperator);
|
|
39
|
+
export { HelixHeadlessFilterOperator };
|