@ketvietlab/design-system 0.1.24 → 0.1.29
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/README.md +29 -0
- package/dist/atlas/island-runtime.d.mts +2 -0
- package/dist/atlas/island-runtime.d.mts.map +1 -0
- package/dist/atlas/island-runtime.mjs +2 -0
- package/dist/atlas/island-runtime.mjs.map +1 -0
- package/dist/atlas/relation-select-hydration.d.ts +2 -0
- package/dist/atlas/relation-select-hydration.d.ts.map +1 -0
- package/dist/atlas/relation-select-hydration.js +40 -0
- package/dist/atlas/relation-select-hydration.js.map +1 -0
- package/dist/atlas-cli.d.ts.map +1 -1
- package/dist/atlas-cli.js +18 -3
- package/dist/atlas-cli.js.map +1 -1
- package/dist/catalogue/index.js +1 -1
- package/dist/catalogue/inventory-policy.json +49 -0
- package/dist/catalogue/inventory.generated.js +1 -1
- package/dist/catalogue/inventory.generated.js.map +1 -1
- package/dist/catalogue/inventory.generated.json +2613 -1092
- package/dist/catalogue/registry.d.ts.map +1 -1
- package/dist/catalogue/registry.js +18 -0
- package/dist/catalogue/registry.js.map +1 -1
- package/dist/catalogue/specimens.d.ts.map +1 -1
- package/dist/catalogue/specimens.js +130 -10
- package/dist/catalogue/specimens.js.map +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +10 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/foundations/tokens.css +34 -26
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/interactions/ket-table/demo.d.ts +4 -0
- package/dist/interactions/ket-table/demo.d.ts.map +1 -0
- package/dist/interactions/ket-table/demo.js +147 -0
- package/dist/interactions/ket-table/demo.js.map +1 -0
- package/dist/interactions/ket-table/index.d.ts +199 -0
- package/dist/interactions/ket-table/index.d.ts.map +1 -0
- package/dist/interactions/ket-table/index.js +376 -0
- package/dist/interactions/ket-table/index.js.map +1 -0
- package/dist/interactions/ket-table/styles.css +282 -0
- package/dist/interactions/lightbox/demo.d.ts +3 -0
- package/dist/interactions/lightbox/demo.d.ts.map +1 -0
- package/dist/interactions/lightbox/demo.js +21 -0
- package/dist/interactions/lightbox/demo.js.map +1 -0
- package/dist/interactions/lightbox/index.d.ts +58 -0
- package/dist/interactions/lightbox/index.d.ts.map +1 -0
- package/dist/interactions/lightbox/index.js +220 -0
- package/dist/interactions/lightbox/index.js.map +1 -0
- package/dist/interactions/lightbox/styles.css +153 -0
- package/dist/interactions/relation-select/demo.d.ts +3 -0
- package/dist/interactions/relation-select/demo.d.ts.map +1 -0
- package/dist/interactions/relation-select/demo.js +36 -0
- package/dist/interactions/relation-select/demo.js.map +1 -0
- package/dist/interactions/relation-select/index.d.ts +87 -0
- package/dist/interactions/relation-select/index.d.ts.map +1 -0
- package/dist/interactions/relation-select/index.js +307 -0
- package/dist/interactions/relation-select/index.js.map +1 -0
- package/dist/interactions/relation-select/styles.css +311 -0
- package/dist/interactions/reorder-list/index.d.ts +23 -0
- package/dist/interactions/reorder-list/index.d.ts.map +1 -0
- package/dist/interactions/reorder-list/index.js +15 -0
- package/dist/interactions/reorder-list/index.js.map +1 -0
- package/dist/interactions/reorder-list/runtime.d.ts +5 -0
- package/dist/interactions/reorder-list/runtime.d.ts.map +1 -0
- package/dist/interactions/reorder-list/runtime.js +85 -0
- package/dist/interactions/reorder-list/runtime.js.map +1 -0
- package/dist/interactions/reorder-list/styles.css +33 -0
- package/dist/interactions/search-filter/demo.d.ts +3 -0
- package/dist/interactions/search-filter/demo.d.ts.map +1 -0
- package/dist/interactions/search-filter/demo.js +74 -0
- package/dist/interactions/search-filter/demo.js.map +1 -0
- package/dist/interactions/search-filter/index.d.ts +153 -0
- package/dist/interactions/search-filter/index.d.ts.map +1 -0
- package/dist/interactions/search-filter/index.js +824 -0
- package/dist/interactions/search-filter/index.js.map +1 -0
- package/dist/interactions/search-filter/styles.css +580 -0
- package/dist/layouts/app-navigation/index.d.ts +11 -5
- package/dist/layouts/app-navigation/index.d.ts.map +1 -1
- package/dist/layouts/app-navigation/index.js +5 -3
- package/dist/layouts/app-navigation/index.js.map +1 -1
- package/dist/layouts/app-navigation/styles.css +50 -0
- package/dist/layouts/page-identity/index.d.ts +2 -0
- package/dist/layouts/page-identity/index.d.ts.map +1 -1
- package/dist/layouts/page-identity/index.js +2 -2
- package/dist/layouts/page-identity/index.js.map +1 -1
- package/dist/layouts/shell/index.d.ts +34 -1
- package/dist/layouts/shell/index.d.ts.map +1 -1
- package/dist/layouts/shell/index.js +26 -2
- package/dist/layouts/shell/index.js.map +1 -1
- package/dist/layouts/shell/styles.css +283 -0
- package/dist/patterns/data-table/styles.css +1 -1
- package/dist/patterns/list-page/index.d.ts +10 -3
- package/dist/patterns/list-page/index.d.ts.map +1 -1
- package/dist/patterns/list-page/index.js +10 -6
- package/dist/patterns/list-page/index.js.map +1 -1
- package/dist/patterns/list-page/responsive.css +1 -5
- package/dist/patterns/list-page/styles.css +25 -3
- package/dist/patterns/modal-sheet/index.d.ts +2 -0
- package/dist/patterns/modal-sheet/index.d.ts.map +1 -1
- package/dist/patterns/modal-sheet/index.js +3 -1
- package/dist/patterns/modal-sheet/index.js.map +1 -1
- package/dist/patterns/record-form/styles.css +34 -0
- package/dist/primitives/actions/index.d.ts +4 -0
- package/dist/primitives/actions/index.d.ts.map +1 -1
- package/dist/primitives/actions/index.js +2 -2
- package/dist/primitives/actions/index.js.map +1 -1
- package/dist/primitives/actions/styles.css +9 -0
- package/dist/primitives/feedback/styles.css +4 -1
- package/dist/primitives/status/index.d.ts +12 -1
- package/dist/primitives/status/index.d.ts.map +1 -1
- package/dist/primitives/status/index.js +16 -1
- package/dist/primitives/status/index.js.map +1 -1
- package/dist/primitives/status/styles.css +28 -0
- package/dist/record/formatted-values/index.d.ts +2 -1
- package/dist/record/formatted-values/index.d.ts.map +1 -1
- package/dist/record/formatted-values/index.js +2 -1
- package/dist/record/formatted-values/index.js.map +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +129 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@ketvietlab/ketjs-view/jsx-runtime";
|
|
2
|
+
import { ModalSheet } from '../../patterns/modal-sheet/index.js';
|
|
3
|
+
import { each, signal } from '@ketvietlab/ketjs-view';
|
|
4
|
+
export const HOOKS = [
|
|
5
|
+
'search-filter',
|
|
6
|
+
'search-filter-bar',
|
|
7
|
+
'search-filter-section-toggle',
|
|
8
|
+
'search-filter-sheet',
|
|
9
|
+
'search-filter-clear',
|
|
10
|
+
'search-filter-icon',
|
|
11
|
+
'search-filter-field',
|
|
12
|
+
'search-filter-input',
|
|
13
|
+
'search-filter-facets',
|
|
14
|
+
'search-filter-facet',
|
|
15
|
+
'search-filter-facet-remove',
|
|
16
|
+
'search-filter-suggestions',
|
|
17
|
+
'search-filter-suggestion',
|
|
18
|
+
'search-filter-toggle',
|
|
19
|
+
'search-filter-columns',
|
|
20
|
+
'search-filter-column',
|
|
21
|
+
'search-filter-column-title',
|
|
22
|
+
'search-filter-empty',
|
|
23
|
+
'search-filter-grouping',
|
|
24
|
+
'search-filter-grouping-head',
|
|
25
|
+
'search-filter-grouping-list',
|
|
26
|
+
'search-filter-grouping-item',
|
|
27
|
+
'search-filter-grouping-order',
|
|
28
|
+
'search-filter-grouping-label',
|
|
29
|
+
'search-filter-grouping-actions',
|
|
30
|
+
'search-filter-grouping-action',
|
|
31
|
+
'search-filter-grouping-clear',
|
|
32
|
+
'search-filter-grouping-add-label',
|
|
33
|
+
'custom-filter',
|
|
34
|
+
'custom-filter-row',
|
|
35
|
+
'custom-filter-add',
|
|
36
|
+
'custom-group-by',
|
|
37
|
+
'favorite-list',
|
|
38
|
+
'favorite-item',
|
|
39
|
+
'favorite-item-default',
|
|
40
|
+
'favorite-item-remove',
|
|
41
|
+
'favorite-save',
|
|
42
|
+
'favorite-save-toggle',
|
|
43
|
+
'favorite-save-row',
|
|
44
|
+
];
|
|
45
|
+
const defaultOperators = {
|
|
46
|
+
text: ['contains', 'notContains', 'equals', 'notEquals', 'startsWith', 'isSet', 'isNotSet'],
|
|
47
|
+
number: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],
|
|
48
|
+
boolean: ['isTrue', 'isFalse'],
|
|
49
|
+
selection: ['equals', 'notEquals', 'anyOf', 'isSet', 'isNotSet'],
|
|
50
|
+
reference: ['equals', 'notEquals', 'anyOf', 'isSet', 'isNotSet'],
|
|
51
|
+
date: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],
|
|
52
|
+
datetime: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],
|
|
53
|
+
};
|
|
54
|
+
const operatorLabels = {
|
|
55
|
+
contains: 'contains',
|
|
56
|
+
notContains: 'does not contain',
|
|
57
|
+
equals: 'is',
|
|
58
|
+
notEquals: 'is not',
|
|
59
|
+
startsWith: 'starts with',
|
|
60
|
+
gt: 'is greater than',
|
|
61
|
+
gte: 'is greater than or equal to',
|
|
62
|
+
lt: 'is less than',
|
|
63
|
+
lte: 'is less than or equal to',
|
|
64
|
+
between: 'is between',
|
|
65
|
+
anyOf: 'is any of',
|
|
66
|
+
isTrue: 'is true',
|
|
67
|
+
isFalse: 'is false',
|
|
68
|
+
isSet: 'is set',
|
|
69
|
+
isNotSet: 'is not set',
|
|
70
|
+
};
|
|
71
|
+
/** An operator that stands on its own and takes no value ("is set", "is true", …). */
|
|
72
|
+
const isValuelessOperator = (operator) => operator === 'isTrue' || operator === 'isFalse' || operator === 'isSet' || operator === 'isNotSet';
|
|
73
|
+
/**
|
|
74
|
+
* The rule a free-text "Search <field> for: …" suggestion stands for, or null
|
|
75
|
+
* when the text cannot be one. Every suggestion must be a filter the server
|
|
76
|
+
* accepts, so a field whose values free text cannot name — a choice, a record,
|
|
77
|
+
* a date — is left to the custom-filter editor instead of being offered here.
|
|
78
|
+
*/
|
|
79
|
+
const suggestedRule = (field, text) => {
|
|
80
|
+
if (field.type === 'text')
|
|
81
|
+
return { field: field.value, operator: 'contains', value: text };
|
|
82
|
+
// Whole numbers only: `1.000` is a thousand to a Vietnamese reader and one to `Number`.
|
|
83
|
+
if (field.type === 'number' && /^-?\d+$/.test(text))
|
|
84
|
+
return { field: field.value, operator: 'equals', value: text };
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
/** Shared by SSR and interactive chips; values remain data and choices supply display labels. */
|
|
88
|
+
export const searchFilterRuleLabel = (options) => {
|
|
89
|
+
const operator = options.operatorLabels?.[options.operator] ?? operatorLabels[options.operator];
|
|
90
|
+
const prefix = `${options.fieldLabel} ${operator}`;
|
|
91
|
+
if (isValuelessOperator(options.operator) || options.value == null || options.value === '')
|
|
92
|
+
return prefix;
|
|
93
|
+
const values = Array.isArray(options.value)
|
|
94
|
+
? options.value
|
|
95
|
+
: options.operator === 'anyOf' || options.operator === 'between'
|
|
96
|
+
? String(options.value)
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((part) => part.trim())
|
|
99
|
+
: [options.value];
|
|
100
|
+
const value = values
|
|
101
|
+
.map((part) => options.choices?.find((choice) => choice.value === String(part))?.label ?? String(part))
|
|
102
|
+
.join(', ');
|
|
103
|
+
return `${prefix}: ${value}`;
|
|
104
|
+
};
|
|
105
|
+
const string = (value) => (value == null ? '' : String(value));
|
|
106
|
+
/**
|
|
107
|
+
* A function call that failed. `retryable` when it never reached a verdict — the
|
|
108
|
+
* network dropped or the server faulted — so the same request may yet succeed;
|
|
109
|
+
* a refusal (4xx) would only be refused again.
|
|
110
|
+
*/
|
|
111
|
+
class ApiError extends Error {
|
|
112
|
+
retryable;
|
|
113
|
+
constructor(message, retryable) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.retryable = retryable;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const callApi = async (name, input) => {
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
credentials: 'same-origin',
|
|
124
|
+
headers: { 'content-type': 'application/json' },
|
|
125
|
+
body: JSON.stringify(input),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (caught) {
|
|
129
|
+
throw new ApiError(caught instanceof Error ? caught.message : 'network error', true);
|
|
130
|
+
}
|
|
131
|
+
// A proxy in front of a faulting server answers with HTML, not the JSON envelope.
|
|
132
|
+
const payload = (await response.json().catch(() => ({})));
|
|
133
|
+
if (!response.ok || payload.ok === false) {
|
|
134
|
+
const domainError = payload.errors?.[0];
|
|
135
|
+
throw new ApiError(string(domainError?.message ?? payload.message ?? `HTTP ${response.status}`), response.status >= 500);
|
|
136
|
+
}
|
|
137
|
+
return payload.value;
|
|
138
|
+
};
|
|
139
|
+
const flatten = (options) => options.flatMap((option) => (option.options?.length ? flatten(option.options) : [option]));
|
|
140
|
+
export function createSearchFilterView(props) {
|
|
141
|
+
const { config } = props;
|
|
142
|
+
const labels = {
|
|
143
|
+
clearFilters: 'Clear filters',
|
|
144
|
+
close: 'Close',
|
|
145
|
+
favoriteCancel: 'Cancel',
|
|
146
|
+
favoriteError: 'Could not save changes to saved searches',
|
|
147
|
+
valueFrom: 'From',
|
|
148
|
+
valueTo: 'To',
|
|
149
|
+
groupByApplied: config.labels.groupBy,
|
|
150
|
+
groupByAdd: config.labels.customGroupByPlaceholder,
|
|
151
|
+
groupByClear: config.labels.clear,
|
|
152
|
+
groupByMoveEarlier: 'Move earlier',
|
|
153
|
+
groupByMoveLater: 'Move later',
|
|
154
|
+
...config.labels,
|
|
155
|
+
};
|
|
156
|
+
const operatorLabel = (operator) => labels.operatorLabels?.[operator] ?? operatorLabels[operator];
|
|
157
|
+
const capabilities = config.capabilities ?? {};
|
|
158
|
+
const manager = config.manager;
|
|
159
|
+
const hasGrouping = capabilities.groupBy !== false && config.groupBy.length > 0;
|
|
160
|
+
const groupByOptions = flatten(config.groupBy);
|
|
161
|
+
const fieldSuggestions = (text) => config.customFilterFields.filter((field) => suggestedRule(field, text));
|
|
162
|
+
const facets = signal(config.facets);
|
|
163
|
+
const favorites = signal(config.favorites);
|
|
164
|
+
const hasFavorites = () => capabilities.favorites !== false &&
|
|
165
|
+
Boolean(favorites().length || manager?.saveFavoriteFunction || config.favoriteHref);
|
|
166
|
+
const customFilterRules = signal(Object.fromEntries((config.customFilters ?? []).map((rule) => [
|
|
167
|
+
rule.id,
|
|
168
|
+
{ field: rule.field, operator: rule.operator, value: rule.value },
|
|
169
|
+
])));
|
|
170
|
+
const query = signal(config.query ?? '');
|
|
171
|
+
const suggestionsOpen = signal(false);
|
|
172
|
+
const customFilterField = signal('');
|
|
173
|
+
const customFilterOperator = signal('');
|
|
174
|
+
const customFilterValue = signal('');
|
|
175
|
+
const menuOpen = signal(false);
|
|
176
|
+
const panel = signal('filter');
|
|
177
|
+
const sheetOpen = signal(false);
|
|
178
|
+
const savingFavorite = signal(false);
|
|
179
|
+
const customFilterEnd = signal('');
|
|
180
|
+
let sheetOpener = null;
|
|
181
|
+
let menuOpener = null;
|
|
182
|
+
const focusControl = (selector) => {
|
|
183
|
+
const control = [...(mountedRoot?.querySelectorAll(selector) ?? [])][0];
|
|
184
|
+
if (typeof HTMLElement !== 'undefined' && control instanceof HTMLElement)
|
|
185
|
+
control.focus();
|
|
186
|
+
};
|
|
187
|
+
const closeFavoriteForm = () => {
|
|
188
|
+
savingFavorite.set(false);
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
if (menuOpen() || sheetOpen())
|
|
191
|
+
focusControl('[data-ui="favorite-save-toggle"]');
|
|
192
|
+
else
|
|
193
|
+
menuOpener?.focus();
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const closeMenu = () => {
|
|
197
|
+
savingFavorite.set(false);
|
|
198
|
+
menuOpen.set(false);
|
|
199
|
+
};
|
|
200
|
+
const dismissDisclosure = (event) => {
|
|
201
|
+
const disclosure = event.target instanceof Element ? event.target.closest('details[data-ui="disclosure"][open]') : null;
|
|
202
|
+
if (!(disclosure instanceof HTMLDetailsElement))
|
|
203
|
+
return false;
|
|
204
|
+
disclosure.open = false;
|
|
205
|
+
const summary = disclosure.querySelector('summary');
|
|
206
|
+
if (summary instanceof HTMLElement)
|
|
207
|
+
summary.focus();
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
const dialogElement = () => {
|
|
213
|
+
const candidate = [...(mountedRoot?.querySelectorAll('[data-ui="search-filter-sheet"]') ?? [])][0];
|
|
214
|
+
return typeof HTMLDialogElement !== 'undefined' && candidate instanceof HTMLDialogElement
|
|
215
|
+
? candidate
|
|
216
|
+
: null;
|
|
217
|
+
};
|
|
218
|
+
const closeSheet = () => {
|
|
219
|
+
closeMenu();
|
|
220
|
+
dialogElement()?.close();
|
|
221
|
+
sheetOpen.set(false);
|
|
222
|
+
sheetOpener?.focus();
|
|
223
|
+
};
|
|
224
|
+
const openPanel = (kind) => {
|
|
225
|
+
if (panel() !== kind)
|
|
226
|
+
savingFavorite.set(false);
|
|
227
|
+
panel.set(kind);
|
|
228
|
+
menuOpen.set(true);
|
|
229
|
+
suggestionsOpen.set(false);
|
|
230
|
+
};
|
|
231
|
+
const togglePanel = (event) => {
|
|
232
|
+
menuOpener = event.currentTarget;
|
|
233
|
+
const switching = menuOpen() && panel() !== 'filter';
|
|
234
|
+
if (switching)
|
|
235
|
+
savingFavorite.set(false);
|
|
236
|
+
panel.set('filter');
|
|
237
|
+
suggestionsOpen.set(false);
|
|
238
|
+
if (!window.matchMedia('(max-width: 640px)').matches) {
|
|
239
|
+
if (switching)
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
sheetOpener = event.currentTarget;
|
|
245
|
+
closeMenu();
|
|
246
|
+
sheetOpen.set(true);
|
|
247
|
+
queueMicrotask(() => {
|
|
248
|
+
const dialog = dialogElement();
|
|
249
|
+
if (dialog && !dialog.open) {
|
|
250
|
+
dialog.showModal();
|
|
251
|
+
focusControl('[data-ui="search-filter-sheet"] [data-ui="modal-close"]');
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
const saveFavoriteName = signal('');
|
|
256
|
+
const saveFavoriteDefault = signal(false);
|
|
257
|
+
const pending = signal(false);
|
|
258
|
+
const error = signal('');
|
|
259
|
+
// What the rendered list actually shows. Chips change before the request goes
|
|
260
|
+
// out, so a request that fails must put them back here rather than leave a
|
|
261
|
+
// filter on screen that never applied.
|
|
262
|
+
let settled = { facets: facets(), rules: customFilterRules() };
|
|
263
|
+
const retryDraft = signal(null);
|
|
264
|
+
let mountedRoot = null;
|
|
265
|
+
// `value={query()}` only writes the attribute, which a field the reader has
|
|
266
|
+
// typed into no longer displays — the live property is what they see.
|
|
267
|
+
const setQuery = (value) => {
|
|
268
|
+
query.set(value);
|
|
269
|
+
if (!mountedRoot)
|
|
270
|
+
return;
|
|
271
|
+
for (const input of mountedRoot.querySelectorAll('[data-ui="search-filter-input"]'))
|
|
272
|
+
if (input instanceof HTMLInputElement && input.value !== value)
|
|
273
|
+
input.value = value;
|
|
274
|
+
};
|
|
275
|
+
const isActive = (kind, id) => facets().some((facet) => facet.type === kind && facet.id === id);
|
|
276
|
+
const countOf = (kind) => facets().filter((facet) => facet.type === kind).length;
|
|
277
|
+
const activeFavoriteId = () => facets().find((facet) => facet.type === 'favorite')?.id ?? '';
|
|
278
|
+
const groupByFacets = () => facets().filter((facet) => facet.type === 'groupBy');
|
|
279
|
+
const operatorsFor = (fieldValue) => {
|
|
280
|
+
const field = config.customFilterFields.find((entry) => entry.value === fieldValue);
|
|
281
|
+
return field ? defaultOperators[field.type] : [];
|
|
282
|
+
};
|
|
283
|
+
const applyPayload = () => ({
|
|
284
|
+
...(manager?.applyInput ?? {}),
|
|
285
|
+
query: query(),
|
|
286
|
+
facets: facets(),
|
|
287
|
+
filters: facets()
|
|
288
|
+
.filter((facet) => facet.type === 'filter' && !customFilterRules()[facet.id])
|
|
289
|
+
.map((facet) => facet.id),
|
|
290
|
+
groupBy: facets()
|
|
291
|
+
.filter((facet) => facet.type === 'groupBy')
|
|
292
|
+
.map((facet) => facet.id),
|
|
293
|
+
favoriteId: activeFavoriteId() || null,
|
|
294
|
+
customFilters: Object.entries(customFilterRules()).map(([id, rule]) => ({ id, ...rule })),
|
|
295
|
+
});
|
|
296
|
+
let applyVersion = 0;
|
|
297
|
+
let navigationRequest = null;
|
|
298
|
+
let startingNavigation = false;
|
|
299
|
+
/**
|
|
300
|
+
* Sends the current facets. `consumed` is text the triggering action moved
|
|
301
|
+
* out of the search field into a chip; a failed request hands it back.
|
|
302
|
+
*/
|
|
303
|
+
const apply = async (consumed = '') => {
|
|
304
|
+
closeMenu();
|
|
305
|
+
if (!manager?.applyFunction)
|
|
306
|
+
return;
|
|
307
|
+
const version = ++applyVersion;
|
|
308
|
+
navigationRequest?.abort();
|
|
309
|
+
const request = new AbortController();
|
|
310
|
+
navigationRequest = request;
|
|
311
|
+
const attempted = { facets: facets(), rules: customFilterRules() };
|
|
312
|
+
pending.set(true);
|
|
313
|
+
error.set('');
|
|
314
|
+
retryDraft.set(null);
|
|
315
|
+
try {
|
|
316
|
+
const value = (await callApi(manager.applyFunction, applyPayload()));
|
|
317
|
+
if (version !== applyVersion)
|
|
318
|
+
return;
|
|
319
|
+
const body = document.getElementById(manager.bodyId);
|
|
320
|
+
if (body && typeof value?.html === 'string')
|
|
321
|
+
body.innerHTML = value.html;
|
|
322
|
+
if (typeof value?.href === 'string' && typeof value.html !== 'string') {
|
|
323
|
+
// The shell owns routing, history and island reconciliation. A standalone
|
|
324
|
+
// consumer without that shell keeps the native navigation fallback.
|
|
325
|
+
let navigation;
|
|
326
|
+
const detail = {
|
|
327
|
+
id: props.id,
|
|
328
|
+
href: value.href,
|
|
329
|
+
signal: request.signal,
|
|
330
|
+
respondWith: (result) => {
|
|
331
|
+
navigation = result;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
startingNavigation = true;
|
|
335
|
+
try {
|
|
336
|
+
document.dispatchEvent?.(new CustomEvent('ket:search-filter-navigate', { detail }));
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
startingNavigation = false;
|
|
340
|
+
}
|
|
341
|
+
if (navigation)
|
|
342
|
+
await navigation;
|
|
343
|
+
else
|
|
344
|
+
window.location.assign(value.href);
|
|
345
|
+
}
|
|
346
|
+
else if (typeof value?.href === 'string') {
|
|
347
|
+
history.pushState(null, '', value.href);
|
|
348
|
+
}
|
|
349
|
+
if (version === applyVersion)
|
|
350
|
+
settled = attempted;
|
|
351
|
+
}
|
|
352
|
+
catch (caught) {
|
|
353
|
+
if (version !== applyVersion)
|
|
354
|
+
return;
|
|
355
|
+
facets.set(settled.facets);
|
|
356
|
+
customFilterRules.set(settled.rules);
|
|
357
|
+
if (consumed && !query())
|
|
358
|
+
setQuery(consumed);
|
|
359
|
+
if (caught instanceof Error && 'retryable' in caught && caught.retryable === true)
|
|
360
|
+
retryDraft.set({ ...attempted, consumed });
|
|
361
|
+
if (caught instanceof Error && caught.name === 'AbortError')
|
|
362
|
+
return;
|
|
363
|
+
// The server's message is an English diagnostic, not copy for the reader.
|
|
364
|
+
error.set(labels.applyError);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
if (version === applyVersion)
|
|
368
|
+
pending.set(false);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
/** Sends the failed request again, chips and all, as the reader last asked for it. */
|
|
372
|
+
const retry = () => {
|
|
373
|
+
const draft = retryDraft();
|
|
374
|
+
if (!draft)
|
|
375
|
+
return;
|
|
376
|
+
facets.set(draft.facets);
|
|
377
|
+
customFilterRules.set(draft.rules);
|
|
378
|
+
if (draft.consumed && query() === draft.consumed)
|
|
379
|
+
setQuery('');
|
|
380
|
+
void apply(draft.consumed);
|
|
381
|
+
};
|
|
382
|
+
const clearFavorite = () => {
|
|
383
|
+
if (activeFavoriteId())
|
|
384
|
+
facets.set(facets().filter((facet) => facet.type !== 'favorite'));
|
|
385
|
+
};
|
|
386
|
+
const removeFacet = (facet) => {
|
|
387
|
+
if (facet.type !== 'favorite')
|
|
388
|
+
clearFavorite();
|
|
389
|
+
facets.set(facets().filter((held) => held.id !== facet.id));
|
|
390
|
+
if (customFilterRules()[facet.id]) {
|
|
391
|
+
const next = { ...customFilterRules() };
|
|
392
|
+
delete next[facet.id];
|
|
393
|
+
customFilterRules.set(next);
|
|
394
|
+
}
|
|
395
|
+
void apply();
|
|
396
|
+
};
|
|
397
|
+
const toggleFilter = (option) => {
|
|
398
|
+
clearFavorite();
|
|
399
|
+
if (isActive('filter', option.id)) {
|
|
400
|
+
removeFacet({ id: option.id, type: 'filter', label: option.label });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
facets.set([...facets(), { id: option.id, type: 'filter', label: option.label }]);
|
|
404
|
+
void apply();
|
|
405
|
+
};
|
|
406
|
+
const toggleGroupBy = (option) => {
|
|
407
|
+
if (!isActive('groupBy', option.id) && groupByFacets().length >= (config.maxGroupBy ?? Infinity))
|
|
408
|
+
return;
|
|
409
|
+
clearFavorite();
|
|
410
|
+
if (isActive('groupBy', option.id)) {
|
|
411
|
+
removeFacet({ id: option.id, type: 'groupBy', label: option.label });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
facets.set([...facets(), { id: option.id, type: 'groupBy', label: option.label }]);
|
|
415
|
+
void apply();
|
|
416
|
+
};
|
|
417
|
+
const clearFilters = () => {
|
|
418
|
+
facets.set(facets().filter((facet) => facet.type === 'field' || facet.type === 'groupBy'));
|
|
419
|
+
customFilterRules.set({});
|
|
420
|
+
void apply();
|
|
421
|
+
};
|
|
422
|
+
const selectedField = () => config.customFilterFields.find((entry) => entry.value === customFilterField());
|
|
423
|
+
const canAddRule = () => {
|
|
424
|
+
const field = selectedField();
|
|
425
|
+
const operator = customFilterOperator();
|
|
426
|
+
if (!field || !operatorsFor(field.value).includes(operator))
|
|
427
|
+
return false;
|
|
428
|
+
if (isValuelessOperator(operator))
|
|
429
|
+
return true;
|
|
430
|
+
if (!customFilterValue().trim())
|
|
431
|
+
return false;
|
|
432
|
+
if (operator === 'between' && !customFilterEnd().trim())
|
|
433
|
+
return false;
|
|
434
|
+
const values = operator === 'between'
|
|
435
|
+
? [customFilterValue(), customFilterEnd()]
|
|
436
|
+
: operator === 'anyOf'
|
|
437
|
+
? customFilterValue().split(',')
|
|
438
|
+
: [customFilterValue()];
|
|
439
|
+
if (field.type === 'number' && values.some((value) => !Number.isFinite(Number(value))))
|
|
440
|
+
return false;
|
|
441
|
+
if (field.choices && values.some((value) => !field.choices.some((choice) => choice.value === value)))
|
|
442
|
+
return false;
|
|
443
|
+
return true;
|
|
444
|
+
};
|
|
445
|
+
const addCustomFilter = () => {
|
|
446
|
+
const field = config.customFilterFields.find((entry) => entry.value === customFilterField());
|
|
447
|
+
const operator = customFilterOperator();
|
|
448
|
+
if (!field || !canAddRule())
|
|
449
|
+
return;
|
|
450
|
+
const value = isValuelessOperator(operator)
|
|
451
|
+
? ''
|
|
452
|
+
: operator === 'between'
|
|
453
|
+
? `${customFilterValue().trim()},${customFilterEnd().trim()}`
|
|
454
|
+
: customFilterValue().trim();
|
|
455
|
+
const label = searchFilterRuleLabel({
|
|
456
|
+
fieldLabel: field.label,
|
|
457
|
+
operator,
|
|
458
|
+
value,
|
|
459
|
+
choices: field.choices,
|
|
460
|
+
operatorLabels: labels.operatorLabels,
|
|
461
|
+
});
|
|
462
|
+
clearFavorite();
|
|
463
|
+
const id = `custom-filter:${crypto.randomUUID()}`;
|
|
464
|
+
customFilterRules.set({ ...customFilterRules(), [id]: { field: field.value, operator, value } });
|
|
465
|
+
facets.set([...facets(), { id, type: 'filter', label }]);
|
|
466
|
+
customFilterField.set('');
|
|
467
|
+
customFilterOperator.set('');
|
|
468
|
+
customFilterValue.set('');
|
|
469
|
+
customFilterEnd.set('');
|
|
470
|
+
void apply();
|
|
471
|
+
};
|
|
472
|
+
const handleSearchInput = (event) => {
|
|
473
|
+
if (!(event.currentTarget instanceof HTMLInputElement))
|
|
474
|
+
return;
|
|
475
|
+
query.set(event.currentTarget.value);
|
|
476
|
+
suggestionsOpen.set(event.currentTarget.value.trim() !== '');
|
|
477
|
+
};
|
|
478
|
+
const selectGenericSuggestion = () => {
|
|
479
|
+
const value = query().trim();
|
|
480
|
+
if (!value)
|
|
481
|
+
return;
|
|
482
|
+
clearFavorite();
|
|
483
|
+
facets.set([
|
|
484
|
+
...facets().filter((facet) => facet.type !== 'field'),
|
|
485
|
+
{ id: `query:${crypto.randomUUID()}`, type: 'field', label: value },
|
|
486
|
+
]);
|
|
487
|
+
setQuery('');
|
|
488
|
+
suggestionsOpen.set(false);
|
|
489
|
+
void apply(value);
|
|
490
|
+
};
|
|
491
|
+
const selectFieldSuggestion = (field) => {
|
|
492
|
+
const value = query().trim();
|
|
493
|
+
const rule = value ? suggestedRule(field, value) : null;
|
|
494
|
+
if (!rule)
|
|
495
|
+
return;
|
|
496
|
+
clearFavorite();
|
|
497
|
+
const id = `custom-filter:${crypto.randomUUID()}`;
|
|
498
|
+
customFilterRules.set({ ...customFilterRules(), [id]: rule });
|
|
499
|
+
facets.set([
|
|
500
|
+
...facets(),
|
|
501
|
+
{
|
|
502
|
+
id,
|
|
503
|
+
type: 'filter',
|
|
504
|
+
label: searchFilterRuleLabel({
|
|
505
|
+
fieldLabel: field.label,
|
|
506
|
+
operator: rule.operator,
|
|
507
|
+
value: rule.value,
|
|
508
|
+
choices: field.choices,
|
|
509
|
+
operatorLabels: labels.operatorLabels,
|
|
510
|
+
}),
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
setQuery('');
|
|
514
|
+
suggestionsOpen.set(false);
|
|
515
|
+
void apply(value);
|
|
516
|
+
};
|
|
517
|
+
const handleSearchKeydown = (event) => {
|
|
518
|
+
if (!(event instanceof KeyboardEvent))
|
|
519
|
+
return;
|
|
520
|
+
if (event.key === 'Enter') {
|
|
521
|
+
event.preventDefault();
|
|
522
|
+
selectGenericSuggestion();
|
|
523
|
+
}
|
|
524
|
+
else if (event.key === 'Escape') {
|
|
525
|
+
suggestionsOpen.set(false);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const applyFavorite = (favorite) => {
|
|
529
|
+
facets.set([
|
|
530
|
+
...facets().filter((held) => held.type !== 'favorite'),
|
|
531
|
+
{ id: favorite.id, type: 'favorite', label: favorite.label },
|
|
532
|
+
]);
|
|
533
|
+
void apply();
|
|
534
|
+
};
|
|
535
|
+
const moveGroupBy = (facet, direction) => {
|
|
536
|
+
const grouped = groupByFacets();
|
|
537
|
+
const from = grouped.findIndex((entry) => entry.id === facet.id);
|
|
538
|
+
const to = from + direction;
|
|
539
|
+
if (from < 0 || to < 0 || to >= grouped.length)
|
|
540
|
+
return;
|
|
541
|
+
const next = [...grouped];
|
|
542
|
+
[next[from], next[to]] = [next[to], next[from]];
|
|
543
|
+
clearFavorite();
|
|
544
|
+
facets.set([...facets().filter((entry) => entry.type !== 'groupBy'), ...next]);
|
|
545
|
+
void apply();
|
|
546
|
+
};
|
|
547
|
+
const clearGroupBy = () => {
|
|
548
|
+
if (!groupByFacets().length)
|
|
549
|
+
return;
|
|
550
|
+
clearFavorite();
|
|
551
|
+
facets.set(facets().filter((facet) => facet.type !== 'groupBy'));
|
|
552
|
+
void apply();
|
|
553
|
+
};
|
|
554
|
+
const saveFavorite = async (event) => {
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
const name = saveFavoriteName().trim();
|
|
557
|
+
if (!name || !manager?.saveFavoriteFunction)
|
|
558
|
+
return;
|
|
559
|
+
pending.set(true);
|
|
560
|
+
error.set('');
|
|
561
|
+
retryDraft.set(null);
|
|
562
|
+
try {
|
|
563
|
+
const value = (await callApi(manager.saveFavoriteFunction, {
|
|
564
|
+
name,
|
|
565
|
+
isDefault: saveFavoriteDefault(),
|
|
566
|
+
state: applyPayload(),
|
|
567
|
+
}));
|
|
568
|
+
const isDefault = saveFavoriteDefault();
|
|
569
|
+
const favorite = {
|
|
570
|
+
id: string(value?.id) || crypto.randomUUID(),
|
|
571
|
+
label: name,
|
|
572
|
+
isDefault,
|
|
573
|
+
active: true,
|
|
574
|
+
};
|
|
575
|
+
favorites.set([
|
|
576
|
+
...favorites().map((entry) => (isDefault ? { ...entry, isDefault: false } : entry)),
|
|
577
|
+
favorite,
|
|
578
|
+
]);
|
|
579
|
+
applyFavorite(favorite);
|
|
580
|
+
saveFavoriteName.set('');
|
|
581
|
+
saveFavoriteDefault.set(false);
|
|
582
|
+
closeFavoriteForm();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
error.set(labels.favoriteError);
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
pending.set(false);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const removeFavorite = async (favorite) => {
|
|
592
|
+
if (!manager?.deleteFavoriteFunction)
|
|
593
|
+
return;
|
|
594
|
+
pending.set(true);
|
|
595
|
+
error.set('');
|
|
596
|
+
retryDraft.set(null);
|
|
597
|
+
try {
|
|
598
|
+
await callApi(manager.deleteFavoriteFunction, { ...(manager.applyInput ?? {}), id: favorite.id });
|
|
599
|
+
favorites.set(favorites().filter((entry) => entry.id !== favorite.id));
|
|
600
|
+
if (activeFavoriteId() === favorite.id)
|
|
601
|
+
removeFacet({ id: favorite.id, type: 'favorite', label: favorite.label });
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
error.set(labels.favoriteError);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
pending.set(false);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const setDefaultFavorite = async (favorite) => {
|
|
611
|
+
if (!manager?.setDefaultFavoriteFunction)
|
|
612
|
+
return;
|
|
613
|
+
pending.set(true);
|
|
614
|
+
error.set('');
|
|
615
|
+
retryDraft.set(null);
|
|
616
|
+
try {
|
|
617
|
+
await callApi(manager.setDefaultFavoriteFunction, { ...(manager.applyInput ?? {}), id: favorite.id });
|
|
618
|
+
favorites.set(favorites().map((entry) => ({ ...entry, isDefault: entry.id === favorite.id })));
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
error.set(labels.favoriteError);
|
|
622
|
+
}
|
|
623
|
+
finally {
|
|
624
|
+
pending.set(false);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const facetChip = (facet) => (_jsxs("li", { "data-ui": "search-filter-facet", "data-type": facet.type, children: [_jsxs("span", { children: [facet.type === 'field'
|
|
628
|
+
? `${labels.searchGenericLabel}: `
|
|
629
|
+
: facet.type === 'groupBy'
|
|
630
|
+
? `${labels.groupBy}: `
|
|
631
|
+
: '', facet.label] }), _jsx("button", { "data-ui": "search-filter-facet-remove", type: "button", "aria-label": `${labels.clear}: ${facet.label}`, title: labels.clear, onClick: () => removeFacet(facet), children: "\u00D7" })] }));
|
|
632
|
+
const filterMenuItem = (option, previous) => {
|
|
633
|
+
const divider = previous && previous.group !== option.group ? _jsx("hr", { "data-ui": "menu-separator" }) : null;
|
|
634
|
+
if (option.options?.length)
|
|
635
|
+
return (_jsxs(_Fragment, { children: [divider, _jsxs("details", { "data-ui": "disclosure", children: [_jsx("summary", { "data-ui": "disclosure-summary", children: option.label }), _jsx("div", { "data-ui": "disclosure-body", children: each(option.options, (entry) => entry.id, (entry, index) => filterMenuItem(entry, index === 0 ? null : option.options[index - 1])) })] })] }));
|
|
636
|
+
const active = isActive('filter', option.id);
|
|
637
|
+
return (_jsxs(_Fragment, { children: [divider, _jsxs("button", { "data-ui": "menu-item", type: "button", "aria-pressed": String(active), onClick: () => toggleFilter(option), children: [_jsx("span", { "data-ui": "menu-item-check", "aria-hidden": "true", children: active ? '✓' : '' }), _jsx("span", { "data-ui": "menu-item-copy", children: _jsx("span", { children: option.label }) })] })] }));
|
|
638
|
+
};
|
|
639
|
+
const groupByMenuItem = (option, previous) => {
|
|
640
|
+
const divider = previous && previous.group !== option.group ? _jsx("hr", { "data-ui": "menu-separator" }) : null;
|
|
641
|
+
if (option.options?.length)
|
|
642
|
+
return (_jsxs(_Fragment, { children: [divider, _jsxs("details", { "data-ui": "disclosure", children: [_jsx("summary", { "data-ui": "disclosure-summary", children: option.label }), _jsx("div", { "data-ui": "disclosure-body", children: each(option.options, (entry) => entry.id, (entry, index) => groupByMenuItem(entry, index === 0 ? null : option.options[index - 1])) })] })] }));
|
|
643
|
+
const active = isActive('groupBy', option.id);
|
|
644
|
+
if (active)
|
|
645
|
+
return _jsx(_Fragment, {});
|
|
646
|
+
return (_jsxs(_Fragment, { children: [divider, _jsxs("button", { "data-ui": "menu-item", type: "button", disabled: groupByFacets().length >= (config.maxGroupBy ?? Infinity), onClick: () => toggleGroupBy(option), children: [_jsx("span", { "data-ui": "menu-item-check", "aria-hidden": "true", children: active ? '✓' : '' }), _jsx("span", { "data-ui": "menu-item-copy", children: _jsx("span", { children: option.label }) })] })] }));
|
|
647
|
+
};
|
|
648
|
+
const favoriteRow = (favorite) => (_jsxs("li", { "data-ui": "favorite-item", "data-active": String(favorite.id === activeFavoriteId()), children: [_jsx("button", { "data-ui": "menu-item", type: "button", onClick: () => applyFavorite(favorite), children: _jsx("span", { "data-ui": "menu-item-copy", children: _jsx("span", { children: favorite.label }) }) }), _jsxs("div", { "data-ui": "favorite-item-actions", children: [manager?.setDefaultFavoriteFunction && (_jsx("button", { "data-ui": "favorite-item-default", type: "button", "data-default": String(favorite.isDefault), "aria-label": labels.favoriteSetDefault, title: labels.favoriteSetDefault, onClick: () => setDefaultFavorite(favorite), children: favorite.isDefault ? '★' : '☆' })), manager?.deleteFavoriteFunction && (_jsx("button", { "data-ui": "favorite-item-remove", type: "button", "aria-label": `${labels.favoriteRemove}: ${favorite.label}`, title: labels.favoriteRemove, onClick: () => removeFavorite(favorite), children: "\u00D7" }))] })] }));
|
|
649
|
+
const columnTitle = (label, count) => (_jsxs("h3", { "data-ui": "search-filter-column-title", children: [label, count ? ` (${count})` : ''] }));
|
|
650
|
+
const groupByPipeline = () => {
|
|
651
|
+
const grouped = groupByFacets();
|
|
652
|
+
if (!grouped.length)
|
|
653
|
+
return _jsx(_Fragment, {});
|
|
654
|
+
return (_jsxs("section", { "data-ui": "search-filter-grouping", "aria-label": labels.groupByApplied, children: [_jsxs("div", { "data-ui": "search-filter-grouping-head", children: [_jsx("span", { children: labels.groupByApplied }), _jsx("button", { "data-ui": "search-filter-grouping-clear", type: "button", onClick: clearGroupBy, children: labels.groupByClear })] }), _jsx("ol", { "data-ui": "search-filter-grouping-list", children: each(grouped, (facet) => facet.id, (facet, index) => (_jsxs("li", { "data-ui": "search-filter-grouping-item", children: [_jsx("span", { "data-ui": "search-filter-grouping-order", children: String(index + 1) }), _jsx("span", { "data-ui": "search-filter-grouping-label", children: facet.label }), _jsxs("span", { "data-ui": "search-filter-grouping-actions", children: [index > 0 && (_jsx("button", { "data-ui": "search-filter-grouping-action", type: "button", "aria-label": `${labels.groupByMoveEarlier}: ${facet.label}`, title: `${labels.groupByMoveEarlier}: ${facet.label}`, onClick: () => moveGroupBy(facet, -1), children: "\u2191" })), index + 1 < grouped.length && (_jsx("button", { "data-ui": "search-filter-grouping-action", type: "button", "aria-label": `${labels.groupByMoveLater}: ${facet.label}`, title: `${labels.groupByMoveLater}: ${facet.label}`, onClick: () => moveGroupBy(facet, 1), children: "\u2193" })), _jsx("button", { "data-ui": "search-filter-grouping-action", "data-action": "remove", type: "button", "aria-label": `${labels.clear}: ${facet.label}`, title: `${labels.clear}: ${facet.label}`, onClick: () => removeFacet(facet), children: "\u00D7" })] })] }))) })] }));
|
|
655
|
+
};
|
|
656
|
+
const valueEditor = () => {
|
|
657
|
+
const field = selectedField();
|
|
658
|
+
const operator = customFilterOperator();
|
|
659
|
+
if (!field || !operator || isValuelessOperator(operator))
|
|
660
|
+
return null;
|
|
661
|
+
if (field.choices)
|
|
662
|
+
return (_jsxs("select", { "aria-label": labels.customFilterValue, multiple: operator === 'anyOf', onChange: (event) => {
|
|
663
|
+
if (event.currentTarget instanceof HTMLSelectElement)
|
|
664
|
+
customFilterValue.set(Array.from(event.currentTarget.selectedOptions)
|
|
665
|
+
.map((option) => option.value)
|
|
666
|
+
.filter(Boolean)
|
|
667
|
+
.join(','));
|
|
668
|
+
}, children: [_jsx("option", { value: "", disabled: true, selected: !customFilterValue(), children: labels.customFilterValue }), each(field.choices, (choice) => choice.value, (choice) => (_jsx("option", { value: choice.value, selected: customFilterValue().split(',').includes(choice.value), children: choice.label })))] }));
|
|
669
|
+
const type = field.type === 'number'
|
|
670
|
+
? 'number'
|
|
671
|
+
: field.type === 'date'
|
|
672
|
+
? 'date'
|
|
673
|
+
: field.type === 'datetime'
|
|
674
|
+
? 'datetime-local'
|
|
675
|
+
: 'text';
|
|
676
|
+
return (_jsxs(_Fragment, { children: [_jsx("input", { type: type, step: field.type === 'number' ? 'any' : undefined, autocomplete: "off", "aria-label": operator === 'between' ? labels.valueFrom : labels.customFilterValue, placeholder: labels.customFilterValue, value: customFilterValue(), onInput: (event) => {
|
|
677
|
+
if (event.currentTarget instanceof HTMLInputElement)
|
|
678
|
+
customFilterValue.set(event.currentTarget.value);
|
|
679
|
+
} }), operator === 'between' ? (_jsx("input", { type: type, step: field.type === 'number' ? 'any' : undefined, "aria-label": labels.valueTo, value: customFilterEnd(), onInput: (event) => {
|
|
680
|
+
if (event.currentTarget instanceof HTMLInputElement)
|
|
681
|
+
customFilterEnd.set(event.currentTarget.value);
|
|
682
|
+
} })) : null] }));
|
|
683
|
+
};
|
|
684
|
+
const icon = (kind) => (_jsx("svg", { "data-ui": "search-filter-icon", viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: _jsx("path", { d: kind === 'filter'
|
|
685
|
+
? 'M4 5h16M7 12h10M10 19h4'
|
|
686
|
+
: kind === 'groupBy'
|
|
687
|
+
? 'M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z'
|
|
688
|
+
: kind === 'favorite'
|
|
689
|
+
? 'M6 3h12v18l-6-4-6 4z'
|
|
690
|
+
: 'M21 21l-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0' }) }));
|
|
691
|
+
const sectionTrigger = (kind, label) => (_jsxs("button", { "data-ui": "search-filter-section-toggle", "data-section": kind, type: "button", "aria-expanded": String(menuOpen() && panel() === kind), "aria-controls": `${props.id}-panel`, onClick: (event) => {
|
|
692
|
+
event.stopPropagation();
|
|
693
|
+
menuOpener = event.currentTarget;
|
|
694
|
+
if (menuOpen() && panel() === kind)
|
|
695
|
+
closeMenu();
|
|
696
|
+
else
|
|
697
|
+
openPanel(kind);
|
|
698
|
+
}, children: [icon(kind), _jsx("span", { children: label }), countOf(kind) ? _jsx("span", { "data-ui": "menu-trigger-count", children: String(countOf(kind)) }) : null] }));
|
|
699
|
+
const panelContent = () => (_jsxs("div", { "data-ui": "search-filter-columns", "data-columns": 1 + Number(hasGrouping) + Number(hasFavorites()), "data-panel": sheetOpen() ? 'all' : panel(), children: [_jsxs("div", { "data-ui": "search-filter-column", "data-facet-type": "filter", children: [columnTitle(labels.filters, countOf('filter')), each(config.filters, (option) => option.id, (option, index) => filterMenuItem(option, index === 0 ? null : config.filters[index - 1])), capabilities.customFilters !== false && config.customFilterFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx("hr", { "data-ui": "menu-separator" }), _jsxs("div", { "data-ui": "custom-filter", children: [_jsxs("div", { "data-ui": "custom-filter-row", children: [_jsxs("select", { "aria-label": labels.customFilterField, onChange: (event) => {
|
|
700
|
+
if (event.currentTarget instanceof HTMLSelectElement) {
|
|
701
|
+
customFilterField.set(event.currentTarget.value);
|
|
702
|
+
customFilterOperator.set('');
|
|
703
|
+
customFilterValue.set('');
|
|
704
|
+
customFilterEnd.set('');
|
|
705
|
+
}
|
|
706
|
+
}, children: [_jsx("option", { value: "", disabled: true, selected: !customFilterField(), children: labels.customFilterField }), each(config.customFilterFields, (field) => field.value, (field) => (_jsx("option", { value: field.value, selected: field.value === customFilterField(), children: field.label })))] }), _jsxs("select", { "aria-label": labels.customFilterOperator, disabled: !customFilterField(), onChange: (event) => {
|
|
707
|
+
if (event.currentTarget instanceof HTMLSelectElement) {
|
|
708
|
+
customFilterOperator.set(event.currentTarget.value);
|
|
709
|
+
customFilterValue.set('');
|
|
710
|
+
customFilterEnd.set('');
|
|
711
|
+
}
|
|
712
|
+
}, children: [_jsx("option", { value: "", disabled: true, selected: !customFilterOperator(), children: labels.customFilterOperator }), each(operatorsFor(customFilterField()), (operator) => operator, (operator) => (_jsx("option", { value: operator, selected: operator === customFilterOperator(), children: operatorLabel(operator) })))] }), valueEditor()] }), _jsx("button", { "data-ui": "custom-filter-add", type: "button", disabled: !canAddRule(), onClick: addCustomFilter, children: labels.customFilterAdd })] })] }))] }), hasGrouping && (_jsxs("div", { "data-ui": "search-filter-column", "data-facet-type": "groupBy", children: [columnTitle(labels.groupBy, countOf('groupBy')), groupByPipeline(), config.groupBy.length ? (_jsx("p", { "data-ui": "search-filter-grouping-add-label", children: labels.groupByAdd })) : null, each(config.groupBy, (option) => option.id, (option, index) => groupByMenuItem(option, index === 0 ? null : config.groupBy[index - 1])), groupByOptions.length ? _jsx("hr", { "data-ui": "menu-separator" }) : null, groupByOptions.length ? (_jsxs("select", { "data-ui": "custom-group-by", disabled: groupByFacets().length >= (config.maxGroupBy ?? Infinity), "aria-label": labels.customGroupByPlaceholder, onChange: (event) => {
|
|
713
|
+
if (!(event.currentTarget instanceof HTMLSelectElement))
|
|
714
|
+
return;
|
|
715
|
+
const select = event.currentTarget;
|
|
716
|
+
const option = groupByOptions.find((entry) => entry.id === select.value);
|
|
717
|
+
select.value = '';
|
|
718
|
+
if (option && !isActive('groupBy', option.id))
|
|
719
|
+
toggleGroupBy(option);
|
|
720
|
+
}, children: [_jsx("option", { value: "", disabled: true, selected: true, children: labels.customGroupByPlaceholder }), each(groupByOptions, (option) => option.id, (option) => (_jsx("option", { value: option.id, children: option.label })))] })) : null] })), hasFavorites() && (_jsxs("div", { "data-ui": "search-filter-column", "data-facet-type": "favorite", children: [columnTitle(labels.favorites, activeFavoriteId() ? 1 : 0), config.favoriteHref ? (_jsx("a", { "data-ui": "menu-item", href: config.favoriteHref, children: labels.saveSearch })) : manager?.saveFavoriteFunction ? (_jsx("button", { "data-ui": "favorite-save-toggle", type: "button", "aria-expanded": String(savingFavorite()), "aria-controls": `${props.id}-favorite-form`, onClick: () => {
|
|
721
|
+
if (savingFavorite())
|
|
722
|
+
closeFavoriteForm();
|
|
723
|
+
else {
|
|
724
|
+
savingFavorite.set(true);
|
|
725
|
+
queueMicrotask(() => focusControl('[data-ui="favorite-save"] input[type="text"]'));
|
|
726
|
+
}
|
|
727
|
+
}, children: labels.saveSearch })) : null, manager?.saveFavoriteFunction && savingFavorite() && !config.favoriteHref ? (_jsxs("form", { id: `${props.id}-favorite-form`, "data-ui": "favorite-save", "aria-label": labels.saveSearch, onSubmit: saveFavorite, children: [_jsxs("div", { "data-ui": "favorite-save-row", children: [_jsx("input", { type: "text", autocomplete: "off", placeholder: labels.favoriteName, "aria-label": labels.favoriteName, value: saveFavoriteName(), onInput: (event) => {
|
|
728
|
+
if (event.currentTarget instanceof HTMLInputElement)
|
|
729
|
+
saveFavoriteName.set(event.currentTarget.value);
|
|
730
|
+
} }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: saveFavoriteDefault(), onChange: (event) => {
|
|
731
|
+
if (event.currentTarget instanceof HTMLInputElement)
|
|
732
|
+
saveFavoriteDefault.set(event.currentTarget.checked);
|
|
733
|
+
} }), labels.favoriteDefault] })] }), _jsx("button", { "data-ui": "action", "data-variant": "primary", "data-size": "compact", type: "submit", disabled: !saveFavoriteName().trim(), children: labels.favoriteSaveAction }), _jsx("button", { "data-ui": "action", "data-variant": "secondary", type: "button", onClick: (event) => {
|
|
734
|
+
// Removing the form also detaches this target before the shared
|
|
735
|
+
// outside-click dismissor runs. Keep cancellation inside the panel.
|
|
736
|
+
event.stopPropagation();
|
|
737
|
+
closeFavoriteForm();
|
|
738
|
+
}, children: labels.favoriteCancel })] })) : null, favorites().length ? (_jsx("ul", { "data-ui": "favorite-list", children: each(favorites(), (favorite) => favorite.id, favoriteRow) })) : (_jsx("p", { "data-ui": "search-filter-empty", children: labels.noFavorites }))] }))] }));
|
|
739
|
+
const notice = () => error() ? (_jsxs("aside", { "data-ui": "notice", "data-pattern": "notice", "data-tone": "danger", role: "alert", children: [_jsx("div", { "data-ui": "notice-copy", children: _jsx("p", { "data-ui": "notice-message", children: error() }) }), retryDraft() ? (_jsx("div", { "data-ui": "notice-actions", children: _jsx("button", { "data-ui": "action", "data-variant": "secondary", type: "button", onClick: retry, children: labels.retry }) })) : null] })) : null;
|
|
740
|
+
return {
|
|
741
|
+
view: () => (_jsxs("div", { "data-ui": "search-filter", id: props.id, role: "search", "aria-label": labels.searchLabel, "data-name": config.name, "data-size": config.size ?? 'default', "data-busy": pending() ? 'true' : null, onKeydown: (event) => {
|
|
742
|
+
if (event.key !== 'Escape' || sheetOpen() || !menuOpen())
|
|
743
|
+
return;
|
|
744
|
+
if (dismissDisclosure(event))
|
|
745
|
+
return;
|
|
746
|
+
event.preventDefault();
|
|
747
|
+
event.stopPropagation();
|
|
748
|
+
if (savingFavorite()) {
|
|
749
|
+
closeFavoriteForm();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
closeMenu();
|
|
753
|
+
queueMicrotask(() => menuOpener?.focus());
|
|
754
|
+
}, children: [_jsxs("div", { "data-ui": "search-filter-bar", children: [_jsxs("div", { "data-ui": "search-filter-field", children: [icon('search'), _jsx("input", { "data-ui": "search-filter-input", type: "search", value: query(), autocomplete: "off", placeholder: labels.searchPlaceholder, "aria-label": labels.searchLabel, onInput: handleSearchInput, onKeydown: handleSearchKeydown, onFocus: () => {
|
|
755
|
+
if (query().trim())
|
|
756
|
+
suggestionsOpen.set(true);
|
|
757
|
+
} }), suggestionsOpen() && query().trim() ? (_jsxs("div", { "data-ui": "search-filter-suggestions", role: "group", "aria-label": labels.searchGenericLabel, children: [_jsxs("button", { "data-ui": "search-filter-suggestion", type: "button", onClick: selectGenericSuggestion, children: [labels.searchGenericLabel, ": ", _jsxs("b", { children: ["\"", query().trim(), "\""] })] }), each(fieldSuggestions(query().trim()), (field) => field.value, (field) => (_jsxs("button", { "data-ui": "search-filter-suggestion", type: "button", onClick: () => selectFieldSuggestion(field), children: [labels.searchFieldPrefix, " ", _jsx("b", { children: field.label }), " ", labels.searchFieldPreposition, ":", ' ', _jsxs("b", { children: ["\"", query().trim(), "\""] })] })))] })) : null] }), _jsxs("details", { "data-ui": "menu", "data-variant": "search-filter", "data-align": "end", "data-active": countOf('filter') ? 'true' : null, open: menuOpen() === true ? true : undefined, onToggle: (event) => {
|
|
758
|
+
if (!(event.currentTarget instanceof HTMLDetailsElement))
|
|
759
|
+
return;
|
|
760
|
+
if (event.currentTarget.open)
|
|
761
|
+
menuOpen.set(true);
|
|
762
|
+
else
|
|
763
|
+
closeMenu();
|
|
764
|
+
}, children: [_jsxs("summary", { "data-ui": "search-filter-toggle", role: "button", "aria-expanded": String(sheetOpen() || (menuOpen() && panel() === 'filter')), "aria-label": labels.filters, title: labels.filters, onClick: togglePanel, "aria-controls": `${props.id}-panel`, children: [icon('filter'), _jsx("span", { children: labels.filters }), countOf('filter') ? (_jsx("span", { "data-ui": "menu-trigger-count", children: String(countOf('filter')) })) : null] }), _jsx("div", { id: `${props.id}-panel`, "data-ui": "menu-panel", role: "group", "aria-label": panel() === 'filter'
|
|
765
|
+
? labels.filters
|
|
766
|
+
: panel() === 'groupBy'
|
|
767
|
+
? labels.groupBy
|
|
768
|
+
: labels.favorites, children: !sheetOpen() ? panelContent() : null })] }), hasGrouping ? sectionTrigger('groupBy', labels.groupBy) : null, hasFavorites() ? sectionTrigger('favorite', labels.favorites) : null] }), facets().length ? (_jsxs("ul", { "data-ui": "search-filter-facets", children: [each(facets(), (facet) => facet.id, facetChip), facets().some((facet) => facet.type === 'filter' || facet.type === 'favorite') ? (_jsx("li", { children: _jsx("button", { "data-ui": "search-filter-clear", type: "button", onClick: clearFilters, children: labels.clearFilters }) })) : null] })) : null, _jsx("dialog", { "data-ui": "search-filter-sheet", "aria-label": labels.toggleLabel, onKeydown: (event) => {
|
|
769
|
+
if (event.key === 'Escape') {
|
|
770
|
+
if (dismissDisclosure(event))
|
|
771
|
+
return;
|
|
772
|
+
event.preventDefault();
|
|
773
|
+
event.stopPropagation();
|
|
774
|
+
if (savingFavorite()) {
|
|
775
|
+
closeFavoriteForm();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
closeSheet();
|
|
779
|
+
}
|
|
780
|
+
}, onCancel: (event) => {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
closeSheet();
|
|
783
|
+
}, onClick: (event) => {
|
|
784
|
+
if (event.target instanceof Element &&
|
|
785
|
+
event.target.closest('[data-ui="modal-close"], [data-ui="modal-backdrop"]'))
|
|
786
|
+
closeSheet();
|
|
787
|
+
}, children: sheetOpen() ? (_jsx(ModalSheet, { id: `${props.id}-sheet`, mode: "client", dialogSemantics: "parent", title: labels.toggleLabel, closeLabel: labels.close, body: _jsxs(_Fragment, { children: [notice(), panelContent()] }) })) : null }), !sheetOpen() ? notice() : null] })),
|
|
788
|
+
mount: ({ root, lifetime }) => {
|
|
789
|
+
mountedRoot = root;
|
|
790
|
+
document.addEventListener('ket:navigation-start', () => {
|
|
791
|
+
// A link or Back/Forward supersedes an RPC which has not returned yet.
|
|
792
|
+
// Our own navigation emits this event synchronously from respondWith.
|
|
793
|
+
if (startingNavigation)
|
|
794
|
+
return;
|
|
795
|
+
applyVersion++;
|
|
796
|
+
navigationRequest?.abort();
|
|
797
|
+
facets.set(settled.facets);
|
|
798
|
+
customFilterRules.set(settled.rules);
|
|
799
|
+
pending.set(false);
|
|
800
|
+
}, { signal: lifetime });
|
|
801
|
+
// Autocomplete is an island-owned popup rather than a native <details>
|
|
802
|
+
// disclosure, so the shared details-menu dismissor cannot see it. Keep
|
|
803
|
+
// the boundary local to the component: an outside click abandons the
|
|
804
|
+
// suggestions without changing the query the reader has entered.
|
|
805
|
+
document.addEventListener('click', (event) => {
|
|
806
|
+
if (!suggestionsOpen() && !menuOpen())
|
|
807
|
+
return;
|
|
808
|
+
const target = event.target;
|
|
809
|
+
if (target instanceof Node && root.contains(target))
|
|
810
|
+
return;
|
|
811
|
+
suggestionsOpen.set(false);
|
|
812
|
+
closeMenu();
|
|
813
|
+
}, { signal: lifetime });
|
|
814
|
+
},
|
|
815
|
+
dispose: () => {
|
|
816
|
+
applyVersion++;
|
|
817
|
+
pending.set(false);
|
|
818
|
+
dialogElement()?.close();
|
|
819
|
+
mountedRoot = null;
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
export const searchFilter = (props) => createSearchFilterView(props);
|
|
824
|
+
//# sourceMappingURL=index.js.map
|