@kubex/zinc 1.1.120 → 1.1.122
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/dist/custom-elements.json +1952 -921
- package/dist/vscode.html-custom-data.json +80 -58
- package/dist/web-types.json +208 -142
- package/dist/zn.d.ts +403 -298
- package/dist/zn.min.js +1511 -1370
- package/docs/pages/components/button.md +9 -0
- package/docs/pages/components/data-table-filter.md +60 -4
- package/docs/pages/components/data-table-search.md +3 -3
- package/docs/pages/components/data-table.md +78 -9
- package/docs/pages/components/datepicker.md +10 -0
- package/docs/pages/components/menu-item.md +16 -2
- package/docs/pages/components/translation-group.md +2 -2
- package/package.json +1 -1
- package/src/components/button/button.component.ts +11 -4
- package/src/components/button/button.scss +12 -1
- package/src/components/data-table/data-table.component.ts +341 -139
- package/src/components/data-table/data-table.scss +105 -42
- package/src/components/data-table/data-table.test.ts +110 -0
- package/src/components/data-table-filter/data-table-filter.component.ts +487 -49
- package/src/components/data-table-filter/data-table-filter.scss +163 -9
- package/src/components/data-table-filter/data-table-filter.test.ts +328 -2
- package/src/components/data-table-search/data-table-search.component.ts +1 -1
- package/src/components/datepicker/datepicker.component.ts +23 -2
- package/src/components/datepicker/datepicker.scss +25 -0
- package/src/components/datepicker/datepicker.test.ts +28 -1
- package/src/components/input/input.component.ts +1 -0
- package/src/components/input/input.scss +1 -1
- package/src/components/menu/menu.component.ts +2 -0
- package/src/components/menu/menu.scss +23 -2
- package/src/components/menu-item/menu-item.component.ts +7 -1
- package/src/components/menu-item/menu-item.scss +4 -4
- package/src/components/menu-item/menu-item.test.ts +39 -1
- package/src/components/panel/panel.component.ts +12 -5
- package/src/components/panel/panel.scss +3 -3
- package/src/components/query-builder/query-builder.component.ts +1 -1
- package/src/components/tooltip/tooltip.component.ts +2 -2
- package/src/components/tooltip/tooltip.test.ts +70 -2
- package/src/components/translation-group/translation-group.component.ts +5 -7
- package/src/components/translation-group/translation-group.test.ts +8 -8
- package/src/components/translations/translations.component.ts +3 -4
- package/src/components/translations/translations.test.ts +1 -1
|
@@ -1,39 +1,103 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {classMap} from 'lit/directives/class-map.js';
|
|
2
|
+
import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
|
|
2
3
|
import {FormControlController, validValidityState} from "../../internal/form";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
operatorText,
|
|
6
|
+
type QueryBuilderData,
|
|
7
|
+
type QueryBuilderDateSubmitFormat,
|
|
8
|
+
type QueryBuilderItem,
|
|
9
|
+
QueryBuilderOperators,
|
|
10
|
+
type QueryBuilderOptions
|
|
11
|
+
} from "../query-builder";
|
|
12
|
+
import {property, state} from 'lit/decorators.js';
|
|
4
13
|
import ZincElement, {type ZincFormControl} from '../../internal/zinc-element';
|
|
5
|
-
|
|
14
|
+
import ZnButton from "../button";
|
|
15
|
+
import ZnDatepicker from "../datepicker";
|
|
16
|
+
import ZnDropdown from "../dropdown";
|
|
17
|
+
import ZnInput from "../input";
|
|
18
|
+
import ZnMenu from "../menu";
|
|
19
|
+
import ZnMenuItem from "../menu-item";
|
|
20
|
+
import type {ZnInputEvent} from "../../events/zn-input";
|
|
6
21
|
|
|
7
22
|
import styles from './data-table-filter.scss';
|
|
8
23
|
|
|
24
|
+
interface ActiveFilter {
|
|
25
|
+
key: string;
|
|
26
|
+
comparator: QueryBuilderOperators;
|
|
27
|
+
value: string;
|
|
28
|
+
/** Display text for values the wire format renders unreadable, such as dates. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Kept so a date can be re-encoded when the comparator changes. */
|
|
31
|
+
timestamp?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BOOLEAN_OPTIONS: QueryBuilderOptions = {1: 'True', 0: 'False'};
|
|
35
|
+
const TEXT_DEBOUNCE = 350;
|
|
36
|
+
const SUGGESTION_LIMIT = 8;
|
|
37
|
+
const OPTION_SEARCH_THRESHOLD = 8;
|
|
38
|
+
|
|
39
|
+
// Pills have to stay short, so the wordier operators get a compact label
|
|
40
|
+
const OPERATOR_LABELS: Partial<Record<QueryBuilderOperators, string>> = {
|
|
41
|
+
[QueryBuilderOperators.Eq]: 'is',
|
|
42
|
+
[QueryBuilderOperators.Neq]: 'is not',
|
|
43
|
+
[QueryBuilderOperators.Gt]: '>',
|
|
44
|
+
[QueryBuilderOperators.Gte]: '\u2265',
|
|
45
|
+
[QueryBuilderOperators.Lt]: '<',
|
|
46
|
+
[QueryBuilderOperators.Lte]: '\u2264',
|
|
47
|
+
[QueryBuilderOperators.In]: 'is any of',
|
|
48
|
+
[QueryBuilderOperators.Nin]: 'is none of',
|
|
49
|
+
[QueryBuilderOperators.Before]: 'before',
|
|
50
|
+
[QueryBuilderOperators.After]: 'after',
|
|
51
|
+
};
|
|
9
52
|
|
|
10
53
|
/**
|
|
11
|
-
* @summary
|
|
54
|
+
* @summary An inline filter bar for data tables. Each active filter is a pill whose dropdown sets its value.
|
|
12
55
|
* @documentation https://zinc.style/components/data-table-filter
|
|
13
56
|
* @status experimental
|
|
14
57
|
* @since 1.0
|
|
15
58
|
*
|
|
16
|
-
* @dependency zn-
|
|
59
|
+
* @dependency zn-button
|
|
60
|
+
* @dependency zn-datepicker
|
|
61
|
+
* @dependency zn-dropdown
|
|
62
|
+
* @dependency zn-input
|
|
63
|
+
* @dependency zn-menu
|
|
64
|
+
* @dependency zn-menu-item
|
|
17
65
|
*
|
|
18
|
-
* @event zn-
|
|
19
|
-
*
|
|
20
|
-
* @slot - The default slot.
|
|
21
|
-
* @slot example - An example slot.
|
|
22
|
-
*
|
|
23
|
-
* @csspart base - The component's base wrapper.
|
|
24
|
-
*
|
|
25
|
-
* @cssproperty --example - An example CSS custom property.
|
|
66
|
+
* @event zn-filter-change - Emitted when the active filters change. Read the encoded query off `value`.
|
|
26
67
|
*/
|
|
27
68
|
export default class ZnDataTableFilter extends ZincElement implements ZincFormControl {
|
|
28
69
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
70
|
+
static dependencies = {
|
|
71
|
+
'zn-button': ZnButton,
|
|
72
|
+
'zn-datepicker': ZnDatepicker,
|
|
73
|
+
'zn-dropdown': ZnDropdown,
|
|
74
|
+
'zn-input': ZnInput,
|
|
75
|
+
'zn-menu': ZnMenu,
|
|
76
|
+
'zn-menu-item': ZnMenuItem,
|
|
77
|
+
};
|
|
29
78
|
|
|
30
79
|
private _formController: FormControlController = new FormControlController(this, {});
|
|
80
|
+
private _textTimeout?: number;
|
|
81
|
+
private _pendingOpen?: string;
|
|
31
82
|
|
|
32
|
-
@property() filters =
|
|
83
|
+
@property({type: Array}) filters: QueryBuilderData = [];
|
|
33
84
|
|
|
34
85
|
@property() name: string = "data-table-filter";
|
|
35
86
|
|
|
36
|
-
@property() value: string;
|
|
87
|
+
@property() value: string = '';
|
|
88
|
+
|
|
89
|
+
/** Filter keys to show a pill for before the user adds any. */
|
|
90
|
+
@property({attribute: 'default-filters'}) defaultFilters: string = '';
|
|
91
|
+
|
|
92
|
+
/** Typeahead values per filter key. `zn-data-table` fills this from the rows it has loaded. */
|
|
93
|
+
@property({attribute: false}) suggestions: Record<string, string[]> = {};
|
|
94
|
+
|
|
95
|
+
@state() private _active: ActiveFilter[] = [];
|
|
96
|
+
|
|
97
|
+
// What the user has typed so far, which the debounced value hasn't caught up with yet
|
|
98
|
+
@state() private _typing: Record<string, string> = {};
|
|
99
|
+
|
|
100
|
+
@state() private _optionQuery: Record<string, string> = {};
|
|
37
101
|
|
|
38
102
|
get validationMessage(): string {
|
|
39
103
|
return '';
|
|
@@ -64,63 +128,437 @@ export default class ZnDataTableFilter extends ZincElement implements ZincFormCo
|
|
|
64
128
|
this._formController.updateValidity();
|
|
65
129
|
}
|
|
66
130
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
131
|
+
protected updated(changed: PropertyValues) {
|
|
132
|
+
super.updated(changed);
|
|
133
|
+
|
|
134
|
+
const key = this._pendingOpen;
|
|
135
|
+
if (!key) return;
|
|
136
|
+
this._pendingOpen = undefined;
|
|
137
|
+
|
|
138
|
+
// The pill has only just rendered, so let it upgrade before anchoring a panel to it
|
|
139
|
+
requestAnimationFrame(() => {
|
|
140
|
+
const pill = [...this.shadowRoot!.querySelectorAll('zn-dropdown')]
|
|
141
|
+
.find(dropdown => dropdown.dataset.key === key);
|
|
142
|
+
|
|
143
|
+
// The panel animates in, so the field can only take focus once it has settled
|
|
144
|
+
void pill?.show().then(() => requestAnimationFrame(() => pill.querySelector('zn-input')?.focus()));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected willUpdate(changed: PropertyValues) {
|
|
149
|
+
super.willUpdate(changed);
|
|
150
|
+
|
|
151
|
+
if (changed.has('filters') && this._active.length === 0) {
|
|
152
|
+
this.defaultFilters
|
|
153
|
+
.split(',')
|
|
154
|
+
.map(key => key.trim())
|
|
155
|
+
.filter(key => key.length > 0)
|
|
156
|
+
.forEach(key => this.addFilter(key, false));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
disconnectedCallback() {
|
|
161
|
+
super.disconnectedCallback();
|
|
162
|
+
if (this._textTimeout) {
|
|
163
|
+
window.clearTimeout(this._textTimeout);
|
|
71
164
|
}
|
|
165
|
+
}
|
|
72
166
|
|
|
73
|
-
|
|
167
|
+
/** Number of filters with a value set. */
|
|
168
|
+
get activeCount(): number {
|
|
169
|
+
return this._active.filter(item => item.value.length > 0).length;
|
|
74
170
|
}
|
|
75
171
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
172
|
+
/** Removes every active filter. */
|
|
173
|
+
clear() {
|
|
174
|
+
this._active = [];
|
|
175
|
+
this.emitChange();
|
|
176
|
+
this.emit('zn-clear');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private definition(key: string): QueryBuilderItem | undefined {
|
|
180
|
+
return this.filters.find((item: QueryBuilderItem) => item.id === key);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private operatorsFor(filter: QueryBuilderItem): QueryBuilderOperators[] {
|
|
184
|
+
return filter.operators?.length > 0 ? filter.operators : [QueryBuilderOperators.Eq];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private comparatorFor(filter: QueryBuilderItem): QueryBuilderOperators {
|
|
188
|
+
return this.operatorsFor(filter)[0];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private operatorLabel(comparator: QueryBuilderOperators): string {
|
|
192
|
+
return OPERATOR_LABELS[comparator] ?? operatorText[comparator].toLowerCase();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// `in`/`nin` take a set of values; every other comparator holds one
|
|
196
|
+
private isMultiple(comparator: QueryBuilderOperators): boolean {
|
|
197
|
+
return comparator === QueryBuilderOperators.In || comparator === QueryBuilderOperators.Nin;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private optionsFor(filter: QueryBuilderItem): QueryBuilderOptions | undefined {
|
|
201
|
+
if (filter.options) return filter.options;
|
|
202
|
+
if (filter.type === 'bool' || filter.type === 'boolean') return BOOLEAN_OPTIONS;
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private addFilter(key: string, emit = true) {
|
|
207
|
+
if (this._active.some(item => item.key === key)) return;
|
|
208
|
+
|
|
209
|
+
const filter = this.definition(key);
|
|
210
|
+
if (!filter) return;
|
|
211
|
+
|
|
212
|
+
this._active = [...this._active, {key, comparator: this.comparatorFor(filter), value: ''}];
|
|
213
|
+
|
|
214
|
+
if (emit) {
|
|
215
|
+
// Picking a filter and setting its value is one action, so carry straight on to the value
|
|
216
|
+
this._pendingOpen = key;
|
|
217
|
+
this.emitChange();
|
|
80
218
|
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private removeFilter(key: string) {
|
|
222
|
+
this._active = this._active.filter(item => item.key !== key);
|
|
223
|
+
this.emitChange();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private setValue(key: string, value: string, label?: string, timestamp?: number) {
|
|
227
|
+
this._active = this._active.map(item => item.key === key ? {...item, value, label, timestamp} : item);
|
|
228
|
+
this.emitChange();
|
|
229
|
+
}
|
|
81
230
|
|
|
82
|
-
|
|
231
|
+
private setComparator(filter: QueryBuilderItem, active: ActiveFilter, comparator: QueryBuilderOperators) {
|
|
232
|
+
// A set of values can't survive a move to a comparator that holds one
|
|
233
|
+
const value = this.isMultiple(active.comparator) && !this.isMultiple(comparator)
|
|
234
|
+
? this.selectedValues(filter, active)[0] ?? ''
|
|
235
|
+
: active.value;
|
|
236
|
+
|
|
237
|
+
this._active = this._active.map(item => item.key === active.key
|
|
238
|
+
? {
|
|
239
|
+
...item,
|
|
240
|
+
comparator,
|
|
241
|
+
value: item.timestamp
|
|
242
|
+
? this.serializeDate(item.timestamp, comparator, filter.dateSubmitFormat)
|
|
243
|
+
: value,
|
|
244
|
+
}
|
|
245
|
+
: item);
|
|
246
|
+
this.emitChange();
|
|
83
247
|
}
|
|
84
248
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
249
|
+
private toggleOption(filter: QueryBuilderItem, active: ActiveFilter, option: string) {
|
|
250
|
+
if (!this.isMultiple(active.comparator)) {
|
|
251
|
+
this.setValue(active.key, active.value === option ? '' : option);
|
|
88
252
|
return;
|
|
89
253
|
}
|
|
90
254
|
|
|
91
|
-
|
|
255
|
+
const selected = this.selectedValues(filter, active);
|
|
256
|
+
const next = selected.includes(option)
|
|
257
|
+
? selected.filter(item => item !== option)
|
|
258
|
+
: [...selected, option];
|
|
259
|
+
|
|
260
|
+
this.setValue(active.key, next.join(' '));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Option keys keep the space-separated wire format the query builder uses; typed
|
|
264
|
+
// values can contain spaces of their own, so they only split on commas
|
|
265
|
+
private selectedValues(filter: QueryBuilderItem, active: ActiveFilter): string[] {
|
|
266
|
+
return active.value
|
|
267
|
+
.split(this.optionsFor(filter) ? ' ' : ',')
|
|
268
|
+
.map(item => item.trim())
|
|
269
|
+
.filter(item => item.length > 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private emitChange() {
|
|
273
|
+
const data = this._active
|
|
274
|
+
.filter(item => item.value.length > 0)
|
|
275
|
+
.map(item => ({key: item.key, comparator: item.comparator, value: item.value}));
|
|
92
276
|
|
|
93
|
-
this.
|
|
277
|
+
this.value = data.length > 0 ? btoa(JSON.stringify(data)) : '';
|
|
94
278
|
this._formController.updateValidity();
|
|
95
279
|
this.emit('zn-filter-change');
|
|
96
280
|
}
|
|
97
281
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
282
|
+
private handleDateInput(filter: QueryBuilderItem, active: ActiveFilter, event: Event) {
|
|
283
|
+
const picker = event.target as ZnDatepicker;
|
|
284
|
+
const timestamp = picker.timestamp;
|
|
285
|
+
|
|
286
|
+
if (!timestamp) {
|
|
287
|
+
this.setValue(active.key, '');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.setValue(
|
|
292
|
+
active.key,
|
|
293
|
+
this.serializeDate(timestamp, active.comparator, filter.dateSubmitFormat),
|
|
294
|
+
picker.value as string,
|
|
295
|
+
timestamp
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// A time picker needs the panel to stay put while the time is set
|
|
299
|
+
if (filter.type !== 'dateTime') {
|
|
300
|
+
void picker.closest('zn-dropdown')?.hide();
|
|
102
301
|
}
|
|
103
302
|
}
|
|
104
303
|
|
|
105
|
-
|
|
304
|
+
// Mirrors zn-query-builder's date serialization so backends built against it keep working
|
|
305
|
+
private serializeDate(
|
|
306
|
+
timestamp: number,
|
|
307
|
+
comparator: QueryBuilderOperators,
|
|
308
|
+
format: QueryBuilderDateSubmitFormat = 'legacy'
|
|
309
|
+
): string {
|
|
310
|
+
if (format === 'timestamp') return timestamp.toString();
|
|
311
|
+
|
|
312
|
+
if (format === 'iso') {
|
|
313
|
+
const date = new Date(timestamp);
|
|
314
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (comparator === QueryBuilderOperators.Eq || comparator === QueryBuilderOperators.Neq) {
|
|
318
|
+
return (timestamp / 1000).toString();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Everything else goes as minutes from now, negated for `before`
|
|
322
|
+
const multiplier = comparator === QueryBuilderOperators.Before ? -1 : 1;
|
|
323
|
+
return (Math.floor((Date.now() - timestamp) / 1000 / 60) * multiplier).toString();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private handleTextInput(key: string, event: ZnInputEvent) {
|
|
327
|
+
const input = event.target as ZnInput;
|
|
328
|
+
const value = input.value as string;
|
|
329
|
+
|
|
330
|
+
this._typing = {...this._typing, [key]: value};
|
|
331
|
+
|
|
332
|
+
if (this._textTimeout) window.clearTimeout(this._textTimeout);
|
|
333
|
+
this._textTimeout = window.setTimeout(() => this.setValue(key, value), TEXT_DEBOUNCE);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private suggestionsFor(filter: QueryBuilderItem, active: ActiveFilter): string[] {
|
|
337
|
+
const available = this.suggestions[active.key] ?? [];
|
|
338
|
+
if (available.length === 0) return [];
|
|
339
|
+
|
|
340
|
+
// Match the term being typed, which for a multi-value filter is the one after the last comma
|
|
341
|
+
const typed = (this._typing[active.key] ?? active.value).split(',').pop()!.trim().toLowerCase();
|
|
342
|
+
const selected = this.selectedValues(filter, active).map(item => item.toLowerCase());
|
|
343
|
+
|
|
344
|
+
return available
|
|
345
|
+
.filter(value => value.toLowerCase().includes(typed) && !selected.includes(value.toLowerCase()))
|
|
346
|
+
.slice(0, SUGGESTION_LIMIT);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private applySuggestion(filter: QueryBuilderItem, active: ActiveFilter, value: string, event: Event) {
|
|
350
|
+
if (this._textTimeout) window.clearTimeout(this._textTimeout);
|
|
351
|
+
|
|
352
|
+
const next = this.isMultiple(active.comparator)
|
|
353
|
+
? [...this.selectedValues(filter, active), value].join(',')
|
|
354
|
+
: value;
|
|
355
|
+
|
|
356
|
+
this._typing = {...this._typing, [active.key]: next};
|
|
357
|
+
this.setValue(active.key, next);
|
|
358
|
+
|
|
359
|
+
if (!this.isMultiple(active.comparator)) {
|
|
360
|
+
void (event.currentTarget as HTMLElement).closest('zn-dropdown')?.hide();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private humanize(value: string) {
|
|
365
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private pillLabel(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
369
|
+
const name = this.humanize(filter.name);
|
|
370
|
+
const selected = this.selectedValues(filter, active);
|
|
371
|
+
|
|
372
|
+
if (selected.length === 0) return name;
|
|
373
|
+
|
|
374
|
+
// With a choice of operator the pill has to say which one is applied
|
|
375
|
+
const operator = this.operatorsFor(filter).length > 1
|
|
376
|
+
? ` ${this.operatorLabel(active.comparator)}`
|
|
377
|
+
: '';
|
|
378
|
+
|
|
379
|
+
if (selected.length > 1) return `${name}${operator} (${selected.length})`;
|
|
380
|
+
|
|
381
|
+
const options = this.optionsFor(filter);
|
|
382
|
+
const value = active.label ?? (options ? options[selected[0]] ?? selected[0] : selected[0]);
|
|
383
|
+
|
|
384
|
+
return operator ? `${name}${operator} ${value}` : `${name}: ${value}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private renderOperators(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
388
|
+
const operators = this.operatorsFor(filter);
|
|
389
|
+
if (operators.length < 2) return nothing;
|
|
390
|
+
|
|
106
391
|
return html`
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
392
|
+
<div class="filter-bar__operators">
|
|
393
|
+
${operators.map(operator => html`
|
|
394
|
+
<zn-button class="${classMap({
|
|
395
|
+
'filter-bar__operator': true,
|
|
396
|
+
'filter-bar__operator--selected': operator === active.comparator,
|
|
397
|
+
})}"
|
|
398
|
+
outline
|
|
399
|
+
@click="${() => this.setComparator(filter, active, operator)}">
|
|
400
|
+
${this.operatorLabel(operator)}
|
|
401
|
+
</zn-button>`)}
|
|
402
|
+
</div>`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private renderPill(active: ActiveFilter) {
|
|
406
|
+
const filter = this.definition(active.key);
|
|
407
|
+
if (!filter) return nothing;
|
|
112
408
|
|
|
113
|
-
|
|
114
|
-
<zn-query-builder filters="${this.filters}"></zn-query-builder>
|
|
115
|
-
</div>
|
|
409
|
+
const operators = this.operatorsFor(filter);
|
|
116
410
|
|
|
117
|
-
|
|
411
|
+
return html`
|
|
412
|
+
<div class="filter-bar__chip">
|
|
413
|
+
<zn-dropdown placement="bottom-start"
|
|
414
|
+
data-key="${active.key}"
|
|
415
|
+
?stay-open-on-select="${this.isMultiple(active.comparator)}">
|
|
416
|
+
<zn-button slot="trigger"
|
|
417
|
+
class="filter-bar__pill"
|
|
418
|
+
pill
|
|
419
|
+
panel-bg
|
|
420
|
+
icon="chevron-down@lu"
|
|
421
|
+
icon-size="16"
|
|
422
|
+
icon-position="right">${this.pillLabel(filter, active)}
|
|
423
|
+
</zn-button>
|
|
424
|
+
${this.optionsFor(filter)
|
|
425
|
+
? html`
|
|
426
|
+
<zn-menu variant="shell">
|
|
427
|
+
${operators.length > 1 ? html`
|
|
428
|
+
${operators.map(operator => html`
|
|
429
|
+
<zn-menu-item type="checkbox"
|
|
430
|
+
keep-open
|
|
431
|
+
?checked="${operator === active.comparator}"
|
|
432
|
+
@zn-menu-select="${() => this.setComparator(filter, active, operator)}">
|
|
433
|
+
${this.operatorLabel(operator)}
|
|
434
|
+
</zn-menu-item>`)}
|
|
435
|
+
<div class="filter-bar__separator"></div>` : nothing}
|
|
436
|
+
${this.renderOptionSearch(filter, active)}
|
|
437
|
+
${this.renderOptionItems(filter, active)}
|
|
438
|
+
</zn-menu>`
|
|
439
|
+
: html`
|
|
440
|
+
<div class="filter-bar__editor">
|
|
441
|
+
${this.renderOperators(filter, active)}
|
|
442
|
+
${this.renderValueInput(filter, active)}
|
|
443
|
+
</div>`}
|
|
444
|
+
</zn-dropdown>
|
|
445
|
+
<zn-button class="filter-bar__remove"
|
|
446
|
+
icon-button="small"
|
|
447
|
+
plain
|
|
448
|
+
icon="x@lu"
|
|
449
|
+
icon-size="16"
|
|
450
|
+
label="Remove ${this.humanize(filter.name)} filter"
|
|
451
|
+
@click="${() => this.removeFilter(active.key)}">
|
|
118
452
|
</zn-button>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
453
|
+
</div>`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private isDateFilter(filter: QueryBuilderItem): boolean {
|
|
457
|
+
return filter.type === 'date' || filter.type === 'dateTime';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private renderValueInput(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
461
|
+
if (this.isDateFilter(filter)) {
|
|
462
|
+
return html`
|
|
463
|
+
<zn-datepicker inline
|
|
464
|
+
?time-picker="${filter.type === 'dateTime'}"
|
|
465
|
+
format-time="HH:mm"
|
|
466
|
+
@zn-change="${(e: Event) => this.handleDateInput(filter, active, e)}">
|
|
467
|
+
</zn-datepicker>`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const suggestions = this.suggestionsFor(filter, active);
|
|
471
|
+
|
|
472
|
+
return html`
|
|
473
|
+
<zn-input type="${filter.type === 'number' ? 'number' : 'text'}"
|
|
474
|
+
no-spin-buttons
|
|
475
|
+
placeholder="${this.humanize(filter.name)}"
|
|
476
|
+
value="${active.value}"
|
|
477
|
+
@zn-input="${(e: ZnInputEvent) => this.handleTextInput(active.key, e)}">
|
|
478
|
+
</zn-input>
|
|
479
|
+
${suggestions.length > 0 ? html`
|
|
480
|
+
<div class="filter-bar__suggestions">
|
|
481
|
+
${suggestions.map(value => html`
|
|
482
|
+
<zn-button class="filter-bar__suggestion"
|
|
483
|
+
text
|
|
484
|
+
@click="${(e: Event) => this.applySuggestion(filter, active, value, e)}">${value}
|
|
485
|
+
</zn-button>`)}
|
|
486
|
+
</div>` : nothing}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private handleOptionSearch(key: string, event: ZnInputEvent) {
|
|
490
|
+
const input = event.target as ZnInput;
|
|
491
|
+
this._optionQuery = {...this._optionQuery, [key]: input.value as string};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private renderOptionSearch(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
495
|
+
if (Object.keys(this.optionsFor(filter)!).length <= OPTION_SEARCH_THRESHOLD) return nothing;
|
|
496
|
+
|
|
497
|
+
return html`
|
|
498
|
+
<zn-input class="filter-bar__option-search"
|
|
499
|
+
size="small"
|
|
500
|
+
placeholder="Search ${this.humanize(filter.name)}"
|
|
501
|
+
value="${this._optionQuery[active.key] ?? ''}"
|
|
502
|
+
@zn-input="${(e: ZnInputEvent) => this.handleOptionSearch(active.key, e)}"
|
|
503
|
+
@keydown="${(e: KeyboardEvent) => e.stopPropagation()}">
|
|
504
|
+
</zn-input>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private renderOptionItems(filter: QueryBuilderItem, active: ActiveFilter) {
|
|
508
|
+
const options = this.optionsFor(filter)!;
|
|
509
|
+
const selected = this.selectedValues(filter, active);
|
|
510
|
+
const query = (this._optionQuery[active.key] ?? '').trim().toLowerCase();
|
|
511
|
+
const matches = Object.keys(options).filter(key => String(options[key]).toLowerCase().includes(query));
|
|
512
|
+
|
|
513
|
+
if (matches.length === 0) {
|
|
514
|
+
return html`
|
|
515
|
+
<zn-menu-item class="filter-bar__no-matches" disabled>No matches</zn-menu-item>`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return matches.map(key => html`
|
|
519
|
+
<zn-menu-item type="checkbox"
|
|
520
|
+
value="${key}"
|
|
521
|
+
?checked="${selected.includes(key)}"
|
|
522
|
+
@zn-menu-select="${() => this.toggleOption(filter, active, key)}">${options[key]}
|
|
523
|
+
</zn-menu-item>`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private renderAddFilter() {
|
|
527
|
+
const available = this.filters.filter(
|
|
528
|
+
(filter: QueryBuilderItem) => !this._active.some(item => item.key === filter.id)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (available.length === 0) return nothing;
|
|
532
|
+
|
|
533
|
+
return html`
|
|
534
|
+
<zn-dropdown placement="bottom-start">
|
|
535
|
+
<zn-button slot="trigger"
|
|
536
|
+
class="filter-bar__add"
|
|
537
|
+
icon-button="small"
|
|
538
|
+
icon="plus@lu"
|
|
539
|
+
icon-size="18"
|
|
540
|
+
tooltip="Add filter"
|
|
541
|
+
aria-label="Add filter">
|
|
542
|
+
</zn-button>
|
|
543
|
+
<zn-menu variant="shell">
|
|
544
|
+
${available.map((filter: QueryBuilderItem) => html`
|
|
545
|
+
<zn-menu-item value="${filter.id}"
|
|
546
|
+
@zn-menu-select="${() => this.addFilter(filter.id)}">${this.humanize(filter.name)}
|
|
547
|
+
</zn-menu-item>`)}
|
|
548
|
+
</zn-menu>
|
|
549
|
+
</zn-dropdown>`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
render() {
|
|
553
|
+
if (this.filters.length === 0) return nothing;
|
|
554
|
+
|
|
555
|
+
return html`
|
|
556
|
+
<div class="filter-bar">
|
|
557
|
+
${this._active.map((active: ActiveFilter) => this.renderPill(active))}
|
|
558
|
+
${this.renderAddFilter()}
|
|
559
|
+
${this._active.length > 0 ? html`
|
|
560
|
+
<zn-button class="filter-bar__clear" text @click="${this.clear}">Clear</zn-button>` : nothing}
|
|
561
|
+
</div>
|
|
124
562
|
`;
|
|
125
563
|
}
|
|
126
564
|
}
|