@es-joy/jsoe 0.28.2 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/scheduled_tasks.lock +1 -0
- package/CHANGES.md +25 -0
- package/README.md +4 -0
- package/dist/formatAndTypeChoices.d.ts +6 -6
- package/dist/formatAndTypeChoices.d.ts.map +1 -1
- package/dist/formats/schema.d.ts +5 -0
- package/dist/formats/schema.d.ts.map +1 -1
- package/dist/formats.d.ts +2 -2
- package/dist/formats.d.ts.map +1 -1
- package/dist/fundamentalTypes/dateType.d.ts +26 -2
- package/dist/fundamentalTypes/dateType.d.ts.map +1 -1
- package/dist/fundamentalTypes/domexceptionType.d.ts +7 -0
- package/dist/fundamentalTypes/domexceptionType.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/jtltExtensions.d.ts +112 -0
- package/dist/jtltExtensions.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/arraySearchType.d.ts +22 -0
- package/dist/search/fundamentalTypes/arraySearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/bigintSearchType.d.ts +10 -0
- package/dist/search/fundamentalTypes/bigintSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/blobSearchType.d.ts +19 -0
- package/dist/search/fundamentalTypes/blobSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/booleanSearchType.d.ts +11 -0
- package/dist/search/fundamentalTypes/booleanSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/catchSearchType.d.ts +18 -0
- package/dist/search/fundamentalTypes/catchSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/dateSearchType.d.ts +15 -0
- package/dist/search/fundamentalTypes/dateSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts +16 -0
- package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/enumSearchType.d.ts +26 -0
- package/dist/search/fundamentalTypes/enumSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/errorSearchType.d.ts +9 -0
- package/dist/search/fundamentalTypes/errorSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/fileSearchType.d.ts +28 -0
- package/dist/search/fundamentalTypes/fileSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/filelistSearchType.d.ts +24 -0
- package/dist/search/fundamentalTypes/filelistSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/functionSearchType.d.ts +18 -0
- package/dist/search/fundamentalTypes/functionSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/mapSearchType.d.ts +22 -0
- package/dist/search/fundamentalTypes/mapSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/nanSearchType.d.ts +7 -0
- package/dist/search/fundamentalTypes/nanSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/nullSearchType.d.ts +7 -0
- package/dist/search/fundamentalTypes/nullSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/numberSearchType.d.ts +11 -0
- package/dist/search/fundamentalTypes/numberSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/objectSearchType.d.ts +21 -0
- package/dist/search/fundamentalTypes/objectSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/promiseSearchType.d.ts +20 -0
- package/dist/search/fundamentalTypes/promiseSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/regexpSearchType.d.ts +20 -0
- package/dist/search/fundamentalTypes/regexpSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/setSearchType.d.ts +15 -0
- package/dist/search/fundamentalTypes/setSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/stringSearchType.d.ts +21 -0
- package/dist/search/fundamentalTypes/stringSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/symbolSearchType.d.ts +16 -0
- package/dist/search/fundamentalTypes/symbolSearchType.d.ts.map +1 -0
- package/dist/search/fundamentalTypes/undefinedSearchType.d.ts +7 -0
- package/dist/search/fundamentalTypes/undefinedSearchType.d.ts.map +1 -0
- package/dist/search/index.d.ts +70 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/queryTree.d.ts +12 -1
- package/dist/search/queryTree.d.ts.map +1 -1
- package/dist/search/queryTreeBuilders.d.ts +18 -0
- package/dist/search/queryTreeBuilders.d.ts.map +1 -1
- package/dist/search/searchDispatch.d.ts +44 -0
- package/dist/search/searchDispatch.d.ts.map +1 -1
- package/dist/search/searchElementUtils.d.ts +106 -0
- package/dist/search/searchElementUtils.d.ts.map +1 -0
- package/dist/search/searchUtils.d.ts +550 -0
- package/dist/search/searchUtils.d.ts.map +1 -0
- package/dist/search/subTypes/blobHTMLSearchType.d.ts +36 -0
- package/dist/search/subTypes/blobHTMLSearchType.d.ts.map +1 -0
- package/dist/search/subTypes/recordSearchType.d.ts +18 -0
- package/dist/search/subTypes/recordSearchType.d.ts.map +1 -0
- package/dist/search/subTypes/tupleSearchType.d.ts +21 -0
- package/dist/search/subTypes/tupleSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/SpecialRealNumberSearchType.d.ts +14 -0
- package/dist/search/superTypes/SpecialRealNumberSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/buffersourceSearchType.d.ts +17 -0
- package/dist/search/superTypes/buffersourceSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/dommatrixSearchType.d.ts +11 -0
- package/dist/search/superTypes/dommatrixSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/dompointSearchType.d.ts +9 -0
- package/dist/search/superTypes/dompointSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/domrectSearchType.d.ts +9 -0
- package/dist/search/superTypes/domrectSearchType.d.ts.map +1 -0
- package/dist/search/superTypes/errorsSpecialSearchType.d.ts +11 -0
- package/dist/search/superTypes/errorsSpecialSearchType.d.ts.map +1 -0
- package/dist/search/unions/discriminatedUnionSearchType.d.ts +12 -0
- package/dist/search/unions/discriminatedUnionSearchType.d.ts.map +1 -0
- package/dist/search/unions/unionFamilySearchType.d.ts +36 -0
- package/dist/search/unions/unionFamilySearchType.d.ts.map +1 -0
- package/dist/search/unions/unionSearchType.d.ts +8 -0
- package/dist/search/unions/unionSearchType.d.ts.map +1 -0
- package/dist/search/unions/xorSearchType.d.ts +12 -0
- package/dist/search/unions/xorSearchType.d.ts.map +1 -0
- package/dist/superTypes/buffersourceType.d.ts.map +1 -1
- package/dist/typeChoices.d.ts +3 -3
- package/dist/typeChoices.d.ts.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -4
- package/pnpm-workspace.yaml +1 -1
- package/rollup.config.js +12 -0
- package/src/formatAndTypeChoices.js +4 -4
- package/src/formats/schema.js +1 -1
- package/src/formats.js +1 -1
- package/src/fundamentalTypes/dateType.js +40 -14
- package/src/fundamentalTypes/domexceptionType.js +41 -33
- package/src/fundamentalTypes/regexpType.js +1 -1
- package/src/index.js +6 -0
- package/src/jsoe.css +204 -0
- package/src/jtltExtensions.js +103 -0
- package/src/search/fundamentalTypes/arraySearchType.js +101 -0
- package/src/search/fundamentalTypes/bigintSearchType.js +68 -0
- package/src/search/fundamentalTypes/blobSearchType.js +42 -0
- package/src/search/fundamentalTypes/booleanSearchType.js +40 -0
- package/src/search/fundamentalTypes/catchSearchType.js +59 -0
- package/src/search/fundamentalTypes/dateSearchType.js +166 -0
- package/src/search/fundamentalTypes/domexceptionSearchType.js +84 -0
- package/src/search/fundamentalTypes/enumSearchType.js +62 -0
- package/src/search/fundamentalTypes/errorSearchType.js +11 -0
- package/src/search/fundamentalTypes/fileSearchType.js +94 -0
- package/src/search/fundamentalTypes/filelistSearchType.js +103 -0
- package/src/search/fundamentalTypes/functionSearchType.js +102 -0
- package/src/search/fundamentalTypes/mapSearchType.js +117 -0
- package/src/search/fundamentalTypes/nanSearchType.js +11 -0
- package/src/search/fundamentalTypes/nullSearchType.js +11 -0
- package/src/search/fundamentalTypes/numberSearchType.js +59 -0
- package/src/search/fundamentalTypes/objectSearchType.js +367 -0
- package/src/search/fundamentalTypes/promiseSearchType.js +61 -0
- package/src/search/fundamentalTypes/regexpSearchType.js +72 -0
- package/src/search/fundamentalTypes/setSearchType.js +91 -0
- package/src/search/fundamentalTypes/stringSearchType.js +44 -0
- package/src/search/fundamentalTypes/symbolSearchType.js +39 -0
- package/src/search/fundamentalTypes/undefinedSearchType.js +11 -0
- package/src/search/index.js +96 -0
- package/src/search/queryTree.js +244 -0
- package/src/search/queryTreeBuilders.js +233 -0
- package/src/search/searchDispatch.js +291 -0
- package/src/search/searchElementUtils.js +373 -0
- package/src/search/searchUtils.js +847 -0
- package/src/search/subTypes/blobHTMLSearchType.js +122 -0
- package/src/search/subTypes/recordSearchType.js +105 -0
- package/src/search/subTypes/tupleSearchType.js +152 -0
- package/src/search/superTypes/SpecialRealNumberSearchType.js +42 -0
- package/src/search/superTypes/buffersourceSearchType.js +53 -0
- package/src/search/superTypes/dommatrixSearchType.js +24 -0
- package/src/search/superTypes/dompointSearchType.js +15 -0
- package/src/search/superTypes/domrectSearchType.js +15 -0
- package/src/search/superTypes/errorsSpecialSearchType.js +15 -0
- package/src/search/unions/discriminatedUnionSearchType.js +16 -0
- package/src/search/unions/unionFamilySearchType.js +168 -0
- package/src/search/unions/unionSearchType.js +12 -0
- package/src/search/unions/xorSearchType.js +16 -0
- package/src/superTypes/buffersourceType.js +13 -15
- package/src/typeChoices.js +2 -2
- package/src/types.js +2 -2
- package/tsconfig-prod.json +12 -0
- package/tsconfig.json +5 -10
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import {schemaLabel} from '../utils/schemaMeta.js';
|
|
2
|
+
import {getJSONPointerParts} from '../utils/jsonPointer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('../types.js').JamilihArray} JamilihArray
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Label for a leaf/container's own control, preferring the schema's own
|
|
10
|
+
* `meta.title`/`meta.description`/`description` (via `schemaLabel`, same
|
|
11
|
+
* precedence the value-editing side uses) and falling back to the JSON
|
|
12
|
+
* Pointer path's final segment so every control still has a legible label
|
|
13
|
+
* even when the schema carries no metadata.
|
|
14
|
+
* @param {import('../formats/schema.js').ZodexSchema|undefined} schemaObject
|
|
15
|
+
* @param {string} path
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function buildPathLabel (schemaObject, path) {
|
|
19
|
+
const label = schemaLabel(schemaObject);
|
|
20
|
+
if (label) {
|
|
21
|
+
return label;
|
|
22
|
+
}
|
|
23
|
+
const parts = getJSONPointerParts(path);
|
|
24
|
+
return parts.length ? String(parts.at(-1)) : path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds a control belonging directly to `root`'s own widget - as opposed to
|
|
29
|
+
* one belonging to a search widget recursively nested inside it (e.g.
|
|
30
|
+
* `arraySearchType.js`'s own "size" input vs. a nested array-of-arrays'
|
|
31
|
+
* element widget's own "size" input, both reachable from `root` via a plain
|
|
32
|
+
* `querySelectorAll`). A match only counts if the nearest ancestor of the
|
|
33
|
+
* match carrying `data-search-path` is `root` itself; every search element
|
|
34
|
+
* carries that attribute (`searchElementUtils.js`'s `findSearchElement`), so
|
|
35
|
+
* this reliably stops at the first nested search element's boundary.
|
|
36
|
+
*
|
|
37
|
+
* This is also why every `build*`/`read*` pair below reads its class-based
|
|
38
|
+
* selector back through this helper rather than a raw
|
|
39
|
+
* `root.querySelector(...)`: a fixed class name is safe to reuse across
|
|
40
|
+
* every instance of a given control on the page precisely because lookups
|
|
41
|
+
* are always scoped this way, and - unlike a `name` built from a
|
|
42
|
+
* `typeNamespace` closed over inside a `$define` mixin, which is installed
|
|
43
|
+
* once on the shared custom-element prototype the first time a tag is
|
|
44
|
+
* defined - nothing here depends on a per-instance closure at all.
|
|
45
|
+
* @param {Element} root
|
|
46
|
+
* @param {string} selector
|
|
47
|
+
* @returns {HTMLElement|undefined}
|
|
48
|
+
*/
|
|
49
|
+
export function findOwnControl (root, selector) {
|
|
50
|
+
return /** @type {HTMLElement[]} */ (
|
|
51
|
+
[...root.querySelectorAll(selector)]
|
|
52
|
+
).find((el) => el.closest('[data-search-path]') === root);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether `el` sits inside an `objectSearchType.js` has-property row whose
|
|
57
|
+
* own "Has"/"Doesn't have" tri-state is *already* explicitly chosen (not
|
|
58
|
+
* "(any)") - if so, that existence assertion alone is already a complete
|
|
59
|
+
* constraint for the row, so a per-field "no absent values" requirement
|
|
60
|
+
* inside it (a range's own "at least one bound", `buildRangeInputsPair`'s
|
|
61
|
+
* doc) should be relaxed. `setDescendantsRequired` handles this for plain
|
|
62
|
+
* `required`-attribute controls, but a range pair's requirement is
|
|
63
|
+
* re-asserted imperatively on every `input`/`change` via `setCustomValidity`
|
|
64
|
+
* (`syncRangeValidity`, `dateSearchType.js`'s own equivalent) - clearing it
|
|
65
|
+
* once wouldn't stick past the next keystroke, so those call this directly,
|
|
66
|
+
* live, instead.
|
|
67
|
+
*
|
|
68
|
+
* Returns `false` (not exempted) when `el` also sits inside a
|
|
69
|
+
* `buildOptInFieldset` fieldset (`jsoeSearchOptInFieldset--*`), for the same
|
|
70
|
+
* reason `setDescendantsRequired` skips those controls: a dimension range
|
|
71
|
+
* nested in `makeDomShapeSearchType`'s per-dimension opt-in gate (say,
|
|
72
|
+
* DOMRect's "Width") is only reachable at all once its own checkbox is
|
|
73
|
+
* checked, and checking it is itself the user's explicit request to
|
|
74
|
+
* constrain *that* facet - an enclosing "Has property" shouldn't then let
|
|
75
|
+
* its "From"/"To" sit blank and still read as valid, any more than
|
|
76
|
+
* `fileSearchType.js`'s "Name" facet should once opted into.
|
|
77
|
+
* @param {Element} el
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function isExemptedByAncestorHasProperty (el) {
|
|
81
|
+
if (el.closest('fieldset[class^="jsoeSearchOptInFieldset--"]')) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const row = el.closest('jsoe-search-has-property');
|
|
85
|
+
if (!row) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const select = /** @type {HTMLSelectElement|undefined} */ (
|
|
89
|
+
findOwnControl(row, 'select.jsoeSearchTriState--')
|
|
90
|
+
);
|
|
91
|
+
return select !== undefined && select.value !== '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Re-runs every `input`/`change`-driven validator within `root` by
|
|
96
|
+
* re-dispatching those events on each descendant form control -
|
|
97
|
+
* `objectSearchType.js`'s `buildHasPropertyRow` calls this when its own
|
|
98
|
+
* "Has"/"Doesn't have" tri-state changes, since a range pair (or anything
|
|
99
|
+
* else reactive) nested anywhere inside the child widget needs to
|
|
100
|
+
* re-evaluate `isExemptedByAncestorHasProperty` against the *new* tri-state
|
|
101
|
+
* value - nothing else would otherwise prompt it to run again until the
|
|
102
|
+
* user happens to interact with that specific field themselves.
|
|
103
|
+
* @param {Element} root
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
export function revalidateDescendants (root) {
|
|
107
|
+
[...root.querySelectorAll('input, select, textarea')].forEach((el) => {
|
|
108
|
+
el.dispatchEvent(new Event('input'));
|
|
109
|
+
el.dispatchEvent(new Event('change'));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The `buildRangeInputsPair` cross-validation check, factored out so a
|
|
115
|
+
* consuming widget's own `$define.connectedCallback` (custom elements,
|
|
116
|
+
* search plan §8) can also call it - the `input`/`change` events
|
|
117
|
+
* `buildRangeInputsPair` wires below only fire from the user's *first*
|
|
118
|
+
* interaction, so a freshly-built pair with both ends still empty would
|
|
119
|
+
* otherwise stay "valid" by the browser's reckoning (no `setCustomValidity`
|
|
120
|
+
* call has ever run yet) until then, contradicting the "leaving both blank
|
|
121
|
+
* is invalid" rule documented below. `connectedCallback` is a native
|
|
122
|
+
* Custom Elements lifecycle method (invoked once per instance the moment it
|
|
123
|
+
* connects to the document), so calling this from there closes that gap
|
|
124
|
+
* without needing any build-time live element reference.
|
|
125
|
+
*
|
|
126
|
+
* "Leaving both blank is invalid" is itself relaxed when
|
|
127
|
+
* `isExemptedByAncestorHasProperty` says so - a range nested as a has-
|
|
128
|
+
* property row's own child widget (e.g. a `date`/`number` property) needs
|
|
129
|
+
* the same "'Has property X' is already a complete constraint" relief
|
|
130
|
+
* `setDescendantsRequired` gives `required`-attribute controls, just
|
|
131
|
+
* re-checked live on every call rather than toggled once, since this runs
|
|
132
|
+
* imperatively on every keystroke regardless.
|
|
133
|
+
* @param {Element} root
|
|
134
|
+
* @param {string} [key]
|
|
135
|
+
* @returns {void}
|
|
136
|
+
*/
|
|
137
|
+
export function syncRangeValidity (root, key = '') {
|
|
138
|
+
const gteEl = /** @type {HTMLInputElement|undefined} */ (
|
|
139
|
+
findOwnControl(root, `input.jsoeSearchRangeGte--${key}`)
|
|
140
|
+
);
|
|
141
|
+
const lteEl = /** @type {HTMLInputElement|undefined} */ (
|
|
142
|
+
findOwnControl(root, `input.jsoeSearchRangeLte--${key}`)
|
|
143
|
+
);
|
|
144
|
+
if (!gteEl || !lteEl) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const bothEmpty = gteEl.value === '' && lteEl.value === '' &&
|
|
148
|
+
!isExemptedByAncestorHasProperty(gteEl);
|
|
149
|
+
const outOfOrder = gteEl.value !== '' && lteEl.value !== '' &&
|
|
150
|
+
Number(lteEl.value) < Number(gteEl.value);
|
|
151
|
+
const emptyMessage = 'Enter at least one bound (From or To).';
|
|
152
|
+
gteEl.setCustomValidity(bothEmpty ? emptyMessage : '');
|
|
153
|
+
let lteMessage = '';
|
|
154
|
+
if (bothEmpty) {
|
|
155
|
+
lteMessage = emptyMessage;
|
|
156
|
+
} else if (outOfOrder) {
|
|
157
|
+
lteMessage = 'End of range must not be less than the start of the range.';
|
|
158
|
+
}
|
|
159
|
+
lteEl.setCustomValidity(lteMessage);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A "from"/"to" pair of native inputs sharing one `type`, for the OR-range/
|
|
164
|
+
* Is-Not-Range README affordance (number/bigint/buffersource; `date` uses
|
|
165
|
+
* `dateType.js`'s own `buildDateInputControl` instead, since a
|
|
166
|
+
* `datetime-local` input needs its own ISO-slicing).
|
|
167
|
+
*
|
|
168
|
+
* `key` distinguishes multiple range pairs *within one widget* (e.g.
|
|
169
|
+
* `domrectSearchType.js`'s `x`/`y`/`width`/`height` dimensions, or
|
|
170
|
+
* `errorSearchType.js`'s `lineNumber`/`columnNumber`): `findOwnControl`
|
|
171
|
+
* already isolates one widget's controls from another's, but can't tell
|
|
172
|
+
* apart two same-class controls belonging to the *same* widget, so those
|
|
173
|
+
* need distinct classes. Leave it at the default `''` for the common case
|
|
174
|
+
* of a widget with only one range pair.
|
|
175
|
+
*
|
|
176
|
+
* Cross-validates the pair via the native Constraint Validation API,
|
|
177
|
+
* surfaced by `jsoe.css`'s `input:invalid` styling and a real `<form>`'s
|
|
178
|
+
* `reportValidity`/`checkValidity` (`src/search/index.js`): leaving *both*
|
|
179
|
+
* ends blank is invalid - the same "no absent values" reasoning
|
|
180
|
+
* `buildLiteralRegexControls`'s Value input documents, an added range row
|
|
181
|
+
* needs at least one bound to mean anything - and if both ends are filled
|
|
182
|
+
* with "To" less than "From", that's invalid too. An open-ended range (only
|
|
183
|
+
* one end filled) stays valid. A row nested under an
|
|
184
|
+
* `objectSearchType.js` required-property's opt-in `<fieldset disabled>`
|
|
185
|
+
* (or, for `dateSearchType.js`'s own equivalent pair, its own
|
|
186
|
+
* "Invalid date" fieldset) is unaffected either way, since a disabled field
|
|
187
|
+
* is excluded from constraint validation entirely. Every caller must also
|
|
188
|
+
* call `syncRangeValidity` from its own `connectedCallback` (see that
|
|
189
|
+
* function's doc) so the initial both-blank state is actually invalid from
|
|
190
|
+
* the moment the widget exists, not just after the user's first keystroke.
|
|
191
|
+
* `dateSearchType.js`'s own `datetime-local` pair needs the same two checks
|
|
192
|
+
* but isn't built through this helper (`buildDateInputControl` handles its
|
|
193
|
+
* own ISO-slicing), so it wires an equivalent handler itself.
|
|
194
|
+
* @param {{
|
|
195
|
+
* name: string,
|
|
196
|
+
* key?: string,
|
|
197
|
+
* type?: string,
|
|
198
|
+
* min?: string|number,
|
|
199
|
+
* max?: string|number,
|
|
200
|
+
* step?: string|number
|
|
201
|
+
* }} cfg
|
|
202
|
+
* @returns {JamilihArray[]}
|
|
203
|
+
*/
|
|
204
|
+
export function buildRangeInputsPair ({
|
|
205
|
+
name, key = '', type = 'number', min, max, step
|
|
206
|
+
}) {
|
|
207
|
+
/**
|
|
208
|
+
* `this` is whichever of the pair fired the event - `syncRangeValidity`
|
|
209
|
+
* looks up both fresh via `findOwnControl` rather than closing over
|
|
210
|
+
* either, so it stays correct however many `buildRangeInputsPair` pairs
|
|
211
|
+
* (of possibly-differing `key`s) end up sharing the same widget.
|
|
212
|
+
* @this {HTMLElement}
|
|
213
|
+
* @returns {void}
|
|
214
|
+
*/
|
|
215
|
+
function validateRange () {
|
|
216
|
+
const root = this.closest('[data-search-path]');
|
|
217
|
+
if (root) {
|
|
218
|
+
syncRangeValidity(root, key);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return [
|
|
222
|
+
['label', [
|
|
223
|
+
'From: ',
|
|
224
|
+
['input', {
|
|
225
|
+
name: `${name}-gte`, class: `jsoeSearchRangeGte--${key}`, type, min, max, step,
|
|
226
|
+
$on: {input: validateRange, change: validateRange}
|
|
227
|
+
}]
|
|
228
|
+
]],
|
|
229
|
+
['label', [
|
|
230
|
+
'To: ',
|
|
231
|
+
['input', {
|
|
232
|
+
name: `${name}-lte`, class: `jsoeSearchRangeLte--${key}`, type, min, max, step,
|
|
233
|
+
$on: {input: validateRange, change: validateRange}
|
|
234
|
+
}]
|
|
235
|
+
]]
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Reads back a `buildRangeInputsPair` - pass the same `key` it was built
|
|
241
|
+
* with.
|
|
242
|
+
* @param {HTMLElement} el
|
|
243
|
+
* @param {string} [key]
|
|
244
|
+
* @returns {{gte: string, lte: string}}
|
|
245
|
+
*/
|
|
246
|
+
export function readRangeInputsPair (el, key = '') {
|
|
247
|
+
const gte = /** @type {HTMLInputElement|undefined} */ (
|
|
248
|
+
findOwnControl(el, `input.jsoeSearchRangeGte--${key}`)
|
|
249
|
+
)?.value ?? '';
|
|
250
|
+
const lte = /** @type {HTMLInputElement|undefined} */ (
|
|
251
|
+
findOwnControl(el, `input.jsoeSearchRangeLte--${key}`)
|
|
252
|
+
)?.value ?? '';
|
|
253
|
+
return {gte, lte};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A `<select multiple>` populated from a fixed candidate list - used by
|
|
258
|
+
* `enum`/`multiSelect`/`literalSet` leaf controls (README: "multiple
|
|
259
|
+
* select").
|
|
260
|
+
*
|
|
261
|
+
* `required`, when set, makes leaving *every* option unselected invalid -
|
|
262
|
+
* `<select multiple required>` is natively satisfied by one or more
|
|
263
|
+
* selections, no custom validity code needed. `enumSearchType.js` passes
|
|
264
|
+
* this (an enum widget has nothing else to constrain, so an empty selection
|
|
265
|
+
* is never a meaningful "no constraint" state - same "no absent values"
|
|
266
|
+
* reasoning as `buildLiteralRegexControls`'s Value input); other callers
|
|
267
|
+
* (`regexpSearchType.js`'s Flags, `SpecialRealNumberSearchType.js`) leave it
|
|
268
|
+
* at the default `false`, since an empty selection there legitimately means
|
|
269
|
+
* "no constraint on this facet" alongside a widget that has other facets.
|
|
270
|
+
* @param {{
|
|
271
|
+
* name: string,
|
|
272
|
+
* options: (string|[value: string, title: string])[],
|
|
273
|
+
* required?: boolean
|
|
274
|
+
* }} cfg
|
|
275
|
+
* @returns {JamilihArray}
|
|
276
|
+
*/
|
|
277
|
+
export function buildMultiSelect ({name, options, required = false}) {
|
|
278
|
+
return ['select', {
|
|
279
|
+
name, multiple: true, required, class: 'jsoeSearchMultiSelect'
|
|
280
|
+
}, options.map((opt) => {
|
|
281
|
+
const [value, title] = Array.isArray(opt) ? opt : [opt, opt];
|
|
282
|
+
return ['option', {value}, [title]];
|
|
283
|
+
})];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Reads back a `buildMultiSelect` into the selected values, `[]` if none.
|
|
288
|
+
* @param {Element} el
|
|
289
|
+
* @returns {string[]}
|
|
290
|
+
*/
|
|
291
|
+
export function readMultiSelect (el) {
|
|
292
|
+
const select = /** @type {HTMLSelectElement|undefined} */ (
|
|
293
|
+
findOwnControl(el, 'select.jsoeSearchMultiSelect')
|
|
294
|
+
);
|
|
295
|
+
return [...(select?.selectedOptions ?? [])].map((opt) => opt.value);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A tri-state "has property" control for one already-chosen property name
|
|
300
|
+
* (README: "Has property <property pull-down>", "avoid listing
|
|
301
|
+
* required"). `objectSearchType.js` supplies the additive pull-down that
|
|
302
|
+
* lets a user pick *which* property to add one of these for; this helper is
|
|
303
|
+
* only the fixed per-property toggle it adds each time, so the "any" state
|
|
304
|
+
* (no constraint entered) is distinguishable from an explicit "has"/
|
|
305
|
+
* "doesn't have" - matching `SearchTypeObject.getQuery`'s
|
|
306
|
+
* `undefined`-means-"no constraint" convention. Shares `readTriStateSelect`
|
|
307
|
+
* with `buildTriStateSelect` below - same three-value shape, just different
|
|
308
|
+
* option labels.
|
|
309
|
+
* @param {{name: string, propertyName: string}} cfg
|
|
310
|
+
* @returns {JamilihArray}
|
|
311
|
+
*/
|
|
312
|
+
export function buildHasPropertyToggle ({name, propertyName}) {
|
|
313
|
+
return ['label', [
|
|
314
|
+
`Has property "${propertyName}": `,
|
|
315
|
+
['select', {name, class: 'jsoeSearchTriState--'}, [
|
|
316
|
+
['option', {value: ''}, ['(any)']],
|
|
317
|
+
['option', {value: 'true'}, ['Has']],
|
|
318
|
+
['option', {value: 'false'}, ['Doesn’t have']]
|
|
319
|
+
]]
|
|
320
|
+
]];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A generic "(any)"/true/false `<select>`, shared by every leaf whose only
|
|
325
|
+
* constraint is a plain boolean choice - `booleanSearchType.js`'s "true or
|
|
326
|
+
* false" (README) and `numberSearchType.js`'s "Is/Is Not Integer" both read
|
|
327
|
+
* back through this same three-way convention rather than each rolling
|
|
328
|
+
* their own.
|
|
329
|
+
*
|
|
330
|
+
* `key` distinguishes multiple tri-states *within one widget* (e.g.
|
|
331
|
+
* `dommatrixSearchType.js`'s "Is/Is not Readonly" and "Is/Is not 3d" side
|
|
332
|
+
* by side); see `buildRangeInputsPair`'s doc for why. Leave it at the
|
|
333
|
+
* default `''` for the common case of a widget with only one tri-state.
|
|
334
|
+
* `onChange`, when given, wires the select's own `change` event too (in
|
|
335
|
+
* addition to whatever the caller reads back via `readTriStateSelect` at
|
|
336
|
+
* `getQuery` time) - `dateSearchType.js`'s "Is valid/invalid date" tri-state
|
|
337
|
+
* uses it to disable the (otherwise irrelevant) From/To range while
|
|
338
|
+
* "Invalid date" is selected.
|
|
339
|
+
*
|
|
340
|
+
* `required`, when set, makes leaving the select at "(any)" invalid - the
|
|
341
|
+
* "(any)" option's `value: ''` below is exactly what native `required`
|
|
342
|
+
* treats as "nothing selected", the same trick `unionFamilySearchType.js`'s
|
|
343
|
+
* "Has type" select and `buildMultiSelect`'s `required` use.
|
|
344
|
+
* `booleanSearchType.js` passes this (a boolean widget has nothing else to
|
|
345
|
+
* constrain, so "(any)" is never a meaningful "no constraint" state - same
|
|
346
|
+
* "no absent values" reasoning as `buildLiteralRegexControls`'s Value
|
|
347
|
+
* input); other callers (`numberSearchType.js`'s "Is/Is Not Integer",
|
|
348
|
+
* `dommatrixSearchType.js`'s readonly/3d) leave it at the default `false`,
|
|
349
|
+
* since those are one of *several* facets in their own widget, where
|
|
350
|
+
* "(any)" legitimately means "no constraint on this facet".
|
|
351
|
+
* @param {{
|
|
352
|
+
* name: string, key?: string, trueLabel: string, falseLabel: string,
|
|
353
|
+
* onChange?: (this: HTMLElement) => void, required?: boolean
|
|
354
|
+
* }} cfg
|
|
355
|
+
* @returns {JamilihArray}
|
|
356
|
+
*/
|
|
357
|
+
export function buildTriStateSelect ({
|
|
358
|
+
name, key = '', trueLabel, falseLabel, onChange, required = false
|
|
359
|
+
}) {
|
|
360
|
+
return ['select', {
|
|
361
|
+
name, class: `jsoeSearchTriState--${key}`, required,
|
|
362
|
+
$on: onChange ? {change: onChange} : undefined
|
|
363
|
+
}, [
|
|
364
|
+
['option', {value: ''}, ['(any)']],
|
|
365
|
+
['option', {value: 'true'}, [trueLabel]],
|
|
366
|
+
['option', {value: 'false'}, [falseLabel]]
|
|
367
|
+
]];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Reads back a `buildTriStateSelect`/`buildHasPropertyToggle` - pass the
|
|
372
|
+
* same `key` it was built with. `''` (any) maps to `undefined`.
|
|
373
|
+
* @param {Element} el
|
|
374
|
+
* @param {string} [key]
|
|
375
|
+
* @returns {boolean|undefined}
|
|
376
|
+
*/
|
|
377
|
+
export function readTriStateSelect (el, key = '') {
|
|
378
|
+
const select = /** @type {HTMLSelectElement|undefined} */ (
|
|
379
|
+
findOwnControl(el, `select.jsoeSearchTriState--${key}`)
|
|
380
|
+
);
|
|
381
|
+
if (!select || select.value === '') {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
return select.value === 'true';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A single checkbox - the only search affordance the README grants
|
|
389
|
+
* `undefined`/`void`/`null`/`NaN` ("Require present"; they have "no
|
|
390
|
+
* variants to allow for distinct search", since existence only becomes a
|
|
391
|
+
* meaningful question once the path is optional or nested in a union), and
|
|
392
|
+
* also used by `recordSearchType.js`/`mapSearchType.js` for their "require
|
|
393
|
+
* same entry" toggle.
|
|
394
|
+
*
|
|
395
|
+
* `checked`, when set, pre-checks the checkbox; `disabled` locks it there,
|
|
396
|
+
* non-interactive. `makePresenceOnlySearchType` passes both: the checkbox
|
|
397
|
+
* is that leaf's *only* possible constraint, and its whole schema type has
|
|
398
|
+
* exactly one value ("no variants to allow for distinct search" above) -
|
|
399
|
+
* there is no second state worth offering a choice between, so this simply
|
|
400
|
+
* asserts the one meaningful thing outright rather than making the user
|
|
401
|
+
* pointlessly check a box that could only ever mean one thing. "require
|
|
402
|
+
* same entry" leaves both at their `false` defaults (interactive,
|
|
403
|
+
* unchecked), since that's one optional facet alongside a map/record's own
|
|
404
|
+
* key/value matches, not the whole widget's sole, single-valued constraint.
|
|
405
|
+
* @param {{name: string, label: string, checked?: boolean, disabled?: boolean}} cfg
|
|
406
|
+
* @returns {JamilihArray}
|
|
407
|
+
*/
|
|
408
|
+
export function buildCheckbox ({name, label, checked = false, disabled = false}) {
|
|
409
|
+
return ['label', [
|
|
410
|
+
`${label}: `,
|
|
411
|
+
['input', {type: 'checkbox', name, class: 'jsoeSearchCheckbox', checked, disabled}]
|
|
412
|
+
]];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Reads back a `buildCheckbox`.
|
|
417
|
+
* @param {HTMLElement} el
|
|
418
|
+
* @returns {boolean}
|
|
419
|
+
*/
|
|
420
|
+
export function readCheckbox (el) {
|
|
421
|
+
return Boolean(/** @type {HTMLInputElement|undefined} */ (
|
|
422
|
+
findOwnControl(el, 'input.jsoeSearchCheckbox')
|
|
423
|
+
)?.checked);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The mode selector for the README's "string, StringObject, Blob, File,
|
|
428
|
+
* regexp (source), symbol (description): OR literal or regex search/Does
|
|
429
|
+
* Not contain search" - one shared control (and reader, below) that
|
|
430
|
+
* `stringSearchType.js`, `symbolSearchType.js`, and `regexpSearchType.js`
|
|
431
|
+
* (for its source) each build their own custom element around, since the
|
|
432
|
+
* query semantics are identical and only the label/target facet differs.
|
|
433
|
+
*
|
|
434
|
+
* `key` distinguishes multiple literal/regex controls *within one widget*
|
|
435
|
+
* (e.g. `errorSearchType.js`'s `message`/`name`/`fileName`/`stack`); see
|
|
436
|
+
* `buildRangeInputsPair`'s doc for why. Leave it at the default `''` for
|
|
437
|
+
* the common case of a widget with only one such control.
|
|
438
|
+
*
|
|
439
|
+
* The Value input is `required`: unlike an untouched range/checkbox/select
|
|
440
|
+
* (whose empty/default state unambiguously means "no constraint"), a mode
|
|
441
|
+
* is always selected here (there is no "(any)" option), so an empty Value
|
|
442
|
+
* next to it is never a meaningful "no constraint" state - it just means
|
|
443
|
+
* the row was added and never finished. That leaves the whole form invalid
|
|
444
|
+
* from the moment such a row exists (`buildSearchChoices`'s `<form>`,
|
|
445
|
+
* `src/search/index.js`) until either a value is entered or (for an
|
|
446
|
+
* `objectSearchType.js` has-property row) the row is removed via its own
|
|
447
|
+
* "Remove" button.
|
|
448
|
+
*
|
|
449
|
+
* `onModeChange`, when given, wires the mode select's own `change` event
|
|
450
|
+
* too (in addition to whatever the caller reads back via
|
|
451
|
+
* `readLiteralRegexQuery` at `getQuery` time) - `regexpSearchType.js` uses
|
|
452
|
+
* it to show/hide its Flags multi-select, which only makes sense while
|
|
453
|
+
* "Matches regex" is the chosen mode.
|
|
454
|
+
*
|
|
455
|
+
* `flagOptions`, when given, adds a Flags multi-select (options passed in
|
|
456
|
+
* by the caller - `stringSearchType.js` passes `regexpType.js`'s own
|
|
457
|
+
* `allowedFlags`, the same list `regexpSearchType.js` uses for the actual
|
|
458
|
+
* regexp's own flags - kept out of this generic module to avoid it
|
|
459
|
+
* depending on a specific fundamental type), shown only while "Matches
|
|
460
|
+
* regex" is the chosen mode, the same `$options` a Mongo-flavored `$regex`
|
|
461
|
+
* accepts alongside it - a plain literal/substring match has no regex to
|
|
462
|
+
* apply flags to, so it stays hidden otherwise. Every other caller
|
|
463
|
+
* (`errorSearchType.js`'s props, `fileSearchType.js`'s name/type,
|
|
464
|
+
* `domexceptionSearchType.js`'s message) leaves this unset, matching the
|
|
465
|
+
* README's flags-for-regexp-only-plus-string bullet.
|
|
466
|
+
* @param {{
|
|
467
|
+
* name: string, key?: string, onModeChange?: (this: HTMLElement) => void,
|
|
468
|
+
* flagOptions?: string[]
|
|
469
|
+
* }} cfg
|
|
470
|
+
* @returns {JamilihArray}
|
|
471
|
+
*/
|
|
472
|
+
export function buildLiteralRegexControls ({name, key = '', onModeChange, flagOptions}) {
|
|
473
|
+
/**
|
|
474
|
+
* @this {HTMLElement}
|
|
475
|
+
* @returns {void}
|
|
476
|
+
*/
|
|
477
|
+
function handleModeChange () {
|
|
478
|
+
const flagsLabel = this.closest('[data-search-path]')?.querySelector(
|
|
479
|
+
`.jsoeSearchRegexFlagsLabel--${key}`
|
|
480
|
+
);
|
|
481
|
+
if (flagsLabel) {
|
|
482
|
+
/** @type {HTMLElement} */ (flagsLabel).hidden =
|
|
483
|
+
/** @type {HTMLSelectElement} */ (this).value !== 'regex';
|
|
484
|
+
}
|
|
485
|
+
onModeChange?.call(this);
|
|
486
|
+
}
|
|
487
|
+
/** @type {JamilihArray[]} */
|
|
488
|
+
const flagsChildren = [];
|
|
489
|
+
if (flagOptions) {
|
|
490
|
+
flagsChildren.push(['label', {class: `jsoeSearchRegexFlagsLabel--${key}`, hidden: true}, [
|
|
491
|
+
'Flags: ',
|
|
492
|
+
['select', {
|
|
493
|
+
name: `${name}-flags`, multiple: true, class: `jsoeSearchRegexFlags--${key}`
|
|
494
|
+
}, flagOptions.map((flag) => ['option', {value: flag}, [flag]])]
|
|
495
|
+
]]);
|
|
496
|
+
}
|
|
497
|
+
return ['span', [
|
|
498
|
+
['label', [
|
|
499
|
+
'Mode: ',
|
|
500
|
+
['select', {
|
|
501
|
+
name: `${name}-mode`, class: `jsoeSearchMode--${key}`,
|
|
502
|
+
$on: {change: handleModeChange}
|
|
503
|
+
}, [
|
|
504
|
+
['option', {value: 'literal'}, ['One of (comma-separated)']],
|
|
505
|
+
['option', {value: 'regex'}, ['Matches regex']],
|
|
506
|
+
['option', {value: 'notContains'}, ['Does not contain']]
|
|
507
|
+
]]
|
|
508
|
+
]],
|
|
509
|
+
['label', [
|
|
510
|
+
'Value: ',
|
|
511
|
+
['input', {
|
|
512
|
+
type: 'text', name: `${name}-value`, class: `jsoeSearchValue--${key}`,
|
|
513
|
+
required: true
|
|
514
|
+
}]
|
|
515
|
+
]],
|
|
516
|
+
...flagsChildren
|
|
517
|
+
]];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Reads back `buildLiteralRegexControls` into the corresponding
|
|
522
|
+
* `literalSet`/`regex`/`notContains` leaf - pass the same `key` it was
|
|
523
|
+
* built with. A `regex` leaf's `$options` is only ever populated when the
|
|
524
|
+
* caller built this with `flagOptions` (see that function's doc) and at
|
|
525
|
+
* least one flag is currently selected.
|
|
526
|
+
* @param {HTMLElement} el
|
|
527
|
+
* @param {string} path
|
|
528
|
+
* @param {string} [key]
|
|
529
|
+
* @returns {import('./queryTree.js').QueryLiteralSetLeaf|
|
|
530
|
+
* import('./queryTree.js').QueryRegexLeaf|
|
|
531
|
+
* import('./queryTree.js').QueryNotContainsLeaf|undefined}
|
|
532
|
+
*/
|
|
533
|
+
export function readLiteralRegexQuery (el, path, key = '') {
|
|
534
|
+
const mode = /** @type {HTMLSelectElement|undefined} */ (
|
|
535
|
+
findOwnControl(el, `select.jsoeSearchMode--${key}`)
|
|
536
|
+
)?.value;
|
|
537
|
+
const value = /** @type {HTMLInputElement|undefined} */ (
|
|
538
|
+
findOwnControl(el, `input.jsoeSearchValue--${key}`)
|
|
539
|
+
)?.value;
|
|
540
|
+
if (!value) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
if (mode === 'regex') {
|
|
544
|
+
const flagsSelect = /** @type {HTMLSelectElement|undefined} */ (
|
|
545
|
+
findOwnControl(el, `select.jsoeSearchRegexFlags--${key}`)
|
|
546
|
+
);
|
|
547
|
+
const flags = [...(flagsSelect?.selectedOptions ?? [])].map((opt) => opt.value);
|
|
548
|
+
return {kind: 'regex', path, $regex: value, ...(flags.length ? {$options: flags.join('')} : {})};
|
|
549
|
+
}
|
|
550
|
+
if (mode === 'notContains') {
|
|
551
|
+
return {kind: 'notContains', path, value};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
kind: 'literalSet',
|
|
555
|
+
path,
|
|
556
|
+
$in: value.split(',').map((v) => v.trim()).filter(Boolean)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Has length/size of <number> (README; array/set/tuple-with-rest/
|
|
562
|
+
* filelist), + "Is/Is not sparse" for arrays only. Returns `[]` (no
|
|
563
|
+
* control) when the schema pins an exact length/size, since searching on a
|
|
564
|
+
* constant is uninteresting - the schema's `min`/`max` otherwise become the
|
|
565
|
+
* input's HTML `min`/`max` attributes.
|
|
566
|
+
* @param {{
|
|
567
|
+
* name: string,
|
|
568
|
+
* min?: number,
|
|
569
|
+
* max?: number,
|
|
570
|
+
* includeSparse?: boolean
|
|
571
|
+
* }} cfg
|
|
572
|
+
* @returns {JamilihArray[]}
|
|
573
|
+
*/
|
|
574
|
+
export function buildLengthSizeControls ({name, min, max, includeSparse}) {
|
|
575
|
+
const fixed = min !== undefined && min === max;
|
|
576
|
+
/** @type {JamilihArray[]} */
|
|
577
|
+
const controls = [];
|
|
578
|
+
if (!fixed) {
|
|
579
|
+
controls.push(['label', [
|
|
580
|
+
'Has length/size of: ',
|
|
581
|
+
['input', {
|
|
582
|
+
type: 'number', name: `${name}-size`, class: 'jsoeSearchSize',
|
|
583
|
+
// A length/size is never negative, regardless of whether the
|
|
584
|
+
// schema itself declares a (necessarily non-negative) `min` -
|
|
585
|
+
// floor at 0 rather than leaving the input unbounded below when it
|
|
586
|
+
// doesn't.
|
|
587
|
+
min: min === undefined ? 0 : Math.max(min, 0), max, step: 1
|
|
588
|
+
}]
|
|
589
|
+
]]);
|
|
590
|
+
}
|
|
591
|
+
if (includeSparse) {
|
|
592
|
+
controls.push(['label', [
|
|
593
|
+
'Sparse: ',
|
|
594
|
+
buildTriStateSelect({
|
|
595
|
+
name: `${name}-sparse`, trueLabel: 'Sparse', falseLabel: 'Not sparse'
|
|
596
|
+
})
|
|
597
|
+
]]);
|
|
598
|
+
}
|
|
599
|
+
return controls;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Reads back `buildLengthSizeControls` into one `lengthSize` leaf, or
|
|
604
|
+
* `undefined` if neither the size nor the sparse control was set.
|
|
605
|
+
* @param {Element} el
|
|
606
|
+
* @param {string} path
|
|
607
|
+
* @returns {import('./queryTree.js').QueryLengthSizeLeaf|undefined}
|
|
608
|
+
*/
|
|
609
|
+
export function readLengthSizeQuery (el, path) {
|
|
610
|
+
const sizeStr = /** @type {HTMLInputElement|undefined} */ (
|
|
611
|
+
findOwnControl(el, 'input.jsoeSearchSize')
|
|
612
|
+
)?.value;
|
|
613
|
+
const sparseCheck = readTriStateSelect(el);
|
|
614
|
+
if (!sizeStr && sparseCheck === undefined) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
kind: 'lengthSize',
|
|
619
|
+
path,
|
|
620
|
+
...(sizeStr ? {$size: Number(sizeStr)} : {}),
|
|
621
|
+
...(sparseCheck === undefined ? {} : {sparseCheck})
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Wraps arbitrary markup - a recursed child search widget's array, or a
|
|
627
|
+
* fixed facet's own controls (`buildLiteralRegexControls`'s output, say) -
|
|
628
|
+
* in an opt-in "Search on this" checkbox around a disabled `<fieldset>`.
|
|
629
|
+
* For a facet that's one of *several* independent, individually-optional
|
|
630
|
+
* constraints within a parent widget (array/set/tuple/filelist's element
|
|
631
|
+
* match(es); map/record's key/value; `fileSearchType.js`'s name/content-
|
|
632
|
+
* type), this keeps that facet's own `required` controls (if it has any)
|
|
633
|
+
* from forcing the *whole* form invalid just by the facet existing -
|
|
634
|
+
* generalizes `objectSearchType.js`'s original required-property row (see
|
|
635
|
+
* that file's history) into a shared helper once enough call sites needed
|
|
636
|
+
* the identical checkbox+fieldset shape.
|
|
637
|
+
*
|
|
638
|
+
* `key` distinguishes multiple opt-in fieldsets *within one widget* (e.g.
|
|
639
|
+
* `mapSearchType.js`'s "key" and "value"); see `buildRangeInputsPair`'s doc
|
|
640
|
+
* for why. Leave it at the default `''` for a widget with only one.
|
|
641
|
+
*
|
|
642
|
+
* The checkbox's own `name` attribute is `${name}-optIn`, not the bare
|
|
643
|
+
* `name` passed in: a caller's `name` is typically shared with (a prefix
|
|
644
|
+
* of) the wrapped content's own field names (`mapSearchType.js`'s "value"
|
|
645
|
+
* facet, say, names its checkbox from the same `name` its inner value
|
|
646
|
+
* widget builds its own `-value`-suffixed `<input>` from), and callers
|
|
647
|
+
* cannot always predict what suffix a given recursed `SearchTypeObject`
|
|
648
|
+
* will pick - the fixed `-optIn` suffix here guarantees no collision
|
|
649
|
+
* regardless, without every caller needing to reason about it.
|
|
650
|
+
*
|
|
651
|
+
* Call `wireOptInFieldset` once, right after this is built into real DOM
|
|
652
|
+
* (e.g. inside a `jml(...)` caller, same as `objectSearchType.js`'s rows
|
|
653
|
+
* do), to actually connect the checkbox to the fieldset's `disabled` state -
|
|
654
|
+
* this function only builds the static markup (default unchecked/disabled).
|
|
655
|
+
* @param {{
|
|
656
|
+
* name: string, key?: string, label: string, children: JamilihArray[]
|
|
657
|
+
* }} cfg
|
|
658
|
+
* @returns {JamilihArray[]}
|
|
659
|
+
*/
|
|
660
|
+
export function buildOptInFieldset ({name, key = '', label, children}) {
|
|
661
|
+
return [
|
|
662
|
+
['label', {class: 'jsoeSearchOptInLabel'}, [
|
|
663
|
+
`${label}: `,
|
|
664
|
+
['input', {type: 'checkbox', name: `${name}-optIn`, class: `jsoeSearchOptIn--${key}`}]
|
|
665
|
+
]],
|
|
666
|
+
['fieldset', {disabled: true, class: `jsoeSearchOptInFieldset--${key}`}, children]
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Reads back a `buildOptInFieldset`'s checkbox - pass the same `key` it was
|
|
672
|
+
* built with.
|
|
673
|
+
* @param {Element} root
|
|
674
|
+
* @param {string} [key]
|
|
675
|
+
* @returns {boolean}
|
|
676
|
+
*/
|
|
677
|
+
export function readOptInChecked (root, key = '') {
|
|
678
|
+
return Boolean(/** @type {HTMLInputElement|undefined} */ (
|
|
679
|
+
findOwnControl(root, `input.jsoeSearchOptIn--${key}`)
|
|
680
|
+
)?.checked);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Connects a `buildOptInFieldset`'s checkbox to toggle its own fieldset's
|
|
685
|
+
* `disabled` state - call once, synchronously, right after both are live
|
|
686
|
+
* DOM nodes (see that function's doc). Pass the same `key` it was built
|
|
687
|
+
* with.
|
|
688
|
+
*
|
|
689
|
+
* `onToggle`, when given, is called after each toggle (with the checkbox as
|
|
690
|
+
* `this`) - `mapSearchType.js`/`recordSearchType.js`/`fileSearchType.js`
|
|
691
|
+
* use it to re-run their own `syncAtLeastOneCheck` whenever one of their
|
|
692
|
+
* (exactly two) opt-in facets changes, since checking/unchecking either one
|
|
693
|
+
* can change whether "at least one" is satisfied.
|
|
694
|
+
* @param {Element} root
|
|
695
|
+
* @param {string} [key]
|
|
696
|
+
* @param {((this: HTMLInputElement) => void)} [onToggle]
|
|
697
|
+
* @returns {void}
|
|
698
|
+
*/
|
|
699
|
+
export function wireOptInFieldset (root, key = '', onToggle = undefined) {
|
|
700
|
+
const checkbox = /** @type {HTMLInputElement|undefined} */ (
|
|
701
|
+
findOwnControl(root, `input.jsoeSearchOptIn--${key}`)
|
|
702
|
+
);
|
|
703
|
+
const fieldset = /** @type {HTMLFieldSetElement|undefined} */ (
|
|
704
|
+
findOwnControl(root, `fieldset.jsoeSearchOptInFieldset--${key}`)
|
|
705
|
+
);
|
|
706
|
+
checkbox?.addEventListener('change', () => {
|
|
707
|
+
if (fieldset) {
|
|
708
|
+
fieldset.disabled = !checkbox.checked;
|
|
709
|
+
}
|
|
710
|
+
onToggle?.call(checkbox);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* A visually-hidden but still-rendered (and so still constraint-validation-
|
|
716
|
+
* eligible - see `jsoe.css`'s `.searchAtLeastOneSentinel`, the same
|
|
717
|
+
* "visually-hidden" pattern used for accessibility, which keeps an element
|
|
718
|
+
* off-screen without `display: none`/`hidden` triggering the Constraint
|
|
719
|
+
* Validation API's own "not rendered" exemption) sentinel control, for a
|
|
720
|
+
* widget with several independent optional facets where leaving *every one*
|
|
721
|
+
* unconfigured should be invalid even though no single facet is itself
|
|
722
|
+
* always required (`mapSearchType.js`/`recordSearchType.js`'s key/value,
|
|
723
|
+
* `fileSearchType.js`'s name/content-type). Pair with `syncAtLeastOneCheck`.
|
|
724
|
+
* @returns {JamilihArray}
|
|
725
|
+
*/
|
|
726
|
+
export function buildAtLeastOneSentinel () {
|
|
727
|
+
return ['input', {
|
|
728
|
+
type: 'text', class: 'searchAtLeastOneSentinel', tabindex: -1,
|
|
729
|
+
'aria-hidden': 'true'
|
|
730
|
+
}];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @type {WeakMap<Element, () => void>}
|
|
735
|
+
*/
|
|
736
|
+
const atLeastOneResyncs = new WeakMap();
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Sets a `buildAtLeastOneSentinel`'s custom validity from a live check -
|
|
740
|
+
* call once at `connectedCallback` time for the initial state (a freshly-
|
|
741
|
+
* built widget has satisfied none of its facets yet) and again whenever a
|
|
742
|
+
* facet that could change the answer does (an opt-in checkbox toggling, a
|
|
743
|
+
* recursed child's own `input`/`change`).
|
|
744
|
+
*
|
|
745
|
+
* Also relaxed, independently of `isSatisfied`, while
|
|
746
|
+
* `isExemptedByAncestorHasProperty` says an enclosing
|
|
747
|
+
* `objectSearchType.js` has-property row's own "Has"/"Doesn't have"
|
|
748
|
+
* already provides a complete constraint - the same relief
|
|
749
|
+
* `syncRangeValidity` and `dateSearchType.js`'s own range validator give
|
|
750
|
+
* their "at least one bound" rule, needed here too since a has-property
|
|
751
|
+
* row's own child could just as easily be an `array`/`map`/`file`/etc.
|
|
752
|
+
* whose *own* sole constraint is this same "at least one facet" sentinel
|
|
753
|
+
* rather than a plain `required` attribute or a range pair.
|
|
754
|
+
*
|
|
755
|
+
* Every call re-registers `isSatisfied` (keyed by `root` in a `WeakMap`, so
|
|
756
|
+
* it never leaks past the element's own lifetime) for `resyncAtLeastOne` to
|
|
757
|
+
* call later. This exists because not every "at least one" widget's own
|
|
758
|
+
* answer changes via a plain `input`/`change` event - `objectSearchType.js`
|
|
759
|
+
* itself is the reason: its sentinel changes when a row is added/removed
|
|
760
|
+
* (a button click, and a structural DOM change, not a value change on any
|
|
761
|
+
* control), which `revalidateDescendants`' blanket event-redispatch can
|
|
762
|
+
* never reach, unlike `array`/`map`/etc.'s own opt-in checkboxes and size
|
|
763
|
+
* inputs. `objectSearchType.js`'s `buildHasPropertyRow` calls
|
|
764
|
+
* `resyncAtLeastOne` directly instead, precisely because it can't rely on
|
|
765
|
+
* redispatched events reaching every possible child type.
|
|
766
|
+
* @param {Element} root
|
|
767
|
+
* @param {() => boolean} isSatisfied
|
|
768
|
+
* @returns {void}
|
|
769
|
+
*/
|
|
770
|
+
export function syncAtLeastOneCheck (root, isSatisfied) {
|
|
771
|
+
atLeastOneResyncs.set(root, () => syncAtLeastOneCheck(root, isSatisfied));
|
|
772
|
+
const sentinel = /** @type {HTMLInputElement|undefined} */ (
|
|
773
|
+
findOwnControl(root, 'input.searchAtLeastOneSentinel')
|
|
774
|
+
);
|
|
775
|
+
sentinel?.setCustomValidity(
|
|
776
|
+
isSatisfied() || isExemptedByAncestorHasProperty(root)
|
|
777
|
+
? ''
|
|
778
|
+
: 'Configure at least one of this widget’s facets.'
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Re-runs every `syncAtLeastOneCheck` registered anywhere within `root`
|
|
784
|
+
* (itself included) - see that function's doc for why this exists
|
|
785
|
+
* alongside `revalidateDescendants` rather than relying on it alone.
|
|
786
|
+
* @param {Element} root
|
|
787
|
+
* @returns {void}
|
|
788
|
+
*/
|
|
789
|
+
export function resyncAtLeastOne (root) {
|
|
790
|
+
atLeastOneResyncs.get(root)?.();
|
|
791
|
+
[...root.querySelectorAll('[data-search-path]')].forEach((el) => {
|
|
792
|
+
atLeastOneResyncs.get(el)?.();
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Toggles the native `required` attribute on every originally-`required`
|
|
798
|
+
* control inside `root`, leaving everything else (visibility, `disabled`)
|
|
799
|
+
* alone - unlike `buildOptInFieldset`'s `<fieldset disabled>`, this keeps
|
|
800
|
+
* the whole subtree fully interactive, for a case where an ancestor's own
|
|
801
|
+
* choice already provides a complete constraint on its own, but should
|
|
802
|
+
* still let the recursed child *optionally* add a further constraint on
|
|
803
|
+
* top rather than blocking interaction with it entirely.
|
|
804
|
+
* `objectSearchType.js`'s `buildHasPropertyRow` uses this for its "Has"/
|
|
805
|
+
* "Doesn't have" tri-state: once existence is explicitly asserted either
|
|
806
|
+
* way, the child's own required inputs (whatever type it turns out to be)
|
|
807
|
+
* should stop forcing it to be filled in, while a user who still wants to
|
|
808
|
+
* combine "Has property X" with "X matches Y" can keep typing into it -
|
|
809
|
+
* `dateSearchType.js`'s "Valid"/"Invalid date" tri-state needs the
|
|
810
|
+
* equivalent relaxation for its own (structurally different, so not built
|
|
811
|
+
* through this helper) range pair.
|
|
812
|
+
*
|
|
813
|
+
* Relaxing (`required: false`) marks each control it touches with
|
|
814
|
+
* `data-jsoe-was-required` before clearing the IDL `required` property -
|
|
815
|
+
* setting that property to `false` reflects back to *removing* the
|
|
816
|
+
* `required` content attribute entirely (standard boolean-attribute
|
|
817
|
+
* reflection), so a later call to restore it can't rely on `[required]`
|
|
818
|
+
* still matching that element; it looks for the marker instead.
|
|
819
|
+
*
|
|
820
|
+
* Skips any control inside a `buildOptInFieldset` fieldset
|
|
821
|
+
* (`jsoeSearchOptInFieldset--*`): that facet already has its own gate on
|
|
822
|
+
* whether it applies at all (the checkbox's `disabled` toggle exempts it
|
|
823
|
+
* from constraint validation while unchecked), so an enclosing "Has
|
|
824
|
+
* property" shouldn't also strip its `required` - a user who explicitly
|
|
825
|
+
* opts into e.g. `fileSearchType.js`'s "Name" facet under a "Has property"
|
|
826
|
+
* ancestor still needs to fill in the Value it requires, or uncheck "Name"
|
|
827
|
+
* again; only the widget's own `buildAtLeastOneSentinel` (which *is*
|
|
828
|
+
* exempted, via `isExemptedByAncestorHasProperty`) should be satisfied by
|
|
829
|
+
* "Has property" alone.
|
|
830
|
+
* @param {Element} root
|
|
831
|
+
* @param {boolean} required
|
|
832
|
+
* @returns {void}
|
|
833
|
+
*/
|
|
834
|
+
export function setDescendantsRequired (root, required) {
|
|
835
|
+
const selector = required ? '[data-jsoe-was-required]' : '[required]';
|
|
836
|
+
[...root.querySelectorAll(selector)].forEach((el) => {
|
|
837
|
+
if (el.closest('fieldset[class^="jsoeSearchOptInFieldset--"]')) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!required) {
|
|
841
|
+
/** @type {HTMLElement} */ (el).dataset.jsoeWasRequired = 'true';
|
|
842
|
+
}
|
|
843
|
+
/** @type {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement} */ (
|
|
844
|
+
el
|
|
845
|
+
).required = required;
|
|
846
|
+
});
|
|
847
|
+
}
|