@es-joy/jsoe 0.29.0 → 0.31.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 (88) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGES.md +11 -0
  3. package/README.md +3 -0
  4. package/dist/fundamentalTypes/dateType.d.ts +8 -3
  5. package/dist/fundamentalTypes/dateType.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/search/fundamentalTypes/arraySearchType.d.ts +11 -4
  9. package/dist/search/fundamentalTypes/arraySearchType.d.ts.map +1 -1
  10. package/dist/search/fundamentalTypes/bigintSearchType.d.ts.map +1 -1
  11. package/dist/search/fundamentalTypes/blobSearchType.d.ts.map +1 -1
  12. package/dist/search/fundamentalTypes/dateSearchType.d.ts +8 -3
  13. package/dist/search/fundamentalTypes/dateSearchType.d.ts.map +1 -1
  14. package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts +5 -5
  15. package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts.map +1 -1
  16. package/dist/search/fundamentalTypes/enumSearchType.d.ts +4 -0
  17. package/dist/search/fundamentalTypes/enumSearchType.d.ts.map +1 -1
  18. package/dist/search/fundamentalTypes/fileSearchType.d.ts +19 -10
  19. package/dist/search/fundamentalTypes/fileSearchType.d.ts.map +1 -1
  20. package/dist/search/fundamentalTypes/filelistSearchType.d.ts +4 -12
  21. package/dist/search/fundamentalTypes/filelistSearchType.d.ts.map +1 -1
  22. package/dist/search/fundamentalTypes/functionSearchType.d.ts +4 -4
  23. package/dist/search/fundamentalTypes/functionSearchType.d.ts.map +1 -1
  24. package/dist/search/fundamentalTypes/mapSearchType.d.ts +8 -4
  25. package/dist/search/fundamentalTypes/mapSearchType.d.ts.map +1 -1
  26. package/dist/search/fundamentalTypes/numberSearchType.d.ts.map +1 -1
  27. package/dist/search/fundamentalTypes/objectSearchType.d.ts +10 -4
  28. package/dist/search/fundamentalTypes/objectSearchType.d.ts.map +1 -1
  29. package/dist/search/fundamentalTypes/regexpSearchType.d.ts +7 -1
  30. package/dist/search/fundamentalTypes/regexpSearchType.d.ts.map +1 -1
  31. package/dist/search/fundamentalTypes/setSearchType.d.ts +5 -4
  32. package/dist/search/fundamentalTypes/setSearchType.d.ts.map +1 -1
  33. package/dist/search/fundamentalTypes/stringSearchType.d.ts +9 -1
  34. package/dist/search/fundamentalTypes/stringSearchType.d.ts.map +1 -1
  35. package/dist/search/fundamentalTypes/symbolSearchType.d.ts.map +1 -1
  36. package/dist/search/index.d.ts +18 -8
  37. package/dist/search/index.d.ts.map +1 -1
  38. package/dist/search/queryTree.d.ts +15 -1
  39. package/dist/search/queryTree.d.ts.map +1 -1
  40. package/dist/search/queryTreeBuilders.d.ts +8 -1
  41. package/dist/search/queryTreeBuilders.d.ts.map +1 -1
  42. package/dist/search/searchElementUtils.d.ts +17 -8
  43. package/dist/search/searchElementUtils.d.ts.map +1 -1
  44. package/dist/search/searchUtils.d.ts +350 -18
  45. package/dist/search/searchUtils.d.ts.map +1 -1
  46. package/dist/search/subTypes/blobHTMLSearchType.d.ts +32 -5
  47. package/dist/search/subTypes/blobHTMLSearchType.d.ts.map +1 -1
  48. package/dist/search/subTypes/recordSearchType.d.ts +4 -4
  49. package/dist/search/subTypes/recordSearchType.d.ts.map +1 -1
  50. package/dist/search/subTypes/tupleSearchType.d.ts +3 -3
  51. package/dist/search/subTypes/tupleSearchType.d.ts.map +1 -1
  52. package/dist/search/superTypes/buffersourceSearchType.d.ts.map +1 -1
  53. package/dist/search/unions/unionFamilySearchType.d.ts.map +1 -1
  54. package/package.json +3 -3
  55. package/pnpm-workspace.yaml +26 -26
  56. package/src/fundamentalTypes/dateType.js +9 -4
  57. package/src/fundamentalTypes/regexpType.js +1 -1
  58. package/src/jsoe.css +204 -0
  59. package/src/search/fundamentalTypes/arraySearchType.js +42 -7
  60. package/src/search/fundamentalTypes/bigintSearchType.js +7 -1
  61. package/src/search/fundamentalTypes/blobSearchType.js +2 -1
  62. package/src/search/fundamentalTypes/booleanSearchType.js +1 -1
  63. package/src/search/fundamentalTypes/dateSearchType.js +113 -20
  64. package/src/search/fundamentalTypes/domexceptionSearchType.js +39 -10
  65. package/src/search/fundamentalTypes/enumSearchType.js +5 -1
  66. package/src/search/fundamentalTypes/fileSearchType.js +64 -11
  67. package/src/search/fundamentalTypes/filelistSearchType.js +32 -7
  68. package/src/search/fundamentalTypes/functionSearchType.js +36 -12
  69. package/src/search/fundamentalTypes/mapSearchType.js +43 -13
  70. package/src/search/fundamentalTypes/numberSearchType.js +8 -1
  71. package/src/search/fundamentalTypes/objectSearchType.js +236 -17
  72. package/src/search/fundamentalTypes/regexpSearchType.js +29 -5
  73. package/src/search/fundamentalTypes/setSearchType.js +33 -7
  74. package/src/search/fundamentalTypes/stringSearchType.js +11 -2
  75. package/src/search/fundamentalTypes/symbolSearchType.js +2 -1
  76. package/src/search/index.js +26 -9
  77. package/src/search/queryTree.js +14 -2
  78. package/src/search/queryTreeBuilders.js +12 -2
  79. package/src/search/searchElementUtils.js +108 -24
  80. package/src/search/searchUtils.js +576 -24
  81. package/src/search/subTypes/blobHTMLSearchType.js +202 -9
  82. package/src/search/subTypes/recordSearchType.js +34 -13
  83. package/src/search/subTypes/tupleSearchType.js +51 -10
  84. package/src/search/superTypes/SpecialRealNumberSearchType.js +1 -1
  85. package/src/search/superTypes/buffersourceSearchType.js +7 -1
  86. package/src/search/unions/unionFamilySearchType.js +8 -0
  87. package/vendor/zod/classic/schemas.d.ts +3 -1
  88. package/vendor/zod/core/versions.js +1 -1
