@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.
Files changed (85) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGES.md +6 -0
  3. package/README.md +2 -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 +12 -1
  39. package/dist/search/queryTree.d.ts.map +1 -1
  40. package/dist/search/queryTreeBuilders.d.ts +6 -0
  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 +343 -18
  45. package/dist/search/searchUtils.d.ts.map +1 -1
  46. package/dist/search/subTypes/blobHTMLSearchType.d.ts +22 -2
  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 +1 -1
  55. package/src/fundamentalTypes/dateType.js +9 -4
  56. package/src/fundamentalTypes/regexpType.js +1 -1
  57. package/src/jsoe.css +204 -0
  58. package/src/search/fundamentalTypes/arraySearchType.js +42 -7
  59. package/src/search/fundamentalTypes/bigintSearchType.js +7 -1
  60. package/src/search/fundamentalTypes/blobSearchType.js +2 -1
  61. package/src/search/fundamentalTypes/booleanSearchType.js +1 -1
  62. package/src/search/fundamentalTypes/dateSearchType.js +113 -20
  63. package/src/search/fundamentalTypes/domexceptionSearchType.js +39 -10
  64. package/src/search/fundamentalTypes/enumSearchType.js +5 -1
  65. package/src/search/fundamentalTypes/fileSearchType.js +64 -11
  66. package/src/search/fundamentalTypes/filelistSearchType.js +32 -7
  67. package/src/search/fundamentalTypes/functionSearchType.js +36 -12
  68. package/src/search/fundamentalTypes/mapSearchType.js +43 -13
  69. package/src/search/fundamentalTypes/numberSearchType.js +8 -1
  70. package/src/search/fundamentalTypes/objectSearchType.js +236 -17
  71. package/src/search/fundamentalTypes/regexpSearchType.js +29 -5
  72. package/src/search/fundamentalTypes/setSearchType.js +33 -7
  73. package/src/search/fundamentalTypes/stringSearchType.js +11 -2
  74. package/src/search/fundamentalTypes/symbolSearchType.js +2 -1
  75. package/src/search/index.js +26 -9
  76. package/src/search/queryTree.js +10 -1
  77. package/src/search/queryTreeBuilders.js +9 -0
  78. package/src/search/searchElementUtils.js +108 -24
  79. package/src/search/searchUtils.js +516 -24
  80. package/src/search/subTypes/blobHTMLSearchType.js +64 -7
  81. package/src/search/subTypes/recordSearchType.js +34 -13
  82. package/src/search/subTypes/tupleSearchType.js +51 -10
  83. package/src/search/superTypes/SpecialRealNumberSearchType.js +1 -1
  84. package/src/search/superTypes/buffersourceSearchType.js +7 -1
  85. package/src/search/unions/unionFamilySearchType.js +8 -0
package/src/jsoe.css CHANGED
@@ -151,6 +151,24 @@ video.recordedMedia {
151
151
  color: #b00;
152
152
  }
153
153
 
154
+ /* `demo/index-search.js`'s "Check validity" button and its immediately-
155
+ following result text (`.validityResult`) are both inline (a `<button>`
156
+ and a `<span>`), so without this they run together with no gap. */
157
+ .checkValidityButton {
158
+ margin-inline-end: 0.5em;
159
+ }
160
+
161
+ /* `demo/index-search.js`'s per-section "Get query" result: a query for a
162
+ deeply-nested schema can be a long single-line JSON string, so this needs
163
+ the same scrollable-box treatment as `.jsoe-raw-editor` rather than
164
+ growing the page. */
165
+ .queryResult {
166
+ max-width: 100%;
167
+ max-height: 300px;
168
+ overflow: auto;
169
+ border: 1px solid;
170
+ }
171
+
154
172
  .visualizer {
155
173
  height: 60px;
156
174
  }
@@ -211,6 +229,7 @@ video.recordedMedia {
211
229
  background-color: purple;
212
230
  }
213
231
  [data-type="date"] {
232
+ color: white;
214
233
  background-color: teal;
215
234
  }
