@enonic/ui-kit 0.3.0 → 0.5.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.
Files changed (50) hide show
  1. package/README.md +78 -8
  2. package/dist/dialog/action-dialog-context.d.ts +22 -0
  3. package/dist/dialog/action-dialog-context.js +21 -0
  4. package/dist/dialog/action-dialog-context.js.map +1 -0
  5. package/dist/dialog/action-dialog.d.ts +135 -0
  6. package/dist/dialog/action-dialog.js +213 -0
  7. package/dist/dialog/action-dialog.js.map +1 -0
  8. package/dist/dialog/delete-confirm.d.ts +33 -0
  9. package/dist/dialog/delete-confirm.js +80 -0
  10. package/dist/dialog/delete-confirm.js.map +1 -0
  11. package/dist/dialog/delete-expectation.d.ts +17 -0
  12. package/dist/dialog/delete-expectation.js +13 -0
  13. package/dist/dialog/delete-expectation.js.map +1 -0
  14. package/dist/dialog/dialog-preset.d.ts +34 -0
  15. package/dist/dialog/dialog-preset.js +38 -0
  16. package/dist/dialog/dialog-preset.js.map +1 -0
  17. package/dist/dialog/gate-match.d.ts +6 -0
  18. package/dist/dialog/gate-match.js +11 -0
  19. package/dist/dialog/gate-match.js.map +1 -0
  20. package/dist/dialog/gate.d.ts +31 -0
  21. package/dist/dialog/gate.js +72 -0
  22. package/dist/dialog/gate.js.map +1 -0
  23. package/dist/dialog/use-close-guard.d.ts +34 -0
  24. package/dist/dialog/use-close-guard.js +40 -0
  25. package/dist/dialog/use-close-guard.js.map +1 -0
  26. package/dist/filter/filter-fields.d.ts +33 -0
  27. package/dist/filter/filter-fields.js +72 -0
  28. package/dist/filter/filter-fields.js.map +1 -0
  29. package/dist/filter/filter-input.d.ts +35 -0
  30. package/dist/filter/filter-input.js +441 -0
  31. package/dist/filter/filter-input.js.map +1 -0
  32. package/dist/filter/filter-input.phrases.d.ts +26 -0
  33. package/dist/filter/filter-input.phrases.js +26 -0
  34. package/dist/filter/filter-input.phrases.js.map +1 -0
  35. package/dist/filter/filter-query.d.ts +40 -0
  36. package/dist/filter/filter-query.js +61 -0
  37. package/dist/filter/filter-query.js.map +1 -0
  38. package/dist/i18n/fill-phrase.d.ts +10 -0
  39. package/dist/i18n/fill-phrase.js +25 -0
  40. package/dist/i18n/fill-phrase.js.map +1 -0
  41. package/dist/i18n/phrases.d.ts +14 -0
  42. package/dist/i18n/phrases.js +23 -0
  43. package/dist/i18n/phrases.js.map +1 -0
  44. package/dist/i18n/use-phrases.d.ts +9 -0
  45. package/dist/i18n/use-phrases.js +11 -0
  46. package/dist/i18n/use-phrases.js.map +1 -0
  47. package/dist/index.d.ts +16 -1
  48. package/dist/index.js +15 -0
  49. package/package.json +34 -6
  50. package/preset.css +6 -0