@@ -1,6 +1,10 @@
1
1
  import {jml} from '../../vendor-imports.js';
2
2
  import {escapeJSONPointer} from '../../utils/jsonPointer.js';
3
- import {buildPathLabel, buildHasPropertyToggle, readTriStateSelect} from '../searchUtils.js';
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
- return /** @type {HTMLElement} */ (jml('jsoe-search-has-property', {
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
- const childEl = findSearchElement(this, searchPath);
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
- * (removed from the pull-down so it can't be added twice); there is no
78
- * remove affordance, since setting a row's toggle back to "(any)" already
79
- * expresses "no constraint from this row" for the existence half, same as
80
- * clearing the nested widget does for the value half.
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 availableProperties = Object.entries(
90
- objectSchemaObject.properties
91
- ).filter(([, propSchema]) => propSchema.isOptional === true);
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 = select.querySelector(
136
- `option[value="${CSS.escape(propertyName)}"]`
344
+ const option = /** @type {HTMLOptionElement|null} */ (
345
+ select.querySelector(
346
+ `option[value="${CSS.escape(propertyName)}"]`
347
+ )
137
348
  );
138
- option?.remove();
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 {buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery, buildMultiSelect, readMultiSelect} from '../searchUtils.js';
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 selectedFlags = readMultiSelect(this);
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({name}),
39
- ['label', [
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 {buildPathLabel, buildLengthSizeControls, readLengthSizeQuery} from '../searchUtils.js';
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 &lt;number&gt; (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
- ['div', {class: 'searchSetElement'}, [
57
- ['span', ['Element matches: ']],
58
- elementArr
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
@@ -3,7 +3,7 @@ import {getSearchTypeObject, buildSearchWidget, resolveIntersection} from './sea
3
3
 
4
4
  /**
5
5
  * @typedef {{
6
- * container: HTMLDivElement,
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', ...)` since
40
- * `container` is a plain `HTMLDivElement`. Every widget built so far is
41
- * synchronous, so `whenReady` (unlike `buildTypeChoices`'s) has no deferred
42
- * work to wait on and settles immediately - it's still returned, so a
43
- * caller can `await` either function uniformly without checking which one
44
- * it has.
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 {HTMLDivElement} */ (
66
- jml('div', {class: 'searchChoicesContainer'}, [arr])
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 {
@@ -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`).
@@ -159,11 +167,14 @@
159
167
 
160
168
  /**
161
169
  * XPath/CSS-selector/full-text/raw-HTML-regex; no Mongo equivalent.
170
+ * `$options`, mirroring `QueryRegexLeaf`'s field of the same name, is only
171
+ * ever populated for `mode: 'rawHTMLRegex'`.
162
172
  * @typedef {{
163
173
  * kind: 'blobHTML',
164
174
  * path: string,
165
175
  * mode: 'xpath'|'cssSelector'|'fullText'|'rawHTMLRegex',
166
- * value: string
176
+ * value: string,
177
+ * $options?: string
167
178
  * }} QueryBlobHTMLLeaf
168
179
  */
169
180
 
@@ -220,7 +231,8 @@
220
231
 
221
232
  /**
222
233
  * @typedef {QueryHasPropertyLeaf|QueryLengthSizeLeaf|QueryRangeLeaf|
223
- * QueryNotLeaf|QueryIntegerCheckLeaf|QueryLiteralSetLeaf|QueryRegexLeaf|
234
+ * QueryNotLeaf|QueryIntegerCheckLeaf|QueryValidDateCheckLeaf|
235
+ * QueryLiteralSetLeaf|QueryRegexLeaf|
224
236
  * QueryNotContainsLeaf|QueryMultiSelectLeaf|QueryKeyValueEnumLeaf|
225
237
  * QueryTypeOfLeaf|QueryBlobHTMLLeaf|QueryDomShapeLeaf|
226
238
  * 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
@@ -163,10 +172,11 @@ export function makeTypeOfLeaf (path, searchType, discriminatorValue) {
163
172
  * @param {string} path
164
173
  * @param {import('./queryTree.js').QueryBlobHTMLLeaf['mode']} mode
165
174
  * @param {string} value
175
+ * @param {string} [$options]
166
176
  * @returns {import('./queryTree.js').QueryBlobHTMLLeaf}
167
177
  */
168
- export function makeBlobHTMLLeaf (path, mode, value) {
169
- return {kind: 'blobHTML', path, mode, value};
178
+ export function makeBlobHTMLLeaf (path, mode, value, $options) {
179
+ return {kind: 'blobHTML', path, mode, value, ...($options ? {$options} : {})};
170
180
  }
171
181
 
172
182
  /**