216
235
  [data-type="regexp"] {
@@ -258,6 +277,191 @@ video.recordedMedia {
258
277
  background-color: lightgray;
259
278
  }
260
279
 
280
+ /* Search widgets (`buildSearchChoices`/`buildSearchWidget`, `src/search/`):
281
+ every built widget's custom-element root carries `data-search-kind`
282
+ (the leaf/composite kind it was built for - `object`, `array`, or, for
283
+ the "checked"-type factories that have no structural schema of their
284
+ own, the full tag name e.g. `jsoe-search-error`). Block-level with the
285
+ same border/padding treatment as `fieldset.xorTypeChoices` above, so a
286
+ hierarchical type's recursively-nested child widgets land on their own
287
+ indented line rather than running together inline, and the same
288
+ background colors as the corresponding view/edit UI `[data-type]` rules
289
+ above, so a given type reads the same whether browsing or searching. */
290
+ [data-search-kind] {
291
+ display: block;
292
+ margin-block: 0.3em;
293
+ padding: 0.3em 0.6em 0.4em;
294
+ border: 1px solid #ccc;
295
+ border-radius: 4px;
296
+ /* Reset explicitly rather than relying on inheriting the page default:
297
+ a light-background kind (e.g. `tuple`'s "args" widget nested inside a
298
+ dark-background `function`) would otherwise inherit that ancestor's
299
+ `color: white` and become unreadable against its own light
300
+ background. */
301
+ color: #000;
302
+ }
303
+
304
+ /* `objectSearchType.js` hides a "Doesn't have" property's own recursed
305
+ widget via the native `hidden` attribute - restore its effect, since the
306
+ unconditional `display: block` above (an author rule) would otherwise
307
+ beat the browser's default `[hidden] { display: none }` UA rule. */
308
+ [data-search-kind][hidden] {
309
+ display: none;
310
+ }
311
+
312
+ /* A descendant selector rather than `>` (direct child): every search module
313
+ places its own `.searchLabel` as a direct child of its widget root except
314
+ `objectSearchType.js`, which nests it one level deeper inside its own
315
+ `.searchObjectControls` wrapper (alongside the "Add property" pull-down) -
316
+ a `>` combinator would leave that one span inline, squished directly
317
+ against the pull-down's label right after it with no gap. */
318
+ [data-search-kind] .searchLabel {
319
+ display: block;
320
+ font-weight: bold;
321
+ margin-block-end: 0.2em;
322
+ }
323
+
324
+ /* `objectSearchType.js`'s required-property row, and `searchUtils.js`'s
325
+ shared `buildOptInFieldset` (array/set/tuple/filelist's element match(es),
326
+ map/record's key/value, file's name/content-type), both wrap a recursed
327
+ widget in a `<fieldset disabled>` (native constraint-validation opt-out
328
+ for the whole subtree, toggled by an "opt in" checkbox) purely for that
329
+ behavior, not for its default border/padding - which would otherwise
330
+ double up with the child widget's own `[data-search-kind]` box drawn just
331
+ inside it. `[class^=...]` matches `buildOptInFieldset`'s `key`-suffixed
332
+ class regardless of `key`. */
333
+ fieldset.searchRequiredPropertyFieldset,
334
+ fieldset[class^="jsoeSearchOptInFieldset--"] {
335
+ border: none;
336
+ margin: 0;
337
+ padding: 0;
338
+ }
339
+
340
+ /* `buildOptInFieldset`'s own "Key matches"/"Element matches"/etc. checkbox
341
+ label - `[data-search-kind] label`'s `margin-inline-end` above is meant
342
+ for short *inline* control pairs sharing one line (a "From:"/"To:"
343
+ pair), not a facet-toggling label like this one, which reads better
344
+ started on its own line rather than run together with whatever came
345
+ before it (an adjacent length/size control, another facet's own label,
346
+ etc). */
347
+ [data-search-kind] .jsoeSearchOptInLabel {
348
+ display: block;
349
+ }
350
+
351
+ /* `searchUtils.js`'s `buildAtLeastOneSentinel` - kept off-screen without
352
+ `display: none`/the `hidden` attribute, which would exempt it from the
353
+ Constraint Validation API entirely (the same reason `[hidden]` needed a
354
+ `display: none` override above for `objectSearchType.js`'s "Doesn't
355
+ have" case, just the opposite problem: there, hiding needed to *win*;
356
+ here, hiding must *not* affect validity). Standard "visually-hidden"
357
+ technique - present and rendered, just clipped to nothing. */
358
+ .searchAtLeastOneSentinel {
359
+ position: absolute;
360
+ width: 1px;
361
+ height: 1px;
362
+ margin: -1px;
363
+ padding: 0;
364
+ overflow: hidden;
365
+ clip: rect(0, 0, 0, 0);
366
+ white-space: nowrap;
367
+ border: 0;
368
+ }
369
+
370
+ /* Inline control pairs built by `searchUtils.js` (a "Mode:"/"Value:" pair
371
+ from `buildLiteralRegexControls`, a "From:"/"To:" pair from
372
+ `buildRangeInputsPair`, a trailing tri-state like "Integer:") are each
373
+ their own `<label>` with no whitespace between them in the built markup,
374
+ so without this they visually run together (e.g. "To: [input]Integer:
375
+ [select]"). */
376
+ [data-search-kind] label {
377
+ margin-inline-end: 0.6em;
378
+ }
379
+
380
+ /* Nested child widgets get progressively inset, so a hierarchical type's
381
+ structure (object properties, array/set elements, map/record key-value,
382
+ union branches, etc.) stays legible several levels deep. */
383
+ [data-search-kind] [data-search-kind] {
384
+ margin-inline-start: 0.8em;
385
+ }
386
+
387
+ /* Dark backgrounds get a light text color so labels/inputs stay readable
388
+ (unlike the view/edit `[data-type]` rules above, which - for these same
389
+ colors - rely on their content being a short, bolded inline leaf rather
390
+ than a whole widget's worth of labels and form controls). */
391
+ [data-search-kind="jsoe-search-undefined"] {
392
+ background-color: green;
393
+ color: white;
394
+ }
395
+ [data-search-kind="jsoe-search-null"] {
396
+ background-color: lightgreen;
397
+ }
398
+ [data-search-kind="jsoe-search-nan"] {
399
+ background-color: green;
400
+ color: white;
401
+ }
402
+ [data-search-kind="boolean"] {
403
+ background-color: aqua;
404
+ }
405
+ [data-search-kind="number"] {
406
+ background-color: violet;
407
+ }
408
+ [data-search-kind="string"] {
409
+ background-color: lightblue;
410
+ }
411
+ [data-search-kind="array"], [data-search-kind="tuple"] {
412
+ background-color: lightyellow;
413
+ }
414
+ [data-search-kind="object"], [data-search-kind="record"] {
415
+ background-color: orange;
416
+ }
417
+ [data-search-kind="symbol"] {
418
+ background-color: lightgreen;
419
+ }
420
+ [data-search-kind="promise"] {
421
+ background-color: orange;
422
+ }
423
+ [data-search-kind="function"] {
424
+ background-color: teal;
425
+ color: white;
426
+ }
427
+ [data-search-kind="date"] {
428
+ background-color: teal;
429
+ color: white;
430
+ }
431
+ [data-search-kind="regexp"] {
432
+ background-color: yellow;
433
+ }
434
+ [data-search-kind="SpecialRealNumber"] {
435
+ background-color: red;
436
+ color: white;
437
+ }
438
+ [data-search-kind="blobHTML"] {
439
+ background-color: sandybrown;
440
+ }
441
+ [data-search-kind="jsoe-search-error"],
442
+ [data-search-kind="jsoe-search-errors-special"] {
443
+ background-color: red;
444
+ color: white;
445
+ }
446
+ [data-search-kind="file"] {
447
+ background-color: beige;
448
+ }
449
+ [data-search-kind="filelist"] {
450
+ background-color: bisque;
451
+ }
452
+ [data-search-kind="blob"] {
453
+ background-color: lightgray;
454
+ }
455
+ [data-search-kind="buffersource"] {
456
+ background-color: beige;
457
+ }
458
+ [data-search-kind="map"], [data-search-kind="set"],
459
+ [data-search-kind="bigint"], [data-search-kind="domexception"],
460
+ [data-search-kind="jsoe-search-domrect"], [data-search-kind="jsoe-search-dompoint"],
461
+ [data-search-kind="jsoe-search-dommatrix"] {
462
+ background-color: lightgray;
463
+ }
464
+
261
465
  .typedArrayArea {
262
466
  overflow: auto;
263
467
  max-height: 250px;
@@ -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,12 +11,32 @@ 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-array` element
16
+ * @returns {void}
17
+ */
18
+ function syncArrayValidity (root) {
19
+ syncAtLeastOneCheck(
20
+ root, () => readLengthSizeQuery(root, '') !== undefined || readOptInChecked(root)
21
+ );
22
+ }
23
+
10
24
  /**
11
25
  * Has length/size of &lt;number&gt;; Is/Is not sparse (README), plus - by
12
26
  * recursing into the element schema's own search widget (search plan §6
13
27
  * build order) - "the array contains at least one element matching Y",
14
28
  * expressed via a `*` path segment (`queryTree.js`) so no dedicated wrapper
15
- * leaf kind is needed: the two constraints simply combine with `$and`.
29
+ * leaf kind is needed: the two constraints simply combine with `$and`. The
30
+ * element match is wrapped in `buildOptInFieldset` (default unchecked/
31
+ * disabled) so a length/size-only search stays valid: unlike the length/
32
+ * size inputs (plain, un-`required` numbers), the recursed element widget
33
+ * can itself carry `required` controls (e.g. a `string` element's Value),
34
+ * which would otherwise force this whole array invalid just for existing,
35
+ * whether or not the user actually wants an element constraint. Leaving
36
+ * both length/size and the element match unconfigured at once is still
37
+ * invalid, though (`buildAtLeastOneSentinel`) - the same "no absent values"
38
+ * reasoning as everywhere else, just with two facets where either alone
39
+ * already suffices.
16
40
  * @type {SearchTypeObject}
17
41
  */
18
42
  const arraySearchType = {
@@ -35,12 +59,23 @@ const arraySearchType = {
35
59
  dataset: {searchPath: path, searchKind: 'array'},
36
60
  title: label,
37
61
  $define: {
62
+ /** @this {HTMLElement} */
63
+ connectedCallback () {
64
+ wireOptInFieldset(this, '', () => syncArrayValidity(this));
65
+ this.querySelector('input.jsoeSearchSize')?.addEventListener(
66
+ 'input', () => syncArrayValidity(this)
67
+ );
68
+ this.querySelector('select.jsoeSearchTriState--')?.addEventListener(
69
+ 'change', () => syncArrayValidity(this)
70
+ );
71
+ syncArrayValidity(this);
72
+ },
38
73
  /** @this {HTMLElement} */
39
74
  getQuery () {
40
75
  const searchPath = this.dataset.searchPath ?? '';
41
76
  const lengthLeaf = readLengthSizeQuery(this, searchPath);
42
77
  const elementEl = findSearchElement(this, `${searchPath}/*`);
43
- const elementLeaf = elementEl && hasGetQuery(elementEl)
78
+ const elementLeaf = elementEl && hasGetQuery(elementEl) && readOptInChecked(this)
44
79
  ? elementEl.getQuery()
45
80
  : undefined;
46
81
  return combineAnd([lengthLeaf, elementLeaf]);
@@ -54,10 +89,10 @@ const arraySearchType = {
54
89
  max: arraySchemaObject.maxLength,
55
90
  includeSparse: true
56
91
  }),
57
- ['div', {class: 'searchArrayElement'}, [
58
- ['span', ['Element matches: ']],
59
- elementArr
60
- ]]
92
+ ...buildOptInFieldset({
93
+ name: `${name}-element`, label: 'Element matches', children: [elementArr]
94
+ }),
95
+ buildAtLeastOneSentinel()
61
96
  ]];
62
97
  },
63
98
  getQuery: getQueryViaElement
@@ -1,4 +1,6 @@
1
- import {buildPathLabel, buildRangeInputsPair, readRangeInputsPair} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildRangeInputsPair, readRangeInputsPair, syncRangeValidity
3
+ } from '../searchUtils.js';
2
4
  import {makeRangeLeaf} from '../queryTreeBuilders.js';
3
5
  import {getQueryViaElement} from '../searchElementUtils.js';
4
6
 
@@ -37,6 +39,10 @@ const bigintSearchType = {
37
39
  dataset: {searchPath: path, searchKind: 'bigint'},
38
40
  title: label,
39
41
  $define: {
42
+ /** @this {HTMLElement} */
43
+ connectedCallback () {
44
+ syncRangeValidity(this);
45
+ },
40
46
  /** @this {HTMLElement} */
41
47
  getQuery () {
42
48
  const {gte: gteStr, lte: lteStr} = readRangeInputsPair(this);
@@ -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
@@ -32,7 +33,7 @@ const blobSearchType = {
32
33
  }
33
34
  }, [
34
35
  ['span', {class: 'searchLabel'}, [`${label} (MIME type)`]],
35
- buildLiteralRegexControls({name})
36
+ buildLiteralRegexControls({name, flagOptions: regexpType.allowedFlags})
36
37
  ]];
37
38
  },