@@ -0,0 +1,441 @@
1
+ import { EMPTY_FILTER, fieldTerm, isFieldTerm, termKey, textTerm, toggledTerm, valuesOf, withTerm, withoutTerm } from "./filter-query.js";
2
+ import { fieldTyped, groupedValues, isOffered, matchingFields, matchingValues, termLabel } from "./filter-fields.js";
3
+ import { filterInputPhrases } from "./filter-input.phrases.js";
4
+ import { forwardRef, useId, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Button, Combobox, IconButton, Listbox, Tag, cn, useCombobox, useComposedRefs, useControlledState, usePhrases } from "@enonic/ui";
7
+ import { Check, ListFilter, X } from "lucide-react";
8
+ //#region src/filter/filter-input.tsx
9
+ const FILTER_INPUT_NAME = "FilterInput";
10
+ const FIELD_OPTION = "field:";
11
+ const VALUE_OPTION = "value:";
12
+ /**
13
+ * The filter of a list: values of predefined fields combined with free text, every term a
14
+ * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the
15
+ * library returns to the input on Escape does not reopen it. The fields come first; picking one, or
16
+ * typing its label and a colon, opens its values with their hit counts, and the values stay open
17
+ * across picks so several can be ticked. Enter picks the highlighted option, or adds typed text as
18
+ * a tag when no field is entered. The ref is the text input's.
19
+ */
20
+ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue = EMPTY_FILTER, onChange: onValueChange, placeholder: ownPlaceholder, disabled = false, readOnly = false, error = false, className, "data-component": componentName = FILTER_INPUT_NAME }, ref) => {
21
+ const inputRef = useRef(null);
22
+ const composedRef = useComposedRefs(ref, inputRef);
23
+ const removeRefs = useRef([]);
24
+ const pickedRef = useRef(false);
25
+ const keyboardPickRef = useRef(false);
26
+ const headingId = useId();
27
+ const [value, onChange] = useControlledState(controlledValue, defaultValue, onValueChange);
28
+ const [text, setText] = useState("");
29
+ const [open, setOpen] = useState(false);
30
+ const [fieldId, setFieldId] = useState();
31
+ const [activeOption, setActiveOption] = useState(null);
32
+ const [announcement, setAnnouncement] = useState({
33
+ text: "",
34
+ key: 0
35
+ });
36
+ const t = usePhrases(filterInputPhrases);
37
+ const filterLabel = t("enonic.uiKit.filterInput.label");
38
+ const placeholder = ownPlaceholder ?? t("enonic.uiKit.filterInput.placeholder");
39
+ const valuePlaceholder = t("enonic.uiKit.filterInput.valuePlaceholder");
40
+ const clearLabel = t("enonic.uiKit.filterInput.clear");
41
+ const clearedLabel = t("enonic.uiKit.filterInput.cleared");
42
+ const noMatchesLabel = t("enonic.uiKit.filterInput.noMatches");
43
+ const loadingLabel = t("enonic.uiKit.filterInput.loading");
44
+ const field = fields.find(({ id }) => id === fieldId);
45
+ const stage = field === void 0 ? "fields" : "values";
46
+ const offeredFields = field === void 0 ? matchingFields(fields, text) : [];
47
+ const offeredGroups = field === void 0 ? [] : groupedValues(matchingValues(field.values, text));
48
+ const offeredValues = offeredGroups.flatMap(({ values }) => values);
49
+ const picked = field === void 0 ? void 0 : valuesOf(value, field.id);
50
+ const shown = !readOnly && open && (stage === "values" || offeredFields.length > 0);
51
+ const inert = disabled || readOnly;
52
+ const active = !inert && (value.length > 0 || text.length > 0 || field !== void 0);
53
+ const optionIds = field === void 0 ? text.length === 0 ? offeredFields.map(({ id }) => `${FIELD_OPTION}${id}`) : [] : offeredValues.filter((candidate) => isOffered(candidate) || picked?.has(candidate.id) === true).map(({ id }) => `${VALUE_OPTION}${id}`);
54
+ const highlighted = activeOption !== null && optionIds.includes(activeOption) ? activeOption : optionIds[0] ?? null;
55
+ const fieldPrefixes = (label) => [t("enonic.uiKit.filterInput.fieldPrefix", label), `${label}:`];
56
+ const nameOf = (term) => {
57
+ const { field: fieldName, value: valueName } = termLabel(term, fields);
58
+ return fieldName === void 0 ? valueName : t("enonic.uiKit.filterInput.term", fieldName, valueName);
59
+ };
60
+ const focusInput = () => {
61
+ inputRef.current?.focus();
62
+ };
63
+ const announce = (text) => {
64
+ setAnnouncement((previous) => ({
65
+ text,
66
+ key: previous.key + 1
67
+ }));
68
+ };
69
+ const openList = () => {
70
+ setOpen(true);
71
+ };
72
+ const enterField = (next, typed = "") => {
73
+ setFieldId(next?.id);
74
+ setText(typed);
75
+ if (next !== void 0) openList();
76
+ };
77
+ const add = (term) => {
78
+ const next = withTerm(value, term);
79
+ if (next === value) return;
80
+ onChange(next);
81
+ announce(t("enonic.uiKit.filterInput.added", nameOf(term)));
82
+ };
83
+ const remove = (index) => {
84
+ const term = value[index];
85
+ if (term === void 0) return;
86
+ onChange(withoutTerm(value, index));
87
+ announce(t("enonic.uiKit.filterInput.removed", nameOf(term)));
88
+ };
89
+ const pickValue = (candidate) => {
90
+ if (field === void 0) return;
91
+ const term = fieldTerm(field.id, candidate.id);
92
+ if (picked?.has(candidate.id) === true) {
93
+ onChange(toggledTerm(value, term));
94
+ announce(t("enonic.uiKit.filterInput.removed", nameOf(term)));
95
+ } else if (field.multiple === false) {
96
+ const ofField = (held) => isFieldTerm(held) && held.field === field.id;
97
+ const previous = value.find(ofField);
98
+ if (previous === void 0) add(term);
99
+ else {
100
+ onChange([...value.filter((held) => !ofField(held)), term]);
101
+ announce(t("enonic.uiKit.filterInput.replaced", nameOf(previous), nameOf(term)));
102
+ }
103
+ } else add(term);
104
+ setActiveOption(`${VALUE_OPTION}${candidate.id}`);
105
+ setText("");
106
+ };
107
+ const handleTextChange = (typed) => {
108
+ const named = field === void 0 ? fieldTyped(fields, typed ?? "", fieldPrefixes) : void 0;
109
+ if (named !== void 0) {
110
+ enterField(named.field, named.text);
111
+ return;
112
+ }
113
+ setText(typed ?? "");
114
+ openList();
115
+ };
116
+ const handlePick = ([id]) => {
117
+ if (id === void 0) return;
118
+ const fromKeyboard = keyboardPickRef.current;
119
+ keyboardPickRef.current = false;
120
+ pickedRef.current = true;
121
+ if (id.startsWith(FIELD_OPTION)) enterField(fields.find((candidate) => candidate.id === id.slice(6)));
122
+ else if (field !== void 0) {
123
+ const candidate = field.values.find(({ id: valueId }) => valueId === id.slice(6));
124
+ if (candidate !== void 0) pickValue(candidate);
125
+ if (fromKeyboard) return;
126
+ }
127
+ focusInput();
128
+ };
129
+ const noteValueKeyDown = (event) => {
130
+ keyboardPickRef.current = event.key === "Enter" || event.key === " ";
131
+ };
132
+ const handleOpenChange = (next) => {
133
+ if (!next && pickedRef.current) {
134
+ pickedRef.current = false;
135
+ return;
136
+ }
137
+ if (!next && field !== void 0) enterField(void 0);
138
+ setOpen(next);
139
+ };
140
+ const handleEnter = (listOpen) => {
141
+ if (field !== void 0) {
142
+ if (!listOpen) {
143
+ openList();
144
+ return;
145
+ }
146
+ const candidate = offeredValues.find(({ id }) => `${VALUE_OPTION}${id}` === highlighted);
147
+ if (candidate !== void 0) pickValue(candidate);
148
+ return;
149
+ }
150
+ const term = textTerm(text);
151
+ if (term !== void 0) {
152
+ add(term);
153
+ setText("");
154
+ return;
155
+ }
156
+ const named = fields.find(({ id }) => `${FIELD_OPTION}${id}` === highlighted);
157
+ if (listOpen && named !== void 0) enterField(named);
158
+ else openList();
159
+ };
160
+ const handleBackspace = () => {
161
+ if (field !== void 0) enterField(void 0);
162
+ else if (value.length > 0) remove(value.length - 1);
163
+ };
164
+ const handleEscape = () => {
165
+ if (field === void 0) return false;
166
+ enterField(void 0);
167
+ return true;
168
+ };
169
+ const focusTag = (index) => {
170
+ const cross = removeRefs.current[index];
171
+ if (cross === null || cross === void 0) return false;
172
+ cross.focus();
173
+ return true;
174
+ };
175
+ const handleArrowLeft = () => focusTag(value.length - 1);
176
+ const handleTagKeyDown = (index) => (event) => {
177
+ if (event.key === "ArrowLeft") {
178
+ event.preventDefault();
179
+ focusTag(index - 1);
180
+ } else if (event.key === "ArrowRight") {
181
+ event.preventDefault();
182
+ if (!focusTag(index + 1)) focusInput();
183
+ } else if (event.key === "Backspace" || event.key === "Delete") {
184
+ event.preventDefault();
185
+ remove(index);
186
+ if (!focusTag(index - 1)) focusInput();
187
+ } else if (event.key === "Escape") {
188
+ event.preventDefault();
189
+ focusInput();
190
+ }
191
+ };
192
+ const handleBoxPointerDown = (event) => {
193
+ if (inert || !(event.target instanceof Element) || event.target.closest("button, input")) return;
194
+ event.preventDefault();
195
+ focusInput();
196
+ openList();
197
+ };
198
+ const clear = () => {
199
+ if (value.length > 0) onChange([]);
200
+ announce(clearedLabel);
201
+ enterField(void 0);
202
+ focusInput();
203
+ };
204
+ return /* @__PURE__ */ jsx(Combobox, {
205
+ open: shown,
206
+ onOpenChange: handleOpenChange,
207
+ value: text,
208
+ onChange: handleTextChange,
209
+ selection: [],
210
+ onSelectionChange: handlePick,
211
+ active: highlighted,
212
+ setActive: setActiveOption,
213
+ contentType: "listbox",
214
+ disabled,
215
+ error,
216
+ children: /* @__PURE__ */ jsxs(Combobox.Content, {
217
+ "data-component": componentName,
218
+ "data-click-outside-ignore": true,
219
+ onPointerDown: handleBoxPointerDown,
220
+ className: cn("bg-surface-neutral border-bdr-subtle flex min-h-12 shrink-0 items-start gap-1.5 rounded-sm border p-1.5", "hover:outline-bdr-subtle hover:outline-2", "focus-within:ring-ring focus-within:ring-offset-ring-offset focus-within:ring-3 focus-within:ring-offset-3 focus-within:outline-none", "transition-highlight", shown && "border-bdr-strong", error && "border-error focus-within:ring-error hover:outline-error", disabled && "pointer-events-none opacity-50", className),
221
+ children: [
222
+ /* @__PURE__ */ jsx(Button, {
223
+ variant: "text",
224
+ size: "sm",
225
+ startIcon: ListFilter,
226
+ label: filterLabel,
227
+ disabled: inert,
228
+ onClick: () => {
229
+ focusInput();
230
+ openList();
231
+ },
232
+ className: "shrink-0"
233
+ }),
234
+ /* @__PURE__ */ jsxs("div", {
235
+ className: "flex min-h-9 min-w-0 flex-1 flex-wrap content-center items-center gap-x-2 gap-y-1.5",
236
+ children: [
237
+ value.map((term, index) => {
238
+ const { field: fieldName, value: valueName } = termLabel(term, fields);
239
+ return /* @__PURE__ */ jsxs(Tag, {
240
+ disabled,
241
+ children: [
242
+ fieldName !== void 0 && /* @__PURE__ */ jsx(Tag.Prefix, { children: t("enonic.uiKit.filterInput.fieldPrefix", fieldName) }),
243
+ /* @__PURE__ */ jsx(Tag.Label, { children: valueName }),
244
+ !readOnly && /* @__PURE__ */ jsx(Tag.Remove, {
245
+ ref: (node) => {
246
+ removeRefs.current[index] = node;
247
+ },
248
+ tabIndex: -1,
249
+ "aria-label": t("enonic.uiKit.filterInput.remove", nameOf(term)),
250
+ disabled,
251
+ onClick: () => {
252
+ remove(index);
253
+ focusInput();
254
+ },
255
+ onKeyDown: handleTagKeyDown(index)
256
+ })
257
+ ]
258
+ }, termKey(term));
259
+ }),
260
+ field !== void 0 && /* @__PURE__ */ jsx("span", {
261
+ className: "text-subtle shrink-0 whitespace-nowrap",
262
+ children: t("enonic.uiKit.filterInput.fieldPrefix", field.label)
263
+ }),
264
+ !readOnly && /* @__PURE__ */ jsx(Combobox.Control, {
265
+ className: "h-auto min-h-0 min-w-40 flex-1 rounded-none border-0 bg-transparent focus-within:ring-0 focus-within:ring-offset-0",
266
+ children: /* @__PURE__ */ jsx(Combobox.Search, {
267
+ className: "h-auto bg-transparent px-1 py-0 hover:outline-none",
268
+ children: /* @__PURE__ */ jsx(FilterTextInput, {
269
+ inputRef: composedRef,
270
+ text,
271
+ placeholder: field === void 0 ? placeholder : valuePlaceholder,
272
+ label: field === void 0 ? placeholder : t("enonic.uiKit.filterInput.term", field.label, valuePlaceholder),
273
+ onOpen: openList,
274
+ onEnter: handleEnter,
275
+ onBackspace: handleBackspace,
276
+ onEscape: handleEscape,
277
+ onArrowLeft: handleArrowLeft
278
+ })
279
+ })
280
+ })
281
+ ]
282
+ }),
283
+ active && /* @__PURE__ */ jsx(IconButton, {
284
+ icon: X,
285
+ variant: "text",
286
+ iconSize: 28,
287
+ iconStrokeWidth: 1.25,
288
+ title: clearLabel,
289
+ "aria-label": clearLabel,
290
+ disabled,
291
+ onClick: clear,
292
+ className: "text-subtle my-1 mr-1 size-7 shrink-0"
293
+ }),
294
+ /* @__PURE__ */ jsx("div", {
295
+ "aria-live": "polite",
296
+ className: "sr-only",
297
+ children: /* @__PURE__ */ jsx("span", { children: announcement.text }, announcement.key)
298
+ }),
299
+ /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Popup, {
300
+ style: { width: "auto" },
301
+ className: "max-w-md min-w-72",
302
+ children: /* @__PURE__ */ jsxs(Combobox.ListContent, {
303
+ "aria-label": field === void 0 ? filterLabel : field.label,
304
+ "aria-multiselectable": field !== void 0 && field.multiple !== false ? true : void 0,
305
+ className: "max-h-72 gap-y-0.5 overflow-y-auto p-2",
306
+ children: [
307
+ offeredFields.map(({ id, label, icon: Icon }) => /* @__PURE__ */ jsxs(Listbox.Item, {
308
+ value: `${FIELD_OPTION}${id}`,
309
+ className: "data-[active]:bg-surface-neutral-hover gap-2.5 rounded-sm px-3 py-2",
310
+ children: [Icon !== void 0 && /* @__PURE__ */ jsx(Icon, {
311
+ size: 16,
312
+ strokeWidth: 1.5,
313
+ className: "text-subtle shrink-0",
314
+ "aria-hidden": true
315
+ }), /* @__PURE__ */ jsx("span", {
316
+ className: "truncate",
317
+ children: label
318
+ })]
319
+ }, id)),
320
+ field?.notice !== void 0 && /* @__PURE__ */ jsx("div", {
321
+ role: "presentation",
322
+ className: "text-subtle px-3 py-2 text-xs",
323
+ children: field.notice
324
+ }),
325
+ field?.loading === true && /* @__PURE__ */ jsx("div", {
326
+ role: "presentation",
327
+ "aria-busy": true,
328
+ className: "text-subtle px-3 py-2",
329
+ children: loadingLabel
330
+ }),
331
+ field !== void 0 && field.loading !== true && offeredValues.length === 0 && /* @__PURE__ */ jsx("div", {
332
+ role: "presentation",
333
+ className: "text-subtle px-3 py-2",
334
+ children: noMatchesLabel
335
+ }),
336
+ offeredGroups.map(({ label: groupLabel, values }, groupIndex) => {
337
+ const groupHeadingId = groupLabel === void 0 ? void 0 : `${headingId}-group-${groupIndex}`;
338
+ return /* @__PURE__ */ jsxs("div", {
339
+ role: "group",
340
+ "aria-labelledby": groupHeadingId,
341
+ className: "contents",
342
+ children: [groupLabel !== void 0 && /* @__PURE__ */ jsxs("div", {
343
+ role: "presentation",
344
+ className: "flex w-full items-baseline gap-2.5 px-3 pt-2.5 pb-1",
345
+ children: [/* @__PURE__ */ jsx("span", {
346
+ id: groupHeadingId,
347
+ className: "text-subtle min-w-0 truncate text-xs tracking-wider uppercase",
348
+ children: groupLabel
349
+ }), /* @__PURE__ */ jsx("span", { className: "border-bdr-subtle min-w-6 flex-1 border-b" })]
350
+ }), values.map((candidate) => {
351
+ const selected = picked?.has(candidate.id) === true;
352
+ return /* @__PURE__ */ jsxs(Listbox.Item, {
353
+ value: `${VALUE_OPTION}${candidate.id}`,
354
+ disabled: !selected && !isOffered(candidate),
355
+ "aria-selected": selected,
356
+ className: "data-[active]:bg-surface-neutral-hover rounded-sm px-3 py-2",
357
+ onKeyDown: noteValueKeyDown,
358
+ children: [
359
+ /* @__PURE__ */ jsx("span", {
360
+ className: "grow truncate",
361
+ children: candidate.label
362
+ }),
363
+ selected && /* @__PURE__ */ jsx(Check, {
364
+ size: 16,
365
+ strokeWidth: 2,
366
+ className: "shrink-0",
367
+ "aria-hidden": true
368
+ }),
369
+ candidate.count !== void 0 && /* @__PURE__ */ jsxs("span", {
370
+ className: "text-subtle tabular-nums",
371
+ children: [
372
+ "(",
373
+ candidate.count,
374
+ ")"
375
+ ]
376
+ })
377
+ ]
378
+ }, candidate.id);
379
+ })]
380
+ }, groupLabel === void 0 ? "ungrouped" : `group:${groupLabel}`);
381
+ })
382
+ ]
383
+ }, stage)
384
+ }) })
385
+ ]
386
+ })
387
+ });
388
+ });
389
+ FilterInput.displayName = FILTER_INPUT_NAME;
390
+ /**
391
+ * Whether the key belongs to an IME composition. React keeps the flag on `nativeEvent`, Preact on
392
+ * the event itself; 229 is the key code every browser reports for a composing key.
393
+ */
394
+ function isComposing(event) {
395
+ const native = event.nativeEvent ?? event;
396
+ return native.isComposing === true || native.keyCode === 229;
397
+ }
398
+ /**
399
+ * The input, with the keys a tag input adds to a combobox's: Enter picks or commits, Backspace on an
400
+ * empty input takes the last tag back, Escape steps out of a field, ArrowLeft at the start walks into
401
+ * the tags. Everything else — the arrows into the list, Escape closing — is the library's, reached
402
+ * through its context. A key pressed mid-composition is the IME's.
403
+ */
404
+ const FilterTextInput = ({ inputRef, text, placeholder, label, onOpen, onEnter, onBackspace, onEscape, onArrowLeft }) => {
405
+ const { keyHandler, open } = useCombobox();
406
+ const handleKeyDown = (event) => {
407
+ if (isComposing(event)) return;
408
+ if (event.key === "Enter") {
409
+ event.preventDefault();
410
+ onEnter(open);
411
+ return;
412
+ }
413
+ if (event.key === "Backspace" && text.length === 0 && !event.repeat) {
414
+ onBackspace();
415
+ return;
416
+ }
417
+ if (event.key === "Escape" && onEscape()) {
418
+ event.preventDefault();
419
+ return;
420
+ }
421
+ const atStart = event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
422
+ if (event.key === "ArrowLeft" && atStart && onArrowLeft()) {
423
+ event.preventDefault();
424
+ return;
425
+ }
426
+ keyHandler(event);
427
+ };
428
+ return /* @__PURE__ */ jsx(Combobox.Input, {
429
+ ref: inputRef,
430
+ "aria-label": label,
431
+ placeholder,
432
+ className: "bg-transparent",
433
+ onClick: onOpen,
434
+ onKeyDown: handleKeyDown
435
+ });
436
+ };
437
+ FilterTextInput.displayName = "FilterInput.TextInput";
438
+ //#endregion
439
+ export { FilterInput };
440
+
441
+ //# sourceMappingURL=filter-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-input.js","names":[],"sources":["../../src/filter/filter-input.tsx"],"sourcesContent":["import {\n Button,\n cn,\n Combobox,\n IconButton,\n Listbox,\n Tag,\n useCombobox,\n useComposedRefs,\n useControlledState,\n usePhrases,\n} from '@enonic/ui';\nimport { Check, ListFilter, X } from 'lucide-react';\nimport {\n forwardRef,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type Ref,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport {\n fieldTyped,\n groupedValues,\n isOffered,\n matchingFields,\n matchingValues,\n termLabel,\n type FilterField,\n type FilterValue,\n} from './filter-fields';\nimport { filterInputPhrases } from './filter-input.phrases';\nimport {\n EMPTY_FILTER,\n fieldTerm,\n isFieldTerm,\n termKey,\n textTerm,\n toggledTerm,\n valuesOf,\n withoutTerm,\n withTerm,\n type FilterQuery,\n type FilterTerm,\n} from './filter-query';\n\nexport type FilterInputProps = {\n /** What the dropdown offers. None leaves the input a free-text search. */\n fields: readonly FilterField[];\n /** The query, controlled; leave it out and the input keeps its own, starting from `defaultValue`. */\n value?: FilterQuery;\n defaultValue?: FilterQuery;\n onChange?: (query: FilterQuery) => void;\n /** What the empty input invites, named for the list: `Search users`. Defaults to the kit's \"Search\". */\n placeholder?: string;\n disabled?: boolean;\n /** The tags are the whole content: no input, no crosses, no clearing. */\n readOnly?: boolean;\n /** The box reads as invalid; what is wrong with the query is the caller's to say beside it. */\n error?: boolean;\n className?: string;\n 'data-component'?: string;\n};\n\nconst FILTER_INPUT_NAME = 'FilterInput';\n\n// One listbox holds both stages, so the ids say which kind of option was picked.\nconst FIELD_OPTION = 'field:';\nconst VALUE_OPTION = 'value:';\n\n/**\n * The filter of a list: values of predefined fields combined with free text, every term a\n * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the\n * library returns to the input on Escape does not reopen it. The fields come first; picking one, or\n * typing its label and a colon, opens its values with their hit counts, and the values stay open\n * across picks so several can be ticked. Enter picks the highlighted option, or adds typed text as\n * a tag when no field is entered. The ref is the text input's.\n */\nconst FilterInput = forwardRef<HTMLInputElement, FilterInputProps>(\n (\n {\n fields,\n value: controlledValue,\n defaultValue = EMPTY_FILTER,\n onChange: onValueChange,\n placeholder: ownPlaceholder,\n disabled = false,\n readOnly = false,\n error = false,\n className,\n 'data-component': componentName = FILTER_INPUT_NAME,\n },\n ref,\n ): ReactElement => {\n const inputRef = useRef<HTMLInputElement>(null);\n const composedRef = useComposedRefs(ref, inputRef);\n const removeRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const pickedRef = useRef(false);\n const keyboardPickRef = useRef(false);\n const headingId = useId();\n const [value, onChange] = useControlledState(controlledValue, defaultValue, onValueChange);\n const [text, setText] = useState('');\n const [open, setOpen] = useState(false);\n const [fieldId, setFieldId] = useState<string>();\n const [activeOption, setActiveOption] = useState<string | null>(null);\n const [announcement, setAnnouncement] = useState({ text: '', key: 0 });\n\n const t = usePhrases(filterInputPhrases);\n const filterLabel = t('enonic.uiKit.filterInput.label');\n const placeholder = ownPlaceholder ?? t('enonic.uiKit.filterInput.placeholder');\n const valuePlaceholder = t('enonic.uiKit.filterInput.valuePlaceholder');\n const clearLabel = t('enonic.uiKit.filterInput.clear');\n const clearedLabel = t('enonic.uiKit.filterInput.cleared');\n const noMatchesLabel = t('enonic.uiKit.filterInput.noMatches');\n const loadingLabel = t('enonic.uiKit.filterInput.loading');\n\n const field = fields.find(({ id }) => id === fieldId);\n const stage = field === undefined ? 'fields' : 'values';\n const offeredFields = field === undefined ? matchingFields(fields, text) : [];\n const offeredGroups =\n field === undefined ? [] : groupedValues(matchingValues(field.values, text));\n const offeredValues = offeredGroups.flatMap(({ values }) => values);\n const picked = field === undefined ? undefined : valuesOf(value, field.id);\n // The values stage always has something to say — a value, \"no matches\", the field's notice.\n const shown = !readOnly && open && (stage === 'values' || offeredFields.length > 0);\n const inert = disabled || readOnly;\n const active = !inert && (value.length > 0 || text.length > 0 || field !== undefined);\n\n // ? The library highlights an option only while it has the focus, so from the input Enter would act\n // ? on nothing the user can see. The active option is held here instead: the first one the list\n // ? offers, until the user moves it — except in the fields stage with text typed, where Enter is\n // ? the search and nothing is highlighted. It stays put across a pick, so Enter again takes it back.\n const optionIds =\n field === undefined\n ? text.length === 0\n ? offeredFields.map(({ id }) => `${FIELD_OPTION}${id}`)\n : []\n : offeredValues\n .filter((candidate) => isOffered(candidate) || picked?.has(candidate.id) === true)\n .map(({ id }) => `${VALUE_OPTION}${id}`);\n const highlighted =\n activeOption !== null && optionIds.includes(activeOption)\n ? activeOption\n : (optionIds[0] ?? null);\n\n // What a field is typed as to enter it: the form the tags show, and `Label:` whatever the locale.\n const fieldPrefixes = (label: string): string[] => [\n t('enonic.uiKit.filterInput.fieldPrefix', label),\n `${label}:`,\n ];\n\n const nameOf = (term: FilterTerm): string => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n return fieldName === undefined\n ? valueName\n : t('enonic.uiKit.filterInput.term', fieldName, valueName);\n };\n\n const focusInput = (): void => {\n inputRef.current?.focus();\n };\n\n // Keyed, so the same message twice in a row is a new node the live region speaks again.\n const announce = (text: string): void => {\n setAnnouncement((previous) => ({ text, key: previous.key + 1 }));\n };\n\n const openList = (): void => {\n setOpen(true);\n };\n\n const enterField = (next: FilterField | undefined, typed = ''): void => {\n setFieldId(next?.id);\n setText(typed);\n if (next !== undefined) {\n openList();\n }\n };\n\n // A term already held changes nothing and says nothing: `withTerm` answers the same query.\n const add = (term: FilterTerm): void => {\n const next = withTerm(value, term);\n if (next === value) {\n return;\n }\n\n onChange(next);\n announce(t('enonic.uiKit.filterInput.added', nameOf(term)));\n };\n\n const remove = (index: number): void => {\n const term = value[index];\n if (term === undefined) {\n return;\n }\n\n onChange(withoutTerm(value, index));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n };\n\n // A value already picked is picked again to take it back, the way the tag's own cross does; a\n // single-choice field swaps its value. The stage stays, so the next value is one pick away.\n const pickValue = (candidate: FilterValue): void => {\n if (field === undefined) {\n return;\n }\n\n const term = fieldTerm(field.id, candidate.id);\n if (picked?.has(candidate.id) === true) {\n onChange(toggledTerm(value, term));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n } else if (field.multiple === false) {\n const ofField = (held: FilterTerm): boolean => isFieldTerm(held) && held.field === field.id;\n const previous = value.find(ofField);\n if (previous === undefined) {\n add(term);\n } else {\n onChange([...value.filter((held) => !ofField(held)), term]);\n announce(t('enonic.uiKit.filterInput.replaced', nameOf(previous), nameOf(term)));\n }\n } else {\n add(term);\n }\n // The pick stays highlighted once the text that narrowed the list is cleared and the list\n // widens again, so Enter again takes this value back rather than the first of the wider list.\n setActiveOption(`${VALUE_OPTION}${candidate.id}`);\n setText('');\n };\n\n const handleTextChange = (typed: string | undefined): void => {\n const named =\n field === undefined ? fieldTyped(fields, typed ?? '', fieldPrefixes) : undefined;\n if (named !== undefined) {\n enterField(named.field, named.text);\n return;\n }\n\n setText(typed ?? '');\n openList();\n };\n\n const handlePick = ([id]: readonly string[]): void => {\n if (id === undefined) {\n return;\n }\n\n const fromKeyboard = keyboardPickRef.current;\n keyboardPickRef.current = false;\n pickedRef.current = true;\n if (id.startsWith(FIELD_OPTION)) {\n enterField(fields.find((candidate) => candidate.id === id.slice(FIELD_OPTION.length)));\n } else if (field !== undefined) {\n const candidate = field.values.find(\n ({ id: valueId }) => valueId === id.slice(VALUE_OPTION.length),\n );\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n // A value picked from the keyboard stays focused, so the arrows go on to the next value.\n if (fromKeyboard) {\n return;\n }\n }\n // ! Synchronously, before the library's blur check runs: the option that was clicked is about to\n // ! unmount with the focus on it, and a focus that landed on the body would read as leaving.\n focusInput();\n };\n\n // Runs on the item before the listbox's own handler toggles it, so the pick knows its source.\n const noteValueKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n keyboardPickRef.current = event.key === 'Enter' || event.key === ' ';\n };\n\n const handleOpenChange = (next: boolean): void => {\n // ! A single-select combobox closes on a pick. Ours stays open — a pick swaps the stage, and a\n // ! value picked is one of several — so the close a pick asks for is the one to ignore.\n if (!next && pickedRef.current) {\n pickedRef.current = false;\n return;\n }\n\n // Leaving the filter abandons a field no value was picked for: a bare `Field:` prefix left in\n // the input would read as a term that narrows nothing.\n if (!next && field !== undefined) {\n enterField(undefined);\n }\n\n setOpen(next);\n };\n\n // Enter toggles the highlighted option while the list is open — the same option Enter on the\n // focused item toggles — or enters the highlighted field. Typed text with no field entered is a\n // search term. With nothing highlighted the stage stays — the \"no matches\" row says why — and a\n // closed list opens.\n const handleEnter = (listOpen: boolean): void => {\n if (field !== undefined) {\n if (!listOpen) {\n openList();\n return;\n }\n\n const candidate = offeredValues.find(({ id }) => `${VALUE_OPTION}${id}` === highlighted);\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n return;\n }\n\n const term = textTerm(text);\n if (term !== undefined) {\n add(term);\n setText('');\n return;\n }\n\n const named = fields.find(({ id }) => `${FIELD_OPTION}${id}` === highlighted);\n if (listOpen && named !== undefined) {\n enterField(named);\n } else {\n openList();\n }\n };\n\n const handleBackspace = (): void => {\n if (field !== undefined) {\n enterField(undefined);\n } else if (value.length > 0) {\n remove(value.length - 1);\n }\n };\n\n const handleEscape = (): boolean => {\n if (field === undefined) {\n return false;\n }\n\n enterField(undefined);\n return true;\n };\n\n // The tags are one tab stop with the input: arrows walk the crosses, Backspace or Delete on a\n // cross takes its tag back and the focus moves one tag left, or home to the input.\n const focusTag = (index: number): boolean => {\n const cross = removeRefs.current[index];\n if (cross === null || cross === undefined) {\n return false;\n }\n\n cross.focus();\n return true;\n };\n\n const handleArrowLeft = (): boolean => focusTag(value.length - 1);\n\n const handleTagKeyDown = (index: number) => (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n focusTag(index - 1);\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n if (!focusTag(index + 1)) {\n focusInput();\n }\n } else if (event.key === 'Backspace' || event.key === 'Delete') {\n event.preventDefault();\n remove(index);\n if (!focusTag(index - 1)) {\n focusInput();\n }\n } else if (event.key === 'Escape') {\n event.preventDefault();\n focusInput();\n }\n };\n\n // A click on the box's own padding is a click on the field: the input takes the focus the body\n // would otherwise get, and the list opens.\n const handleBoxPointerDown = (event: PointerEvent<HTMLDivElement>): void => {\n if (inert || !(event.target instanceof Element) || event.target.closest('button, input')) {\n return;\n }\n\n event.preventDefault();\n focusInput();\n openList();\n };\n\n const clear = (): void => {\n if (value.length > 0) {\n onChange([]);\n }\n announce(clearedLabel);\n enterField(undefined);\n focusInput();\n };\n\n return (\n <Combobox\n open={shown}\n onOpenChange={handleOpenChange}\n value={text}\n onChange={handleTextChange}\n selection={[]}\n onSelectionChange={handlePick}\n active={highlighted}\n setActive={setActiveOption}\n contentType=\"listbox\"\n disabled={disabled}\n error={error}\n >\n {/*\n ! The box is the Content, not the Control: the library anchors its popup to the Control and\n ! gives it the Control's width, so the Control is the input alone — the popup then opens under\n ! the caret, after the tags, and takes the width of what it lists. The border, hover and focus\n ! ring the Control would have drawn move out here, to the box the user sees as the field.\n ! `data-click-outside-ignore` makes the whole box \"inside\" for the library's outside-click\n ! check, which otherwise knows only the Control: a click on a tag's cross or the Filter button\n ! would close the list and drop the field.\n */}\n <Combobox.Content\n data-component={componentName}\n data-click-outside-ignore\n onPointerDown={handleBoxPointerDown}\n className={cn(\n 'bg-surface-neutral border-bdr-subtle flex min-h-12 shrink-0 items-start gap-1.5 rounded-sm border p-1.5',\n 'hover:outline-bdr-subtle hover:outline-2',\n 'focus-within:ring-ring focus-within:ring-offset-ring-offset focus-within:ring-3 focus-within:ring-offset-3 focus-within:outline-none',\n 'transition-highlight',\n shown && 'border-bdr-strong',\n error && 'border-error focus-within:ring-error hover:outline-error',\n disabled && 'pointer-events-none opacity-50',\n className,\n )}\n >\n <Button\n variant=\"text\"\n size=\"sm\"\n startIcon={ListFilter}\n label={filterLabel}\n disabled={inert}\n onClick={() => {\n focusInput();\n openList();\n }}\n className=\"shrink-0\"\n />\n\n {/* Only this middle wraps; the button and the cross stay on the first line, at the edges. */}\n <div className=\"flex min-h-9 min-w-0 flex-1 flex-wrap content-center items-center gap-x-2 gap-y-1.5\">\n {value.map((term, index) => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n\n return (\n <Tag key={termKey(term)} disabled={disabled}>\n {fieldName !== undefined && (\n <Tag.Prefix>{t('enonic.uiKit.filterInput.fieldPrefix', fieldName)}</Tag.Prefix>\n )}\n <Tag.Label>{valueName}</Tag.Label>\n {!readOnly && (\n <Tag.Remove\n ref={(node: HTMLButtonElement | null) => {\n removeRefs.current[index] = node;\n }}\n tabIndex={-1}\n aria-label={t('enonic.uiKit.filterInput.remove', nameOf(term))}\n disabled={disabled}\n onClick={() => {\n remove(index);\n focusInput();\n }}\n onKeyDown={handleTagKeyDown(index)}\n />\n )}\n </Tag>\n );\n })}\n\n {field !== undefined && (\n <span className=\"text-subtle shrink-0 whitespace-nowrap\">\n {t('enonic.uiKit.filterInput.fieldPrefix', field.label)}\n </span>\n )}\n\n {/* Read-only, the tags are the whole content: an input that takes nothing would be a\n focus stop with nothing to do, and a placeholder that invites a search it cannot take. */}\n {!readOnly && (\n <Combobox.Control className=\"h-auto min-h-0 min-w-40 flex-1 rounded-none border-0 bg-transparent focus-within:ring-0 focus-within:ring-offset-0\">\n <Combobox.Search className=\"h-auto bg-transparent px-1 py-0 hover:outline-none\">\n <FilterTextInput\n inputRef={composedRef}\n text={text}\n placeholder={field === undefined ? placeholder : valuePlaceholder}\n // The field the values belong to is in the name, since the prefix beside the\n // input is not associated with it.\n label={\n field === undefined\n ? placeholder\n : t('enonic.uiKit.filterInput.term', field.label, valuePlaceholder)\n }\n onOpen={openList}\n onEnter={handleEnter}\n onBackspace={handleBackspace}\n onEscape={handleEscape}\n onArrowLeft={handleArrowLeft}\n />\n </Combobox.Search>\n </Combobox.Control>\n )}\n </div>\n\n {active && (\n <IconButton\n icon={X}\n variant=\"text\"\n iconSize={28}\n iconStrokeWidth={1.25}\n title={clearLabel}\n aria-label={clearLabel}\n disabled={disabled}\n onClick={clear}\n className=\"text-subtle my-1 mr-1 size-7 shrink-0\"\n />\n )}\n\n <div aria-live=\"polite\" className=\"sr-only\">\n <span key={announcement.key}>{announcement.text}</span>\n </div>\n\n <Combobox.Portal>\n {/* ? `width: auto` undoes the Control's width the library sets inline; the list sizes to its labels. */}\n <Combobox.Popup style={{ width: 'auto' }} className=\"max-w-md min-w-72\">\n {/* Keyed by stage, so the list activates its first option again when the options swap. */}\n <Combobox.ListContent\n key={stage}\n aria-label={field === undefined ? filterLabel : field.label}\n // ? The listbox is left in single mode so Enter on a focused option picks it; what the\n // ? values stage is — several ticks at once — is said here instead.\n aria-multiselectable={\n field !== undefined && field.multiple !== false ? true : undefined\n }\n className=\"max-h-72 gap-y-0.5 overflow-y-auto p-2\"\n >\n {offeredFields.map(({ id, label, icon: Icon }) => (\n <Listbox.Item\n key={id}\n value={`${FIELD_OPTION}${id}`}\n className=\"data-[active]:bg-surface-neutral-hover gap-2.5 rounded-sm px-3 py-2\"\n >\n {Icon !== undefined && (\n <Icon\n size={16}\n strokeWidth={1.5}\n className=\"text-subtle shrink-0\"\n aria-hidden\n />\n )}\n <span className=\"truncate\">{label}</span>\n </Listbox.Item>\n ))}\n\n {field?.notice !== undefined && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2 text-xs\">\n {field.notice}\n </div>\n )}\n\n {field?.loading === true && (\n <div role=\"presentation\" aria-busy className=\"text-subtle px-3 py-2\">\n {loadingLabel}\n </div>\n )}\n\n {field !== undefined && field.loading !== true && offeredValues.length === 0 && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2\">\n {noMatchesLabel}\n </div>\n )}\n\n {offeredGroups.map(({ label: groupLabel, values }, groupIndex) => {\n const groupHeadingId =\n groupLabel === undefined ? undefined : `${headingId}-group-${groupIndex}`;\n\n return (\n <div\n key={groupLabel === undefined ? 'ungrouped' : `group:${groupLabel}`}\n role=\"group\"\n aria-labelledby={groupHeadingId}\n className=\"contents\"\n >\n {groupLabel !== undefined && (\n <div\n role=\"presentation\"\n className=\"flex w-full items-baseline gap-2.5 px-3 pt-2.5 pb-1\"\n >\n <span\n id={groupHeadingId}\n className=\"text-subtle min-w-0 truncate text-xs tracking-wider uppercase\"\n >\n {groupLabel}\n </span>\n <span className=\"border-bdr-subtle min-w-6 flex-1 border-b\" />\n </div>\n )}\n\n {values.map((candidate) => {\n const selected = picked?.has(candidate.id) === true;\n\n return (\n <Listbox.Item\n key={candidate.id}\n value={`${VALUE_OPTION}${candidate.id}`}\n // Picked or not, the value stays pickable: picking it again takes the term back.\n disabled={!selected && !isOffered(candidate)}\n // ? The listbox's own selection stays empty, so Enter keeps picking the option;\n // ? the item spreads props after its own, which is what lets the state be said here.\n aria-selected={selected}\n className=\"data-[active]:bg-surface-neutral-hover rounded-sm px-3 py-2\"\n onKeyDown={noteValueKeyDown}\n >\n <span className=\"grow truncate\">{candidate.label}</span>\n {selected && (\n <Check size={16} strokeWidth={2} className=\"shrink-0\" aria-hidden />\n )}\n {candidate.count !== undefined && (\n <span className=\"text-subtle tabular-nums\">({candidate.count})</span>\n )}\n </Listbox.Item>\n );\n })}\n </div>\n );\n })}\n </Combobox.ListContent>\n </Combobox.Popup>\n </Combobox.Portal>\n </Combobox.Content>\n </Combobox>\n );\n },\n);\n\nFilterInput.displayName = FILTER_INPUT_NAME;\n\nexport { FilterInput };\n\n//\n// * Internal\n//\n\n/**\n * Whether the key belongs to an IME composition. React keeps the flag on `nativeEvent`, Preact on\n * the event itself; 229 is the key code every browser reports for a composing key.\n */\nfunction isComposing(event: {\n isComposing?: boolean;\n keyCode?: number;\n nativeEvent?: { isComposing?: boolean; keyCode?: number };\n}): boolean {\n const native = event.nativeEvent ?? event;\n return native.isComposing === true || native.keyCode === 229;\n}\n\ntype FilterTextInputProps = {\n inputRef: Ref<HTMLInputElement>;\n text: string;\n placeholder: string;\n label: string;\n onOpen: () => void;\n /** With whether the list is open. */\n onEnter: (listOpen: boolean) => void;\n onBackspace: () => void;\n /** Answers whether it consumed the key; otherwise the library closes the dropdown. */\n onEscape: () => boolean;\n /** Answers whether a tag took the focus. */\n onArrowLeft: () => boolean;\n};\n\n/**\n * The input, with the keys a tag input adds to a combobox's: Enter picks or commits, Backspace on an\n * empty input takes the last tag back, Escape steps out of a field, ArrowLeft at the start walks into\n * the tags. Everything else — the arrows into the list, Escape closing — is the library's, reached\n * through its context. A key pressed mid-composition is the IME's.\n */\nconst FilterTextInput = ({\n inputRef,\n text,\n placeholder,\n label,\n onOpen,\n onEnter,\n onBackspace,\n onEscape,\n onArrowLeft,\n}: FilterTextInputProps): ReactElement => {\n const { keyHandler, open } = useCombobox();\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (isComposing(event)) {\n return;\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n onEnter(open);\n return;\n }\n\n if (event.key === 'Backspace' && text.length === 0 && !event.repeat) {\n onBackspace();\n return;\n }\n\n if (event.key === 'Escape' && onEscape()) {\n event.preventDefault();\n return;\n }\n\n const atStart =\n event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;\n if (event.key === 'ArrowLeft' && atStart && onArrowLeft()) {\n event.preventDefault();\n return;\n }\n\n keyHandler(event);\n };\n\n return (\n <Combobox.Input\n ref={inputRef}\n // The library's input names itself `Search` in English; the caller's own prompt is the name here.\n aria-label={label}\n placeholder={placeholder}\n className=\"bg-transparent\"\n onClick={onOpen}\n onKeyDown={handleKeyDown}\n />\n );\n};\nFilterTextInput.displayName = 'FilterInput.TextInput';\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ //#region src/filter/filter-input.phrases.d.ts
2
+ /**
3
+ * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it
4
+ * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders
5
+ * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its
6
+ * bundle lacks.
7
+ */
8
+ declare const filterInputPhrases: {
9
+ readonly 'enonic.uiKit.filterInput.label': 'Filter';
10
+ readonly 'enonic.uiKit.filterInput.placeholder': 'Search';
11
+ readonly 'enonic.uiKit.filterInput.valuePlaceholder': 'Pick a value';
12
+ readonly 'enonic.uiKit.filterInput.clear': 'Clear the filter';
13
+ readonly 'enonic.uiKit.filterInput.cleared': 'Filter cleared';
14
+ readonly 'enonic.uiKit.filterInput.noMatches': 'No matches';
15
+ readonly 'enonic.uiKit.filterInput.loading': 'Loading…';
16
+ readonly 'enonic.uiKit.filterInput.fieldPrefix': '{0}:';
17
+ readonly 'enonic.uiKit.filterInput.term': '{0}: {1}';
18
+ readonly 'enonic.uiKit.filterInput.remove': 'Remove {0}';
19
+ readonly 'enonic.uiKit.filterInput.added': 'Added {0}';
20
+ readonly 'enonic.uiKit.filterInput.removed': 'Removed {0}';
21
+ readonly 'enonic.uiKit.filterInput.replaced': 'Replaced {0} with {1}';
22
+ };
23
+ type FilterInputPhraseKey = keyof typeof filterInputPhrases;
24
+ //#endregion
25
+ export { FilterInputPhraseKey, filterInputPhrases };
26
+ //# sourceMappingURL=filter-input.phrases.d.ts.map
@@ -0,0 +1,26 @@
1
+ //#region src/filter/filter-input.phrases.ts
2
+ /**
3
+ * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it
4
+ * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders
5
+ * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its
6
+ * bundle lacks.
7
+ */
8
+ const filterInputPhrases = {
9
+ "enonic.uiKit.filterInput.label": "Filter",
10
+ "enonic.uiKit.filterInput.placeholder": "Search",
11
+ "enonic.uiKit.filterInput.valuePlaceholder": "Pick a value",
12
+ "enonic.uiKit.filterInput.clear": "Clear the filter",
13
+ "enonic.uiKit.filterInput.cleared": "Filter cleared",
14
+ "enonic.uiKit.filterInput.noMatches": "No matches",
15
+ "enonic.uiKit.filterInput.loading": "Loading…",
16
+ "enonic.uiKit.filterInput.fieldPrefix": "{0}:",
17
+ "enonic.uiKit.filterInput.term": "{0}: {1}",
18
+ "enonic.uiKit.filterInput.remove": "Remove {0}",
19
+ "enonic.uiKit.filterInput.added": "Added {0}",
20
+ "enonic.uiKit.filterInput.removed": "Removed {0}",
21
+ "enonic.uiKit.filterInput.replaced": "Replaced {0} with {1}"
22
+ };
23
+ //#endregion
24
+ export { filterInputPhrases };
25
+
26
+ //# sourceMappingURL=filter-input.phrases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-input.phrases.js","names":[],"sources":["../../src/filter/filter-input.phrases.ts"],"sourcesContent":["/**\n * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it\n * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders\n * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its\n * bundle lacks.\n */\nexport const filterInputPhrases = {\n 'enonic.uiKit.filterInput.label': 'Filter',\n 'enonic.uiKit.filterInput.placeholder': 'Search',\n 'enonic.uiKit.filterInput.valuePlaceholder': 'Pick a value',\n 'enonic.uiKit.filterInput.clear': 'Clear the filter',\n 'enonic.uiKit.filterInput.cleared': 'Filter cleared',\n 'enonic.uiKit.filterInput.noMatches': 'No matches',\n 'enonic.uiKit.filterInput.loading': 'Loading…',\n 'enonic.uiKit.filterInput.fieldPrefix': '{0}:',\n 'enonic.uiKit.filterInput.term': '{0}: {1}',\n 'enonic.uiKit.filterInput.remove': 'Remove {0}',\n 'enonic.uiKit.filterInput.added': 'Added {0}',\n 'enonic.uiKit.filterInput.removed': 'Removed {0}',\n 'enonic.uiKit.filterInput.replaced': 'Replaced {0} with {1}',\n} as const;\n\nexport type FilterInputPhraseKey = keyof typeof filterInputPhrases;\n"],"mappings":";;;;;;;AAMA,MAAa,qBAAqB;CAChC,kCAAkC;CAClC,wCAAwC;CACxC,6CAA6C;CAC7C,kCAAkC;CAClC,oCAAoC;CACpC,sCAAsC;CACtC,oCAAoC;CACpC,wCAAwC;CACxC,iCAAiC;CACjC,mCAAmC;CACnC,kCAAkC;CAClC,oCAAoC;CACpC,qCAAqC;AACvC"}
@@ -0,0 +1,40 @@
1
+ //#region src/filter/filter-query.d.ts
2
+ /**
3
+ * One term of a filter: a value of a named field, or free text. Ids, never labels — what a term
4
+ * is called is the business of whoever offers the fields. Discriminated by `kind`, so a term shape
5
+ * added later fails to compile where it is not handled instead of passing for one of these.
6
+ */
7
+ type FieldTerm = {
8
+ kind: 'field';
9
+ field: string;
10
+ value: string;
11
+ };
12
+ type TextTerm = {
13
+ kind: 'text';
14
+ text: string;
15
+ };
16
+ type FilterTerm = FieldTerm | TextTerm;
17
+ /** What a list is narrowed by, in the order the terms were added. */
18
+ type FilterQuery = readonly FilterTerm[];
19
+ declare const EMPTY_FILTER: FilterQuery;
20
+ declare function fieldTerm(field: string, value: string): FieldTerm;
21
+ declare function isTextTerm(term: FilterTerm): term is TextTerm;
22
+ declare function isFieldTerm(term: FilterTerm): term is FieldTerm;
23
+ /** A key that tells terms apart in a list, stable across renders; a tuple, so no id can collide with another kind. */
24
+ declare function termKey(term: FilterTerm): string;
25
+ /** The free text of a query as one string, the text terms joined by a space. */
26
+ declare function textOf(query: FilterQuery): string;
27
+ /** Every value the query holds for a field. Empty narrows nothing, the reading every multi-select filter takes. */
28
+ declare function valuesOf(query: FilterQuery, field: string): ReadonlySet<string>;
29
+ /** Text is compared without regard to case, as the matching reads it; a value by its id, exactly. */
30
+ declare function sameTerm(a: FilterTerm, b: FilterTerm): boolean;
31
+ /** A text term from what was typed, or nothing for a blank. */
32
+ declare function textTerm(typed: string): TextTerm | undefined;
33
+ /** The query with a term added at the end; one it already holds leaves it unchanged. */
34
+ declare function withTerm(query: FilterQuery, term: FilterTerm): FilterQuery;
35
+ declare function withoutTerm(query: FilterQuery, index: number): FilterQuery;
36
+ /** The query with a term taken out if it holds it, and added at the end if it does not. */
37
+ declare function toggledTerm(query: FilterQuery, term: FilterTerm): FilterQuery;
38
+ //#endregion
39
+ export { EMPTY_FILTER, FieldTerm, FilterQuery, FilterTerm, TextTerm, fieldTerm, isFieldTerm, isTextTerm, sameTerm, termKey, textOf, textTerm, toggledTerm, valuesOf, withTerm, withoutTerm };
40
+ //# sourceMappingURL=filter-query.d.ts.map