@es-joy/jsoe 0.29.0 → 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 +6 -0
- package/README.md +2 -0
- package/dist/fundamentalTypes/dateType.d.ts +8 -3
- package/dist/fundamentalTypes/dateType.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/search/fundamentalTypes/arraySearchType.d.ts +11 -4
- package/dist/search/fundamentalTypes/arraySearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/bigintSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/blobSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/dateSearchType.d.ts +8 -3
- package/dist/search/fundamentalTypes/dateSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts +5 -5
- package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/enumSearchType.d.ts +4 -0
- package/dist/search/fundamentalTypes/enumSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/fileSearchType.d.ts +19 -10
- package/dist/search/fundamentalTypes/fileSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/filelistSearchType.d.ts +4 -12
- package/dist/search/fundamentalTypes/filelistSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/functionSearchType.d.ts +4 -4
- package/dist/search/fundamentalTypes/functionSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/mapSearchType.d.ts +8 -4
- package/dist/search/fundamentalTypes/mapSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/numberSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/objectSearchType.d.ts +10 -4
- package/dist/search/fundamentalTypes/objectSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/regexpSearchType.d.ts +7 -1
- package/dist/search/fundamentalTypes/regexpSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/setSearchType.d.ts +5 -4
- package/dist/search/fundamentalTypes/setSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/stringSearchType.d.ts +9 -1
- package/dist/search/fundamentalTypes/stringSearchType.d.ts.map +1 -1
- package/dist/search/fundamentalTypes/symbolSearchType.d.ts.map +1 -1
- package/dist/search/index.d.ts +18 -8
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/queryTree.d.ts +12 -1
- package/dist/search/queryTree.d.ts.map +1 -1
- package/dist/search/queryTreeBuilders.d.ts +6 -0
- package/dist/search/queryTreeBuilders.d.ts.map +1 -1
- package/dist/search/searchElementUtils.d.ts +17 -8
- package/dist/search/searchElementUtils.d.ts.map +1 -1
- package/dist/search/searchUtils.d.ts +343 -18
- package/dist/search/searchUtils.d.ts.map +1 -1
- package/dist/search/subTypes/blobHTMLSearchType.d.ts +22 -2
- package/dist/search/subTypes/blobHTMLSearchType.d.ts.map +1 -1
- package/dist/search/subTypes/recordSearchType.d.ts +4 -4
- package/dist/search/subTypes/recordSearchType.d.ts.map +1 -1
- package/dist/search/subTypes/tupleSearchType.d.ts +3 -3
- package/dist/search/subTypes/tupleSearchType.d.ts.map +1 -1
- package/dist/search/superTypes/buffersourceSearchType.d.ts.map +1 -1
- package/dist/search/unions/unionFamilySearchType.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/fundamentalTypes/dateType.js +9 -4
- package/src/fundamentalTypes/regexpType.js +1 -1
- package/src/jsoe.css +204 -0
- package/src/search/fundamentalTypes/arraySearchType.js +42 -7
- package/src/search/fundamentalTypes/bigintSearchType.js +7 -1
- package/src/search/fundamentalTypes/blobSearchType.js +2 -1
- package/src/search/fundamentalTypes/booleanSearchType.js +1 -1
- package/src/search/fundamentalTypes/dateSearchType.js +113 -20
- package/src/search/fundamentalTypes/domexceptionSearchType.js +39 -10
- package/src/search/fundamentalTypes/enumSearchType.js +5 -1
- package/src/search/fundamentalTypes/fileSearchType.js +64 -11
- package/src/search/fundamentalTypes/filelistSearchType.js +32 -7
- package/src/search/fundamentalTypes/functionSearchType.js +36 -12
- package/src/search/fundamentalTypes/mapSearchType.js +43 -13
- package/src/search/fundamentalTypes/numberSearchType.js +8 -1
- package/src/search/fundamentalTypes/objectSearchType.js +236 -17
- package/src/search/fundamentalTypes/regexpSearchType.js +29 -5
- package/src/search/fundamentalTypes/setSearchType.js +33 -7
- package/src/search/fundamentalTypes/stringSearchType.js +11 -2
- package/src/search/fundamentalTypes/symbolSearchType.js +2 -1
- package/src/search/index.js +26 -9
- package/src/search/queryTree.js +10 -1
- package/src/search/queryTreeBuilders.js +9 -0
- package/src/search/searchElementUtils.js +108 -24
- package/src/search/searchUtils.js +516 -24
- package/src/search/subTypes/blobHTMLSearchType.js +64 -7
- package/src/search/subTypes/recordSearchType.js +34 -13
- package/src/search/subTypes/tupleSearchType.js +51 -10
- package/src/search/superTypes/SpecialRealNumberSearchType.js +1 -1
- package/src/search/superTypes/buffersourceSearchType.js +7 -1
- package/src/search/unions/unionFamilySearchType.js +8 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {jml} from '../../vendor-imports.js';
|
|
2
2
|
import {escapeJSONPointer} from '../../utils/jsonPointer.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildPathLabel, buildHasPropertyToggle, readTriStateSelect, findOwnControl,
|
|
5
|
+
buildCheckbox, readCheckbox, buildAtLeastOneSentinel, syncAtLeastOneCheck,
|
|
6
|
+
setDescendantsRequired, revalidateDescendants, resyncAtLeastOne
|
|
7
|
+
} from '../searchUtils.js';
|
|
4
8
|
import {combineAnd, makeHasPropertyLeaf} from '../queryTreeBuilders.js';
|
|
5
9
|
import {
|
|
6
10
|
findSearchElement, getQueryViaElement, hasGetQuery
|
|
@@ -11,6 +15,27 @@ import {buildSearchWidget} from '../searchDispatch.js';
|
|
|
11
15
|
* @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
/**
|
|
19
|
+
* An object with zero active rows (nothing added from the pull-down, and no
|
|
20
|
+
* required property opted into) expresses no constraint at all - the same
|
|
21
|
+
* "no absent values" reasoning as every other leaf's own required control,
|
|
22
|
+
* just with nothing native to hang a `required` attribute directly off of,
|
|
23
|
+
* since what counts as "active" is a dynamically-changing count of child
|
|
24
|
+
* rows rather than one fixed input. `searchUtils.js`'s
|
|
25
|
+
* `buildAtLeastOneSentinel`/`syncAtLeastOneCheck` stand in for that.
|
|
26
|
+
* @param {Element} root - a `jsoe-search-object` element
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
function syncObjectValidity (root) {
|
|
30
|
+
const addedRowCount = root.querySelectorAll(':scope > jsoe-search-has-property').length;
|
|
31
|
+
const checkedRequiredCount = [
|
|
32
|
+
...root.querySelectorAll(
|
|
33
|
+
':scope > jsoe-search-required-property > label > input.jsoeSearchCheckbox'
|
|
34
|
+
)
|
|
35
|
+
].filter((checkbox) => /** @type {HTMLInputElement} */ (checkbox).checked).length;
|
|
36
|
+
syncAtLeastOneCheck(root, () => addedRowCount + checkedRequiredCount > 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/**
|
|
15
40
|
* One added property's row: the fixed "has property" tri-state toggle
|
|
16
41
|
* (`buildHasPropertyToggle`) plus, immediately recursing (search plan §6
|
|
@@ -25,12 +50,14 @@ import {buildSearchWidget} from '../searchDispatch.js';
|
|
|
25
50
|
* typeNamespace: string|undefined,
|
|
26
51
|
* topRoot: import('../../types.js').RootElement|undefined,
|
|
27
52
|
* types: import('../../types.js').default|undefined,
|
|
28
|
-
* originalJSON: import('../../formats/schema.js').ZodexSchema|undefined
|
|
53
|
+
* originalJSON: import('../../formats/schema.js').ZodexSchema|undefined,
|
|
54
|
+
* addPropertySelect: HTMLSelectElement
|
|
29
55
|
* }} cfg
|
|
30
56
|
* @returns {HTMLElement}
|
|
31
57
|
*/
|
|
32
58
|
function buildHasPropertyRow ({
|
|
33
|
-
propertyName, propSchema, path, typeNamespace, topRoot, types, originalJSON
|
|
59
|
+
propertyName, propSchema, path, typeNamespace, topRoot, types, originalJSON,
|
|
60
|
+
addPropertySelect
|
|
34
61
|
}) {
|
|
35
62
|
const childPath = `${path}/${escapeJSONPointer(propertyName)}`;
|
|
36
63
|
const name = `${typeNamespace}-hasProperty-${propertyName}`;
|
|
@@ -38,7 +65,7 @@ function buildHasPropertyRow ({
|
|
|
38
65
|
schemaObject: propSchema, path: childPath, typeNamespace, topRoot, types,
|
|
39
66
|
originalJSON
|
|
40
67
|
});
|
|
41
|
-
|
|
68
|
+
const row = /** @type {HTMLElement} */ (jml('jsoe-search-has-property', {
|
|
42
69
|
dataset: {searchPath: childPath, searchKind: 'hasProperty', propertyName},
|
|
43
70
|
$define: {
|
|
44
71
|
// Reads `childPath` off `this.dataset` rather than closing over the
|
|
@@ -56,7 +83,17 @@ function buildHasPropertyRow ({
|
|
|
56
83
|
const existsLeaf = existsCheck === undefined
|
|
57
84
|
? undefined
|
|
58
85
|
: makeHasPropertyLeaf(searchPath, existsCheck);
|
|
59
|
-
|
|
86
|
+
// "Doesn't have" already hides the child (its own `syncChildState`),
|
|
87
|
+
// since a value/presence constraint on an asserted-absent property
|
|
88
|
+
// is meaningless - skip its query outright rather than combining a
|
|
89
|
+
// stale/leftover constraint with a contradictory `$exists: false`.
|
|
90
|
+
// This matters most for `makePresenceOnlySearchType`'s leaves
|
|
91
|
+
// (undef/null/NaN), whose checkbox is permanently checked
|
|
92
|
+
// (`buildCheckbox`'s doc) and so would otherwise always contribute
|
|
93
|
+
// its `presence` leaf regardless of this row's own answer.
|
|
94
|
+
const childEl = existsCheck === false
|
|
95
|
+
? undefined
|
|
96
|
+
: findSearchElement(this, searchPath);
|
|
60
97
|
const childLeaf = childEl && hasGetQuery(childEl)
|
|
61
98
|
? childEl.getQuery()
|
|
62
99
|
: undefined;
|
|
@@ -65,8 +102,152 @@ function buildHasPropertyRow ({
|
|
|
65
102
|
}
|
|
66
103
|
}, [
|
|
67
104
|
buildHasPropertyToggle({name, propertyName}),
|
|
105
|
+
['button', {
|
|
106
|
+
type: 'button',
|
|
107
|
+
class: 'removePropertyButton',
|
|
108
|
+
$on: {
|
|
109
|
+
// `row`/`addPropertySelect`/`propertyName` are all closed over
|
|
110
|
+
// directly - safe since `buildHasPropertyRow` (unlike `$define`)
|
|
111
|
+
// runs fresh per row, so nothing here is shared across instances.
|
|
112
|
+
click () {
|
|
113
|
+
const option = /** @type {HTMLOptionElement|null} */ (
|
|
114
|
+
addPropertySelect.querySelector(
|
|
115
|
+
`option[value="${CSS.escape(propertyName)}"]`
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
if (option) {
|
|
119
|
+
option.disabled = false;
|
|
120
|
+
}
|
|
121
|
+
const objectRoot = row.closest('jsoe-search-object');
|
|
122
|
+
row.remove();
|
|
123
|
+
if (objectRoot) {
|
|
124
|
+
syncObjectValidity(/** @type {HTMLElement} */ (objectRoot));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, ['Remove']],
|
|
68
129
|
childArr
|
|
69
130
|
]));
|
|
131
|
+
|
|
132
|
+
// Asserting the property is absent ("Doesn't have") makes a value
|
|
133
|
+
// constraint on it meaningless, so hide the recursed child widget while
|
|
134
|
+
// that's selected rather than leaving it showing (and usable) alongside a
|
|
135
|
+
// contradictory "doesn't have" answer - `buildUI` runs fresh per row, so
|
|
136
|
+
// (unlike `$define`) it's safe for this listener to close over `select`/
|
|
137
|
+
// `childRoot` directly.
|
|
138
|
+
const select = /** @type {HTMLSelectElement|undefined} */ (
|
|
139
|
+
findOwnControl(row, 'select.jsoeSearchTriState--')
|
|
140
|
+
);
|
|
141
|
+
const childRoot = findSearchElement(row, childPath);
|
|
142
|
+
// "Doesn't have" hides the child entirely (a value constraint on an
|
|
143
|
+
// asserted-absent property is meaningless) - a hidden control is already
|
|
144
|
+
// exempt from constraint validation on its own. "Has" keeps the child
|
|
145
|
+
// visible and interactive (so "Has property X" AND "X matches Y" can
|
|
146
|
+
// still be combined), but that existence assertion is *already* a
|
|
147
|
+
// complete constraint by itself, so its own required inputs (whatever
|
|
148
|
+
// type the child turns out to be) are relaxed via `setDescendantsRequired`
|
|
149
|
+
// rather than disabled - only "(any)" (no existence assertion) leaves
|
|
150
|
+
// them actually required, since the child's own value is then the row's
|
|
151
|
+
// only possible contribution. `revalidateDescendants` covers anything
|
|
152
|
+
// that (unlike a plain `required` attribute) re-asserts its own
|
|
153
|
+
// constraint imperatively on every keystroke regardless of this row's
|
|
154
|
+
// state - a range pair's `isExemptedByAncestorHasProperty` check
|
|
155
|
+
// (`searchUtils.js`) only takes effect once something re-runs it, which
|
|
156
|
+
// otherwise wouldn't happen until the user next touches that field.
|
|
157
|
+
// `resyncAtLeastOne` covers the same gap for a child that's itself an
|
|
158
|
+
// `object`/`array`/`map`/etc. with its own "at least one facet" sentinel
|
|
159
|
+
// (`buildAtLeastOneSentinel`) - `revalidateDescendants`' event-redispatch
|
|
160
|
+
// reaches `array`/`map`/etc. fine (their own answer changes via a plain
|
|
161
|
+
// `input`/`change` on a real control), but not `objectSearchType.js`
|
|
162
|
+
// itself, whose sentinel changes when a row is added/removed - a button
|
|
163
|
+
// click and a structural DOM change, neither of which redispatching
|
|
164
|
+
// `input`/`change` events can simulate.
|
|
165
|
+
const syncChildState = () => {
|
|
166
|
+
if (!childRoot) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
childRoot.hidden = select?.value === 'false';
|
|
170
|
+
setDescendantsRequired(childRoot, select?.value === '');
|
|
171
|
+
revalidateDescendants(childRoot);
|
|
172
|
+
resyncAtLeastOne(childRoot);
|
|
173
|
+
};
|
|
174
|
+
select?.addEventListener('change', syncChildState);
|
|
175
|
+
syncChildState();
|
|
176
|
+
|
|
177
|
+
return row;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A required property's own value-match widget, wrapped in an opt-in
|
|
182
|
+
* "Search on this property" checkbox (default unchecked) around a disabled
|
|
183
|
+
* `<fieldset>`: a required property has no existence to ask about, but it
|
|
184
|
+
* still auto-appears with no "Remove" affordance the moment its parent
|
|
185
|
+
* object is added (search plan build order never lists it in the "Add
|
|
186
|
+
* property" pull-down at all) - and several leaf widgets now mark their own
|
|
187
|
+
* value input `required` (`buildLiteralRegexControls`'s doc), which would
|
|
188
|
+
* otherwise make the *whole* form invalid the instant such a property's
|
|
189
|
+
* object exists, before the user has asked to search on it at all.
|
|
190
|
+
* `<fieldset disabled>` is a native way to exempt a whole subtree from
|
|
191
|
+
* constraint validation in one step (every descendant control, whatever
|
|
192
|
+
* type it turns out to be, stops blocking `checkValidity`/`reportValidity`
|
|
193
|
+
* while disabled) - checking the box re-enables the fieldset, making its
|
|
194
|
+
* contents both interactive and, if left with a `required` field empty,
|
|
195
|
+
* actually invalid again.
|
|
196
|
+
* @param {{
|
|
197
|
+
* propertyName: string,
|
|
198
|
+
* propSchema: import('../../formats/schema.js').ZodexSchema,
|
|
199
|
+
* path: string,
|
|
200
|
+
* typeNamespace: string|undefined,
|
|
201
|
+
* topRoot: import('../../types.js').RootElement|undefined,
|
|
202
|
+
* types: import('../../types.js').default|undefined,
|
|
203
|
+
* originalJSON: import('../../formats/schema.js').ZodexSchema|undefined
|
|
204
|
+
* }} cfg
|
|
205
|
+
* @returns {HTMLElement}
|
|
206
|
+
*/
|
|
207
|
+
function buildRequiredPropertyRow ({
|
|
208
|
+
propertyName, propSchema, path, typeNamespace, topRoot, types, originalJSON
|
|
209
|
+
}) {
|
|
210
|
+
const childPath = `${path}/${escapeJSONPointer(propertyName)}`;
|
|
211
|
+
const name = `${typeNamespace}-requiredProperty-${propertyName}`;
|
|
212
|
+
const childArr = buildSearchWidget({
|
|
213
|
+
schemaObject: propSchema, path: childPath, typeNamespace, topRoot, types,
|
|
214
|
+
originalJSON
|
|
215
|
+
});
|
|
216
|
+
const row = /** @type {HTMLElement} */ (jml('jsoe-search-required-property', {
|
|
217
|
+
dataset: {searchPath: childPath, searchKind: 'requiredProperty', propertyName},
|
|
218
|
+
$define: {
|
|
219
|
+
/** @this {HTMLElement} */
|
|
220
|
+
getQuery () {
|
|
221
|
+
if (!readCheckbox(this)) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const searchPath = this.dataset.searchPath ?? '';
|
|
225
|
+
const childEl = findSearchElement(this, searchPath);
|
|
226
|
+
return childEl && hasGetQuery(childEl) ? childEl.getQuery() : undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}, [
|
|
230
|
+
buildCheckbox({name, label: `Search on "${propertyName}"`}),
|
|
231
|
+
['fieldset', {disabled: true, class: 'searchRequiredPropertyFieldset'}, [childArr]]
|
|
232
|
+
]));
|
|
233
|
+
|
|
234
|
+
const checkbox = /** @type {HTMLInputElement|undefined} */ (
|
|
235
|
+
findOwnControl(row, 'input.jsoeSearchCheckbox')
|
|
236
|
+
);
|
|
237
|
+
const fieldset = /** @type {HTMLFieldSetElement|null} */ (
|
|
238
|
+
row.querySelector('fieldset.searchRequiredPropertyFieldset')
|
|
239
|
+
);
|
|
240
|
+
checkbox?.addEventListener('change', () => {
|
|
241
|
+
if (fieldset) {
|
|
242
|
+
fieldset.disabled = !checkbox.checked;
|
|
243
|
+
}
|
|
244
|
+
const objectRoot = row.closest('jsoe-search-object');
|
|
245
|
+
if (objectRoot) {
|
|
246
|
+
syncObjectValidity(/** @type {HTMLElement} */ (objectRoot));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return row;
|
|
70
251
|
}
|
|
71
252
|
|
|
72
253
|
/**
|
|
@@ -74,10 +255,16 @@ function buildHasPropertyRow ({
|
|
|
74
255
|
* optional properties are offered ("avoid listing required" - a required
|
|
75
256
|
* property is guaranteed present, so asking about its existence is
|
|
76
257
|
* meaningless), and picking one from the pull-down adds a row for it
|
|
77
|
-
* (
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
258
|
+
* (disabled in the pull-down, rather than removed, so it can't be added a
|
|
259
|
+
* second time while its row exists). Each row's own "Remove" button
|
|
260
|
+
* discards the row and re-enables its property in the pull-down, for
|
|
261
|
+
* undoing an added-by-mistake property - a coarser-grained option than
|
|
262
|
+
* setting the row's toggle back to "(any)" (which already expresses "no
|
|
263
|
+
* constraint from this row" without removing it). A required property gets
|
|
264
|
+
* no pull-down entry (its existence is never in question), but does still
|
|
265
|
+
* get its own row - `buildRequiredPropertyRow`'s opt-in checkbox, defaulting
|
|
266
|
+
* unchecked/disabled, so it doesn't force a search constraint the user never
|
|
267
|
+
* asked for just by existing.
|
|
81
268
|
* @type {SearchTypeObject}
|
|
82
269
|
*/
|
|
83
270
|
const objectSearchType = {
|
|
@@ -86,14 +273,36 @@ const objectSearchType = {
|
|
|
86
273
|
const objectSchemaObject = /** @type {import('zodexy').SzObject} */ (
|
|
87
274
|
schemaObject
|
|
88
275
|
);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
276
|
+
const propertyEntries = Object.entries(objectSchemaObject.properties);
|
|
277
|
+
const availableProperties = propertyEntries.filter(
|
|
278
|
+
([, propSchema]) => propSchema.isOptional === true
|
|
279
|
+
);
|
|
280
|
+
// A required property is guaranteed present, so a has/doesn't-have
|
|
281
|
+
// toggle would be meaningless for it (same reasoning `availableProperties`
|
|
282
|
+
// already uses to leave it out of the "Add property" pull-down) - but it
|
|
283
|
+
// still gets its own row, via `buildRequiredPropertyRow`'s opt-in
|
|
284
|
+
// checkbox, so it doesn't silently force a search constraint the user
|
|
285
|
+
// never asked for just by its parent object existing.
|
|
286
|
+
const requiredProperties = propertyEntries.filter(
|
|
287
|
+
([, propSchema]) => propSchema.isOptional !== true
|
|
288
|
+
);
|
|
289
|
+
const requiredArr = requiredProperties.map(([propertyName, propSchema]) => (
|
|
290
|
+
buildRequiredPropertyRow({
|
|
291
|
+
propertyName, propSchema, path, typeNamespace, topRoot, types,
|
|
292
|
+
originalJSON
|
|
293
|
+
})
|
|
294
|
+
));
|
|
92
295
|
|
|
93
296
|
return ['jsoe-search-object', {
|
|
94
297
|
dataset: {searchPath: path, searchKind: 'object'},
|
|
95
298
|
title: label,
|
|
96
299
|
$define: {
|
|
300
|
+
/** @this {HTMLElement} */
|
|
301
|
+
connectedCallback () {
|
|
302
|
+
if (propertyEntries.length > 0) {
|
|
303
|
+
syncObjectValidity(this);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
97
306
|
/** @this {HTMLElement} */
|
|
98
307
|
getQuery () {
|
|
99
308
|
const rows = [...this.children].filter(hasGetQuery);
|
|
@@ -129,17 +338,27 @@ const objectSearchType = {
|
|
|
129
338
|
const propSchema = objectSchemaObject.properties[propertyName];
|
|
130
339
|
const row = buildHasPropertyRow({
|
|
131
340
|
propertyName, propSchema, path, typeNamespace, topRoot, types,
|
|
132
|
-
originalJSON
|
|
341
|
+
originalJSON, addPropertySelect: select
|
|
133
342
|
});
|
|
134
343
|
container.append(row);
|
|
135
|
-
const option =
|
|
136
|
-
|
|
344
|
+
const option = /** @type {HTMLOptionElement|null} */ (
|
|
345
|
+
select.querySelector(
|
|
346
|
+
`option[value="${CSS.escape(propertyName)}"]`
|
|
347
|
+
)
|
|
137
348
|
);
|
|
138
|
-
option
|
|
349
|
+
if (option) {
|
|
350
|
+
option.disabled = true;
|
|
351
|
+
}
|
|
352
|
+
select.value = '';
|
|
353
|
+
syncObjectValidity(/** @type {HTMLElement} */ (container));
|
|
139
354
|
}
|
|
140
355
|
}
|
|
141
356
|
}, ['Add']]
|
|
142
|
-
]]
|
|
357
|
+
]],
|
|
358
|
+
...requiredArr,
|
|
359
|
+
// Placed last purely for markup order; a plain rendered-but-off-
|
|
360
|
+
// screen input, so its position among siblings has no visual effect.
|
|
361
|
+
...(propertyEntries.length > 0 ? [buildAtLeastOneSentinel()] : [])
|
|
143
362
|
]];
|
|
144
363
|
},
|
|
145
364
|
getQuery: getQueryViaElement
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery,
|
|
3
|
+
buildMultiSelect, readMultiSelect, findOwnControl
|
|
4
|
+
} from '../searchUtils.js';
|
|
2
5
|
import {makeMultiSelectLeaf, combineAnd} from '../queryTreeBuilders.js';
|
|
3
6
|
import {getQueryViaElement} from '../searchElementUtils.js';
|
|
4
7
|
import regexpType from '../../fundamentalTypes/regexpType.js';
|
|
@@ -11,7 +14,13 @@ import regexpType from '../../fundamentalTypes/regexpType.js';
|
|
|
11
14
|
* Regexp (source): OR literal or regex search/Does Not contain search, plus
|
|
12
15
|
* a multiple-select search of flags (README). `regexpType.js`'s own
|
|
13
16
|
* `allowedFlags` list is imported as-is for the flags control (search plan
|
|
14
|
-
* §4 - already a plain exported property, no extraction needed).
|
|
17
|
+
* §4 - already a plain exported property, no extraction needed). Flags are
|
|
18
|
+
* a property of the regexp itself, not of how its source is being matched,
|
|
19
|
+
* but the Flags control is only shown (and only contributes to the query)
|
|
20
|
+
* while the source's own Mode is "Matches regex" - searching a source
|
|
21
|
+
* literal/substring while also constraining flags is a much rarer
|
|
22
|
+
* combination, and hiding Flags the rest of the time keeps the common case
|
|
23
|
+
* uncluttered.
|
|
15
24
|
* @type {SearchTypeObject}
|
|
16
25
|
*/
|
|
17
26
|
const regexpSearchType = {
|
|
@@ -26,7 +35,10 @@ const regexpSearchType = {
|
|
|
26
35
|
getQuery () {
|
|
27
36
|
const searchPath = this.dataset.searchPath ?? '';
|
|
28
37
|
const sourceLeaf = readLiteralRegexQuery(this, searchPath);
|
|
29
|
-
const
|
|
38
|
+
const mode = /** @type {HTMLSelectElement|undefined} */ (
|
|
39
|
+
findOwnControl(this, 'select.jsoeSearchMode--')
|
|
40
|
+
)?.value;
|
|
41
|
+
const selectedFlags = mode === 'regex' ? readMultiSelect(this) : [];
|
|
30
42
|
const flagsLeaf = selectedFlags.length
|
|
31
43
|
? makeMultiSelectLeaf(searchPath, {$in: selectedFlags})
|
|
32
44
|
: undefined;
|
|
@@ -35,8 +47,20 @@ const regexpSearchType = {
|
|
|
35
47
|
}
|
|
36
48
|
}, [
|
|
37
49
|
['span', {class: 'searchLabel'}, [`${label} (source)`]],
|
|
38
|
-
buildLiteralRegexControls({
|
|
39
|
-
|
|
50
|
+
buildLiteralRegexControls({
|
|
51
|
+
name,
|
|
52
|
+
/** @this {HTMLElement} */
|
|
53
|
+
onModeChange () {
|
|
54
|
+
const flagsLabel = this.closest('jsoe-search-regexp')?.querySelector(
|
|
55
|
+
'.searchRegexpFlags'
|
|
56
|
+
);
|
|
57
|
+
if (flagsLabel) {
|
|
58
|
+
/** @type {HTMLElement} */ (flagsLabel).hidden =
|
|
59
|
+
/** @type {HTMLSelectElement} */ (this).value !== 'regex';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
['label', {class: 'searchRegexpFlags', hidden: true}, [
|
|
40
64
|
'Flags: ',
|
|
41
65
|
buildMultiSelect({name: `${name}-flags`, options: regexpType.allowedFlags})
|
|
42
66
|
]]
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildPathLabel, buildLengthSizeControls, readLengthSizeQuery,
|
|
3
|
+
buildOptInFieldset, readOptInChecked, wireOptInFieldset,
|
|
4
|
+
buildAtLeastOneSentinel, syncAtLeastOneCheck
|
|
5
|
+
} from '../searchUtils.js';
|
|
2
6
|
import {combineAnd} from '../queryTreeBuilders.js';
|
|
3
7
|
import {findSearchElement, getQueryViaElement, hasGetQuery} from '../searchElementUtils.js';
|
|
4
8
|
import {buildSearchWidget} from '../searchDispatch.js';
|
|
@@ -7,11 +11,25 @@ import {buildSearchWidget} from '../searchDispatch.js';
|
|
|
7
11
|
* @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
|
|
8
12
|
*/
|
|
9
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {Element} root - a `jsoe-search-set` element
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function syncSetValidity (root) {
|
|
19
|
+
syncAtLeastOneCheck(
|
|
20
|
+
root, () => readLengthSizeQuery(root, '') !== undefined || readOptInChecked(root)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
/**
|
|
11
25
|
* Has length/size of <number> (README) - no sparse toggle: unlike
|
|
12
26
|
* `array`, a `Set` cannot have holes. Also recurses into the value schema's
|
|
13
27
|
* own search widget (as `arraySearchType.js` does for its element), via the
|
|
14
|
-
* same `*` path-segment convention (`queryTree.js`)
|
|
28
|
+
* same `*` path-segment convention (`queryTree.js`), wrapped in the same
|
|
29
|
+
* `buildOptInFieldset` `arraySearchType.js` uses and for the same reason -
|
|
30
|
+
* a length/size-only search should stay valid. Leaving *both* length/size
|
|
31
|
+
* and the element match unconfigured is still invalid, though
|
|
32
|
+
* (`buildAtLeastOneSentinel`), same as `arraySearchType.js`.
|
|
15
33
|
* @type {SearchTypeObject}
|
|
16
34
|
*/
|
|
17
35
|
const setSearchType = {
|
|
@@ -34,12 +52,20 @@ const setSearchType = {
|
|
|
34
52
|
dataset: {searchPath: path, searchKind: 'set'},
|
|
35
53
|
title: label,
|
|
36
54
|
$define: {
|
|
55
|
+
/** @this {HTMLElement} */
|
|
56
|
+
connectedCallback () {
|
|
57
|
+
wireOptInFieldset(this, '', () => syncSetValidity(this));
|
|
58
|
+
this.querySelector('input.jsoeSearchSize')?.addEventListener(
|
|
59
|
+
'input', () => syncSetValidity(this)
|
|
60
|
+
);
|
|
61
|
+
syncSetValidity(this);
|
|
62
|
+
},
|
|
37
63
|
/** @this {HTMLElement} */
|
|
38
64
|
getQuery () {
|
|
39
65
|
const searchPath = this.dataset.searchPath ?? '';
|
|
40
66
|
const lengthLeaf = readLengthSizeQuery(this, searchPath);
|
|
41
67
|
const elementEl = findSearchElement(this, `${searchPath}/*`);
|
|
42
|
-
const elementLeaf = elementEl && hasGetQuery(elementEl)
|
|
68
|
+
const elementLeaf = elementEl && hasGetQuery(elementEl) && readOptInChecked(this)
|
|
43
69
|
? elementEl.getQuery()
|
|
44
70
|
: undefined;
|
|
45
71
|
return combineAnd([lengthLeaf, elementLeaf]);
|
|
@@ -53,10 +79,10 @@ const setSearchType = {
|
|
|
53
79
|
max: setSchemaObject.maxSize,
|
|
54
80
|
includeSparse: false
|
|
55
81
|
}),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
...buildOptInFieldset({
|
|
83
|
+
name: `${name}-element`, label: 'Element matches', children: [elementArr]
|
|
84
|
+
}),
|
|
85
|
+
buildAtLeastOneSentinel()
|
|
60
86
|
]];
|
|
61
87
|
},
|
|
62
88
|
getQuery: getQueryViaElement
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery} from '../searchUtils.js';
|
|
2
2
|
import {getQueryViaElement} from '../searchElementUtils.js';
|
|
3
|
+
import regexpType from '../../fundamentalTypes/regexpType.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
|
|
@@ -8,7 +9,15 @@ import {getQueryViaElement} from '../searchElementUtils.js';
|
|
|
8
9
|
/**
|
|
9
10
|
* OR literal or regex search/Does Not contain search (README; also covers
|
|
10
11
|
* `StringObject` and `templateLiteral`, aliased to this same module in
|
|
11
|
-
* `searchDispatch.js` since the query semantics are identical)
|
|
12
|
+
* `searchDispatch.js` since the query semantics are identical), plus - only
|
|
13
|
+
* while "Matches regex" is the chosen mode - a Flags multi-select
|
|
14
|
+
* (`buildLiteralRegexControls`'s `flagOptions`, `regexpType.js`'s own
|
|
15
|
+
* `allowedFlags` list reused as-is, matching `regexpSearchType.js`'s flags
|
|
16
|
+
* for the actual regexp's own flags): unlike `regexpSearchType.js`, whose
|
|
17
|
+
* "source" is already a real regexp with its own flags regardless of
|
|
18
|
+
* search mode, here the regex is only ever the search's *own* device for
|
|
19
|
+
* matching the string, so flags only make sense while that regex is
|
|
20
|
+
* actually in play.
|
|
12
21
|
* @type {SearchTypeObject}
|
|
13
22
|
*/
|
|
14
23
|
const stringSearchType = {
|
|
@@ -26,7 +35,7 @@ const stringSearchType = {
|
|
|
26
35
|
}
|
|
27
36
|
}, [
|
|
28
37
|
['span', {class: 'searchLabel'}, [label]],
|
|
29
|
-
buildLiteralRegexControls({name})
|
|
38
|
+
buildLiteralRegexControls({name, flagOptions: regexpType.allowedFlags})
|
|
30
39
|
]];
|
|
31
40
|
},
|
|
32
41
|
getQuery: getQueryViaElement
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery} from '../searchUtils.js';
|
|
2
2
|
import {getQueryViaElement} from '../searchElementUtils.js';
|
|
3
|
+
import regexpType from '../../fundamentalTypes/regexpType.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
|
|
@@ -29,7 +30,7 @@ const symbolSearchType = {
|
|
|
29
30
|
}
|
|
30
31
|
}, [
|
|
31
32
|
['span', {class: 'searchLabel'}, [`${label} (description)`]],
|
|
32
|
-
buildLiteralRegexControls({name})
|
|
33
|
+
buildLiteralRegexControls({name, flagOptions: regexpType.allowedFlags})
|
|
33
34
|
]];
|
|
34
35
|
},
|
|
35
36
|
getQuery: getQueryViaElement
|
package/src/search/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {getSearchTypeObject, buildSearchWidget, resolveIntersection} from './sea
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {{
|
|
6
|
-
* container:
|
|
6
|
+
* container: HTMLFormElement,
|
|
7
7
|
* $getQuery: () => import('./queryTree.js').QueryAnd,
|
|
8
8
|
* whenReady: Promise<void>
|
|
9
9
|
* }} SearchChoicesControl
|
|
@@ -36,12 +36,22 @@ import {getSearchTypeObject, buildSearchWidget, resolveIntersection} from './sea
|
|
|
36
36
|
* `$getQuery()` reads the live DOM into a `QueryAnd` on demand (`{$and:
|
|
37
37
|
* []}` when nothing has been entered anywhere, matching the empty-`$and`
|
|
38
38
|
* convention of "no constraint"), and a host that wants live updates wraps
|
|
39
|
-
* it in its own `container.addEventListener('input', ...)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* it in its own `container.addEventListener('input', ...)`. Every widget
|
|
40
|
+
* built so far is synchronous, so `whenReady` (unlike `buildTypeChoices`'s)
|
|
41
|
+
* has no deferred work to wait on and settles immediately - it's still
|
|
42
|
+
* returned, so a caller can `await` either function uniformly without
|
|
43
|
+
* checking which one it has.
|
|
44
|
+
*
|
|
45
|
+
* `container` is a real `<form>` (its own `submit` prevented, since it is
|
|
46
|
+
* never actually submitted) rather than a plain `<div>`, matching the edit
|
|
47
|
+
* side's own root `<form>` (`src/types.js`'s `Types.validValuesSet` reads
|
|
48
|
+
* `form.checkValidity()`) - the various cross-field checks the individual
|
|
49
|
+
* search widgets wire up (`searchUtils.js`'s `buildRangeInputsPair`,
|
|
50
|
+
* `dateSearchType.js`, etc.) are native Constraint Validation API calls
|
|
51
|
+
* (`setCustomValidity`/`required`), so a host only needs the one native
|
|
52
|
+
* `container.checkValidity()`/`container.reportValidity()` to check them
|
|
53
|
+
* all at once, the same way `demo/index-search.js`'s "Check validity"
|
|
54
|
+
* button does.
|
|
45
55
|
* @param {{
|
|
46
56
|
* schemaContent: import('../formats/schema.js').ZodexSchema,
|
|
47
57
|
* typeNamespace?: string,
|
|
@@ -62,8 +72,15 @@ export function buildSearchChoices ({schemaContent, typeNamespace, topRoot, type
|
|
|
62
72
|
schemaObject: schemaContent, path, typeNamespace, topRoot, types,
|
|
63
73
|
originalJSON: schemaContent
|
|
64
74
|
});
|
|
65
|
-
const container = /** @type {
|
|
66
|
-
jml('
|
|
75
|
+
const container = /** @type {HTMLFormElement} */ (
|
|
76
|
+
jml('form', {
|
|
77
|
+
class: 'searchChoicesContainer',
|
|
78
|
+
$on: {
|
|
79
|
+
submit (e) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [arr])
|
|
67
84
|
);
|
|
68
85
|
const schemaObject = resolveIntersection(schemaContent, schemaContent);
|
|
69
86
|
return {
|
package/src/search/queryTree.js
CHANGED
|
@@ -92,6 +92,14 @@
|
|
|
92
92
|
* @typedef {{kind: 'integerCheck', path: string, isInteger: boolean}} QueryIntegerCheckLeaf
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `date`: is/is-not a valid `Date` (a structured-cloned `Date` can itself be
|
|
97
|
+
* an "Invalid Date" - a `NaN` internal time value - `dateType.js`'s own
|
|
98
|
+
* `notANum`/`ValidDate`/`InvalidDate` handling deals with the same state on
|
|
99
|
+
* the edit side).
|
|
100
|
+
* @typedef {{kind: 'validDateCheck', path: string, isValid: boolean}} QueryValidDateCheckLeaf
|
|
101
|
+
*/
|
|
102
|
+
|
|
95
103
|
/**
|
|
96
104
|
* `string`/`StringObject`/`Blob`/`File`/regexp-source/symbol-description
|
|
97
105
|
* (via `$in`/`$nin`).
|
|
@@ -220,7 +228,8 @@
|
|
|
220
228
|
|
|
221
229
|
/**
|
|
222
230
|
* @typedef {QueryHasPropertyLeaf|QueryLengthSizeLeaf|QueryRangeLeaf|
|
|
223
|
-
* QueryNotLeaf|QueryIntegerCheckLeaf|
|
|
231
|
+
* QueryNotLeaf|QueryIntegerCheckLeaf|QueryValidDateCheckLeaf|
|
|
232
|
+
* QueryLiteralSetLeaf|QueryRegexLeaf|
|
|
224
233
|
* QueryNotContainsLeaf|QueryMultiSelectLeaf|QueryKeyValueEnumLeaf|
|
|
225
234
|
* QueryTypeOfLeaf|QueryBlobHTMLLeaf|QueryDomShapeLeaf|
|
|
226
235
|
* QueryMapRecordJointLeaf|QueryPassThroughLeaf|QueryPresenceLeaf|
|
|
@@ -97,6 +97,15 @@ export function makeIntegerCheckLeaf (path, isInteger) {
|
|
|
97
97
|
return {kind: 'integerCheck', path, isInteger};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} path
|
|
102
|
+
* @param {boolean} isValid
|
|
103
|
+
* @returns {import('./queryTree.js').QueryValidDateCheckLeaf}
|
|
104
|
+
*/
|
|
105
|
+
export function makeValidDateCheckLeaf (path, isValid) {
|
|
106
|
+
return {kind: 'validDateCheck', path, isValid};
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
/**
|
|
101
110
|
* @param {string} path
|
|
102
111
|
* @param {{$in?: unknown[], $nin?: unknown[]}} cfg
|