38
39
  getQuery: getQueryViaElement
@@ -30,7 +30,7 @@ const booleanSearchType = {
30
30
  ['span', {class: 'searchLabel'}, [label]],
31
31
  ['label', [
32
32
  `${label}: `,
33
- buildTriStateSelect({name, trueLabel: 'True', falseLabel: 'False'})
33
+ buildTriStateSelect({name, trueLabel: 'True', falseLabel: 'False', required: true})
34
34
  ]]
35
35
  ]];
36
36
  },
@@ -1,6 +1,8 @@
1
1
  import {buildDateInputControl} from '../../fundamentalTypes/dateType.js';
2
- import {buildPathLabel} from '../searchUtils.js';
3
- import {makeRangeLeaf} from '../queryTreeBuilders.js';
2
+ import {
3
+ buildPathLabel, buildTriStateSelect, readTriStateSelect, isExemptedByAncestorHasProperty
4
+ } from '../searchUtils.js';
5
+ import {makeRangeLeaf, makeValidDateCheckLeaf, combineAnd} from '../queryTreeBuilders.js';
4
6
  import {getQueryViaElement} from '../searchElementUtils.js';
5
7
 
6
8
  /**
@@ -13,7 +15,7 @@ import {getQueryViaElement} from '../searchElementUtils.js';
13
15
  * methods (`$define`, search plan §8) are installed once on the shared
14
16
  * prototype the first time the tag is defined - every instance must read
15
17
  * its own state off `this`, never off a build-time closure variable.
16
- * @param {HTMLElement} el
18
+ * @param {Element} el
17
19
  * @returns {{gte: string, lte: string}}
18
20
  */
19
21
  function readInputs (el) {
@@ -24,9 +26,69 @@ function readInputs (el) {
24
26
  }
25
27
 
26
28
  /**
27
- * OR date range/Is Not Range (README). `dateType.js`'s own
28
- * `buildDateInputControl` is reused twice (range start/end) so the min/max
29
- * wiring and ISO-slicing stay in one place.
29
+ * Cross-validates the pair the same way `searchUtils.js`'s
30
+ * `buildRangeInputsPair` does for its own (numeric) range pairs: leaving
31
+ * both ends blank is invalid (an added range row needs *some* bound, same
32
+ * "no absent values" reasoning as `buildLiteralRegexControls`'s Value
33
+ * input) - but only while the "Valid: (any)" tri-state is still unset.
34
+ * Once "Valid date" or "Invalid date" is explicitly chosen, that alone is
35
+ * already a complete constraint, so the range goes back to being fully
36
+ * optional (an empty range combined with "Valid date" still means
37
+ * something: "matches any valid date"); "Invalid date" additionally
38
+ * disables the whole fieldset (an "Invalid Date" has no orderable time
39
+ * value to compare), which independently exempts it from constraint
40
+ * validation regardless. If both ends are filled with "To" before "From",
41
+ * that's invalid either way. `datetime-local` values (`YYYY-MM-DDTHH:mm`)
42
+ * compare correctly as plain strings, so no `Date` parsing is needed here.
43
+ * Also called (with `this` as the `<jsoe-search-date>` root itself, which
44
+ * `this.closest('jsoe-search-date')` still resolves to - `closest` checks
45
+ * the element itself first) from the widget's own `connectedCallback` below
46
+ * and from the "Valid" tri-state's own `change` handler (the "at least one
47
+ * bound" requirement itself flips based on that selection, not just the
48
+ * range values) - the same reason `searchUtils.js`'s `syncRangeValidity`
49
+ * gives for `connectedCallback`: the `input`/`change` events wired below
50
+ * only fire from the user's first interaction, so a freshly-built pair
51
+ * would otherwise stay "valid" until then. Also relaxed, independently of
52
+ * its own local "Valid" tri-state, while `isExemptedByAncestorHasProperty`
53
+ * says an enclosing `objectSearchType.js` has-property row's own "Has"/
54
+ * "Doesn't have" already provides a complete constraint (e.g. this is a
55
+ * `date`-typed property's own widget).
56
+ * @this {Element}
57
+ * @returns {void}
58
+ */
59
+ function validateRange () {
60
+ const root = this.closest('jsoe-search-date');
61
+ if (!root) {
62
+ return;
63
+ }
64
+ const {gte, lte} = readInputs(root);
65
+ const [gteInput, lteInput] = /** @type {HTMLInputElement[]} */ (
66
+ [...root.querySelectorAll('input[type="datetime-local"]')]
67
+ );
68
+ const requiresBound = readTriStateSelect(root, 'valid') === undefined &&
69
+ !isExemptedByAncestorHasProperty(root);
70
+ const bothEmpty = requiresBound && gte === '' && lte === '';
71
+ const outOfOrder = gte !== '' && lte !== '' && lte < gte;
72
+ const emptyMessage = 'Enter at least one bound (From or To).';
73
+ gteInput.setCustomValidity(bothEmpty ? emptyMessage : '');
74
+ let lteMessage = '';
75
+ if (bothEmpty) {
76
+ lteMessage = emptyMessage;
77
+ } else if (outOfOrder) {
78
+ lteMessage = 'End of range must not be before the start of the range.';
79
+ }
80
+ lteInput.setCustomValidity(lteMessage);
81
+ }
82
+
83
+ /**
84
+ * OR date range/Is Not Range, Is/Is not a valid date (README) -
85
+ * `dateType.js`'s own `buildDateInputControl` is reused twice (range start/
86
+ * end) so the min/max wiring and ISO-slicing stay in one place. Choosing
87
+ * "Invalid date" disables the range fieldset (an "Invalid Date" has no
88
+ * orderable time value to compare - same as `dateType.js`'s own
89
+ * `notANum`/`ValidDate`/`InvalidDate` handling treating it as a distinct
90
+ * state from an ordinary `Date`), producing a `validDateCheck` leaf
91
+ * (`queryTreeBuilders.js`) instead of/alongside the range leaf.
30
92
  * @type {SearchTypeObject}
31
93
  */
32
94
  const dateSearchType = {
@@ -35,35 +97,66 @@ const dateSearchType = {
35
97
  const dateSchemaObject = /** @type {import('zodexy').SzDate} */ (
36
98
  schemaObject
37
99
  );
100
+ const name = `${typeNamespace}-date`;
38
101
  return ['jsoe-search-date', {
39
102
  dataset: {searchPath: path, searchKind: 'date'},
40
103
  title: label,
41
104
  $define: {
105
+ /** @this {HTMLElement} */
106
+ connectedCallback () {
107
+ validateRange.call(this);
108
+ },
42
109
  /** @this {HTMLElement} */
43
110
  getQuery () {
111
+ const searchPath = this.dataset.searchPath ?? '';
112
+ const validCheck = readTriStateSelect(this, 'valid');
113
+ const validLeaf = validCheck === undefined
114
+ ? undefined
115
+ : makeValidDateCheckLeaf(searchPath, validCheck);
44
116
  const {gte, lte} = readInputs(this);
45
- if (!gte && !lte) {
46
- return undefined;
47
- }
48
- return makeRangeLeaf(
49
- this.dataset.searchPath ?? '',
50
- 'date',
51
- {
117
+ const rangeLeaf = validCheck === false || (!gte && !lte)
118
+ ? undefined
119
+ : makeRangeLeaf(searchPath, 'date', {
52
120
  ...(gte ? {$gte: new Date(gte).toISOString()} : {}),
53
121
  ...(lte ? {$lte: new Date(lte).toISOString()} : {})
54
- }
55
- );
122
+ });
123
+ return combineAnd([validLeaf, rangeLeaf]);
56
124
  }
57
125
  }
58
126
  }, [
59
127
  ['span', {class: 'searchLabel'}, [label]],
60
128
  ['label', [
61
- 'From: ',
62
- buildDateInputControl({name: `${typeNamespace}-date-gte`, dateSchemaObject})
129
+ 'Valid: ',
130
+ buildTriStateSelect({
131
+ name: `${name}-valid`, key: 'valid', trueLabel: 'Valid date',
132
+ falseLabel: 'Invalid date',
133
+ /** @this {HTMLElement} */
134
+ onChange () {
135
+ const root = this.closest('jsoe-search-date');
136
+ const fieldset = root?.querySelector('fieldset.searchDateRangeFieldset');
137
+ if (fieldset) {
138
+ /** @type {HTMLFieldSetElement} */ (fieldset).disabled =
139
+ /** @type {HTMLSelectElement} */ (this).value === 'false';
140
+ }
141
+ if (root) {
142
+ validateRange.call(root);
143
+ }
144
+ }
145
+ })
63
146
  ]],
64
- ['label', [
65
- 'To: ',
66
- buildDateInputControl({name: `${typeNamespace}-date-lte`, dateSchemaObject})
147
+ ['fieldset', {class: 'searchDateRangeFieldset'}, [
148
+ ['label', [
149
+ 'From: ',
150
+ buildDateInputControl({
151
+ name: `${name}-gte`, dateSchemaObject, onValidate: validateRange
152
+ })
153
+ ]],
154
+ ['label', [
155
+ 'To: ',
156
+ buildDateInputControl({
157
+ name: `${name}-lte`, dateSchemaObject, onValidate: validateRange
158
+ })
159
+ ]]
67
160
  ]]
68
161
  ]];
69
162
  },
@@ -1,19 +1,37 @@
1
1
  import {domExceptionNames} from '../../fundamentalTypes/domexceptionType.js';
2
- import {buildPathLabel, buildMultiSelect, readMultiSelect, buildLiteralRegexControls, readLiteralRegexQuery} from '../searchUtils.js';
2
+ import {
3
+ buildPathLabel, buildMultiSelect, readMultiSelect, buildLiteralRegexControls,
4
+ readLiteralRegexQuery, buildOptInFieldset, readOptInChecked, wireOptInFieldset,
5
+ buildAtLeastOneSentinel, syncAtLeastOneCheck
6
+ } from '../searchUtils.js';
3
7
  import {makeMultiSelectLeaf, combineAnd} from '../queryTreeBuilders.js';
4
8
  import {getQueryViaElement} from '../searchElementUtils.js';
9
+ import regexpType from '../../fundamentalTypes/regexpType.js';
5
10
 
6
11
  /**
7
12
  * @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
8
13
  */
9
14
 
15
+ /**
16
+ * @param {Element} root - a `jsoe-search-domexception` element
17
+ * @returns {void}
18
+ */
19
+ function syncDomexceptionValidity (root) {
20
+ syncAtLeastOneCheck(
21
+ root, () => readMultiSelect(root).length > 0 || readOptInChecked(root, 'message')
22
+ );
23
+ }
24
+
10
25
  /**
11
26
  * DOMException: Literal/Regex search of child string properties; also
12
27
  * pull-down of name for DOMException (README) - `.name` gets the
13
28
  * multi-select pull-down (its standard predefined values, imported as-is
14
29
  * from `domexceptionType.js`'s own `domExceptionNames` export rather than a
15
- * second copy of the list), `.message` gets the usual literal/regex/
16
- * does-not-contain control, combined via `$and`.
30
+ * second copy of the list, and already properly optional on its own - an
31
+ * empty selection needs no opt-in gate), `.message` gets the usual literal/
32
+ * regex/does-not-contain control (wrapped in `buildOptInFieldset`, since
33
+ * that one *does* carry a `required` Value input), combined via `$and`.
34
+ * `buildAtLeastOneSentinel` requires at least one of the two.
17
35
  * @type {SearchTypeObject}
18
36
  */
19
37
  const domexceptionSearchType = {
@@ -24,6 +42,14 @@ const domexceptionSearchType = {
24
42
  dataset: {searchPath: path, searchKind: 'domexception'},
25
43
  title: label,
26
44
  $define: {
45
+ /** @this {HTMLElement} */
46
+ connectedCallback () {
47
+ this.querySelector('select.jsoeSearchMultiSelect')?.addEventListener(
48
+ 'change', () => syncDomexceptionValidity(this)
49
+ );
50
+ wireOptInFieldset(this, 'message', () => syncDomexceptionValidity(this));
51
+ syncDomexceptionValidity(this);
52
+ },
27
53
  /** @this {HTMLElement} */
28
54
  getQuery () {
29
55
  const searchPath = this.dataset.searchPath ?? '';
@@ -31,9 +57,9 @@ const domexceptionSearchType = {
31
57
  const nameLeaf = selectedNames.length
32
58
  ? makeMultiSelectLeaf(`${searchPath}/name`, {$in: selectedNames})
33
59
  : undefined;
34
- const messageLeaf = readLiteralRegexQuery(
35
- this, `${searchPath}/message`, 'message'
36
- );
60
+ const messageLeaf = readOptInChecked(this, 'message')
61
+ ? readLiteralRegexQuery(this, `${searchPath}/message`, 'message')
62
+ : undefined;
37
63
  return combineAnd([nameLeaf, messageLeaf]);
38
64
  }
39
65
  }
@@ -43,10 +69,13 @@ const domexceptionSearchType = {
43
69
  ['span', ['Name: ']],
44
70
  buildMultiSelect({name: `${name}-name`, options: domExceptionNames})
45
71
  ]],
46
- ['div', {class: 'domexceptionMessage'}, [
47
- ['span', ['Message: ']],
48
- buildLiteralRegexControls({name: `${name}-message`, key: 'message'})
49
- ]]
72
+ ...buildOptInFieldset({
73
+ name: `${name}-message`, key: 'message', label: 'Message',
74
+ children: [buildLiteralRegexControls({
75
+ name: `${name}-message`, key: 'message', flagOptions: regexpType.allowedFlags
76
+ })]
77
+ }),
78
+ buildAtLeastOneSentinel()
50
79
  ]];
51
80
  },
52
81
  getQuery: getQueryViaElement
@@ -19,6 +19,10 @@ import {getQueryViaElement} from '../searchElementUtils.js';
19
19
  * from its values (unlike a plain `z.enum([...])`, where they're the same)
20
20
  * still gets a usable widget here, just not that extra key-vs-value mode
21
21
  * yet; `values`'s keys are shown alongside their value as a hint.
22
+ * `buildMultiSelect`'s `required` leaves the whole selection invalid until
23
+ * at least one value is picked, natively (no build-time-empty gap like
24
+ * `buildRangeInputsPair`'s custom validity needed a `connectedCallback`
25
+ * for - `<select multiple required>` is evaluated by the browser itself).
22
26
  * @type {SearchTypeObject}
23
27
  */
24
28
  const enumSearchType = {
@@ -49,7 +53,7 @@ const enumSearchType = {
49
53
  }
50
54
  }, [
51
55
  ['span', {class: 'searchLabel'}, [label]],
52
- buildMultiSelect({name, options})
56
+ buildMultiSelect({name, options, required: true})
53
57
  ]];
54
58
  },
55
59
  getQuery: getQueryViaElement