@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,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
@@ -1,20 +1,48 @@
1
- import {buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildLiteralRegexControls, readLiteralRegexQuery,
3
+ buildOptInFieldset, readOptInChecked, wireOptInFieldset,
4
+ buildAtLeastOneSentinel, syncAtLeastOneCheck
5
+ } from '../searchUtils.js';
6
+ import {combineAnd} from '../queryTreeBuilders.js';
2
7
  import {getQueryViaElement} from '../searchElementUtils.js';
8
+ import regexpType from '../../fundamentalTypes/regexpType.js';
3
9
 
4
10
  /**
5
11
  * @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
6
12
  */
7
13
 
14
+ /**
15
+ * @param {Element} root - a `jsoe-search-file` element
16
+ * @returns {void}
17
+ */
18
+ function syncFileValidity (root) {
19
+ syncAtLeastOneCheck(
20
+ root, () => readOptInChecked(root, 'name') || readOptInChecked(root, 'type')
21
+ );
22
+ }
23
+
8
24
  /**
9
25
  * File: OR literal or regex search/Does Not contain search (README, grouped
10
26
  * with string/StringObject/Blob/regexp-source/symbol-description) - against
11
- * the File's `.name`, a value-level facet the schema itself carries no
12
- * constraint for (`SzFile` only has `min`/`max`/`mime`), the same way
13
- * `symbolSearchType.js` targets `.description` rather than anything the
14
- * schema declares. `SzFile`'s own `min`/`max` (byte size) is left for a
15
- * later pass - the README's literal/regex bullet doesn't ask for a size
16
- * range here, and README explicitly reserves that shape of control
17
- * ("OR Range/Is Not Range") for `buffersource` instead.
27
+ * both the File's `.name` and its `.type` (content/MIME type, the same
28
+ * facet `blobSearchType.js` targets for `Blob`), each a value-level facet
29
+ * the schema itself carries no constraint for (`SzFile` only has
30
+ * `min`/`max`/`mime`, and `mime` is a fixed *allow-list* for validation, not
31
+ * a per-instance value to search against). `name`/`type` sub-paths
32
+ * distinguish the two facets (`${path}/name`, `${path}/type`, matching the
33
+ * real `File` property each reads back), the same convention
34
+ * `errorSearchType.js` uses for its own multiple flat facets, combined via
35
+ * `$and`. Each facet is independently opt-in (`buildOptInFieldset`, so
36
+ * checking against just the name, or just the content type, is as easy as
37
+ * checking against both), but leaving *both* unopted-in is itself invalid
38
+ * (`buildAtLeastOneSentinel`) - the same "key or value, not necessarily
39
+ * both" shape `mapSearchType.js`/`recordSearchType.js` use. `SzFile`'s own
40
+ * `min`/`max` (byte size) is left for a later pass - the README's literal/
41
+ * regex bullet doesn't ask for a size range here, and README explicitly
42
+ * reserves that shape of control ("OR Range/Is Not Range") for
43
+ * `buffersource` instead. `filelistSearchType.js` recurses its element
44
+ * schema through this same module, so a `FileList`'s per-file search gets
45
+ * both facets for free.
18
46
  * @type {SearchTypeObject}
19
47
  */
20
48
  const fileSearchType = {
@@ -25,14 +53,39 @@ const fileSearchType = {
25
53
  dataset: {searchPath: path, searchKind: 'file'},
26
54
  title: label,
27
55
  $define: {
56
+ /** @this {HTMLElement} */
57
+ connectedCallback () {
58
+ wireOptInFieldset(this, 'name', () => syncFileValidity(this));
59
+ wireOptInFieldset(this, 'type', () => syncFileValidity(this));
60
+ syncFileValidity(this);
61
+ },
28
62
  /** @this {HTMLElement} */
29
63
  getQuery () {
30
- return readLiteralRegexQuery(this, this.dataset.searchPath ?? '');
64
+ const searchPath = this.dataset.searchPath ?? '';
65
+ const nameLeaf = readOptInChecked(this, 'name')
66
+ ? readLiteralRegexQuery(this, `${searchPath}/name`, 'name')
67
+ : undefined;
68
+ const typeLeaf = readOptInChecked(this, 'type')
69
+ ? readLiteralRegexQuery(this, `${searchPath}/type`, 'type')
70
+ : undefined;
71
+ return combineAnd([nameLeaf, typeLeaf]);
31
72
  }
32
73
  }
33
74
  }, [
34
- ['span', {class: 'searchLabel'}, [`${label} (name)`]],
35
- buildLiteralRegexControls({name})
75
+ ['span', {class: 'searchLabel'}, [label]],
76
+ ...buildOptInFieldset({
77
+ name: `${name}-name`, key: 'name', label: 'Name',
78
+ children: [buildLiteralRegexControls({
79
+ name: `${name}-name`, key: 'name', flagOptions: regexpType.allowedFlags
80
+ })]
81
+ }),
82
+ ...buildOptInFieldset({
83
+ name: `${name}-type`, key: 'type', label: 'Content type',
84
+ children: [buildLiteralRegexControls({
85
+ name: `${name}-type`, key: 'type', flagOptions: regexpType.allowedFlags
86
+ })]
87
+ }),
88
+ buildAtLeastOneSentinel()
36
89
  ]];
37
90
  },
38
91
  getQuery: getQueryViaElement
@@ -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';
@@ -16,6 +20,16 @@ import {buildSearchWidget} from '../searchDispatch.js';
16
20
  * }} FilelistOutputSchema
17
21
  */
18
22
 
23
+ /**
24
+ * @param {Element} root - a `jsoe-search-filelist` element
25
+ * @returns {void}
26
+ */
27
+ function syncFilelistValidity (root) {
28
+ syncAtLeastOneCheck(
29
+ root, () => readLengthSizeQuery(root, '') !== undefined || readOptInChecked(root)
30
+ );
31
+ }
32
+
19
33
  /**
20
34
  * Has length/size of &lt;number&gt; (README, grouped with array/set/
21
35
  * tuple-with-rest) - no sparse toggle, matching `setSearchType.js`. A
@@ -24,7 +38,10 @@ import {buildSearchWidget} from '../searchDispatch.js';
24
38
  * (`src/formats/schema.js`'s `codec`/`filelist` handling); the length
25
39
  * bounds and element schema both live on that `output` array shape, whose
26
40
  * element recurses into `fileSearchType.js` under the same `*` path-segment
27
- * convention `arraySearchType.js` uses.
41
+ * convention `arraySearchType.js` uses. Leaving *both* length/size and the
42
+ * element match unconfigured is still invalid, though
43
+ * (`buildAtLeastOneSentinel`), same as `arraySearchType.js`/
44
+ * `setSearchType.js`.
28
45
  * @type {SearchTypeObject}
29
46
  */
30
47
  const filelistSearchType = {
@@ -47,12 +64,20 @@ const filelistSearchType = {
47
64
  dataset: {searchPath: path, searchKind: 'filelist'},
48
65
  title: label,
49
66
  $define: {
67
+ /** @this {HTMLElement} */
68
+ connectedCallback () {
69
+ wireOptInFieldset(this, '', () => syncFilelistValidity(this));
70
+ this.querySelector('input.jsoeSearchSize')?.addEventListener(
71
+ 'input', () => syncFilelistValidity(this)
72
+ );
73
+ syncFilelistValidity(this);
74
+ },
50
75
  /** @this {HTMLElement} */
51
76
  getQuery () {
52
77
  const searchPath = this.dataset.searchPath ?? '';
53
78
  const lengthLeaf = readLengthSizeQuery(this, searchPath);
54
79
  const elementEl = findSearchElement(this, `${searchPath}/*`);
55
- const elementLeaf = elementEl && hasGetQuery(elementEl)
80
+ const elementLeaf = elementEl && hasGetQuery(elementEl) && readOptInChecked(this)
56
81
  ? elementEl.getQuery()
57
82
  : undefined;
58
83
  return combineAnd([lengthLeaf, elementLeaf]);
@@ -66,10 +91,10 @@ const filelistSearchType = {
66
91
  max: outputSchema.maxLength,
67
92
  includeSparse: false
68
93
  }),
69
- ['div', {class: 'searchFilelistElement'}, [
70
- ['span', ['File matches: ']],
71
- elementArr
72
- ]]
94
+ ...buildOptInFieldset({
95
+ name: `${name}-element`, label: 'File matches', children: [elementArr]
96
+ }),
97
+ buildAtLeastOneSentinel()
73
98
  ]];
74
99
  },
75
100
  getQuery: getQueryViaElement
@@ -1,4 +1,7 @@
1
- import {buildPathLabel} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildOptInFieldset, readOptInChecked, wireOptInFieldset,
3
+ buildAtLeastOneSentinel, syncAtLeastOneCheck
4
+ } from '../searchUtils.js';
2
5
  import {combineAnd} from '../queryTreeBuilders.js';
3
6
  import {findSearchElement, getQueryViaElement, hasGetQuery} from '../searchElementUtils.js';
4
7
  import {buildSearchWidget} from '../searchDispatch.js';
@@ -7,6 +10,16 @@ import {buildSearchWidget} from '../searchDispatch.js';
7
10
  * @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
8
11
  */
9
12
 
13
+ /**
14
+ * @param {Element} root - a `jsoe-search-function` element
15
+ * @returns {void}
16
+ */
17
+ function syncFunctionValidity (root) {
18
+ syncAtLeastOneCheck(
19
+ root, () => readOptInChecked(root, 'args') || readOptInChecked(root, 'output')
20
+ );
21
+ }
22
+
10
23
  /**
11
24
  * Function; pass on args, return type (README) - recurses into both
12
25
  * `.input` (itself a tuple, so its own per-position controls come for free
@@ -15,12 +28,16 @@ import {buildSearchWidget} from '../searchDispatch.js';
15
28
  * note that `function` is "fuzzy" in the README and could ship as a
16
29
  * minimal stub predates `tupleSearchType.js` existing; now that it does,
17
30
  * recursing into `.input` through it is no more work than `.output` alone
18
- * would have been.
31
+ * would have been. Each is wrapped in its own `buildOptInFieldset` (an args
32
+ * tuple's own positions are themselves individually opt-in, so args as a
33
+ * whole needs the same treatment one level up), with
34
+ * `buildAtLeastOneSentinel` requiring at least one of the two.
19
35
  * @type {SearchTypeObject}
20
36
  */
21
37
  const functionSearchType = {
22
38
  buildUI ({schemaObject, path, typeNamespace, topRoot, types, originalJSON}) {
23
39
  const label = buildPathLabel(schemaObject, path);
40
+ const name = `${typeNamespace}-function`;
24
41
  const functionSchemaObject = /** @type {import('zodexy').SzFunction<any, any>} */ (
25
42
  schemaObject
26
43
  );
@@ -46,15 +63,22 @@ const functionSearchType = {
46
63
  dataset: {searchPath: path, searchKind: 'function'},
47
64
  title: label,
48
65
  $define: {
66
+ /** @this {HTMLElement} */
67
+ connectedCallback () {
68
+ wireOptInFieldset(this, 'args', () => syncFunctionValidity(this));
69
+ wireOptInFieldset(this, 'output', () => syncFunctionValidity(this));
70
+ syncFunctionValidity(this);
71
+ },
49
72
  /** @this {HTMLElement} */
50
73
  getQuery () {
51
74
  const searchPath = this.dataset.searchPath ?? '';
52
75
  const argsEl = findSearchElement(this, `${searchPath}/*args`);
53
76
  const outputEl = findSearchElement(this, `${searchPath}/*output`);
54
- const argsQuery = argsEl && hasGetQuery(argsEl)
77
+ const argsQuery = argsEl && hasGetQuery(argsEl) && readOptInChecked(this, 'args')
55
78
  ? argsEl.getQuery()
56
79
  : undefined;
57
- const outputQuery = outputEl && hasGetQuery(outputEl)
80
+ const outputQuery = outputEl && hasGetQuery(outputEl) &&
81
+ readOptInChecked(this, 'output')
58
82
  ? outputEl.getQuery()
59
83
  : undefined;
60
84
  return combineAnd([argsQuery, outputQuery]);
@@ -62,14 +86,14 @@ const functionSearchType = {
62
86
  }
63
87
  }, [
64
88
  ['span', {class: 'searchLabel'}, [label]],
65
- ['div', {class: 'searchFunctionArgs'}, [
66
- ['span', ['Args match: ']],
67
- argsArr
68
- ]],
69
- ['div', {class: 'searchFunctionOutput'}, [
70
- ['span', ['Return value matches: ']],
71
- outputArr
72
- ]]
89
+ ...buildOptInFieldset({
90
+ name: `${name}-args`, key: 'args', label: 'Args match', children: [argsArr]
91
+ }),
92
+ ...buildOptInFieldset({
93
+ name: `${name}-output`, key: 'output', label: 'Return value matches',
94
+ children: [outputArr]
95
+ }),
96
+ buildAtLeastOneSentinel()
73
97
  ]];
74
98
  },
75
99
  getQuery: getQueryViaElement
@@ -1,4 +1,8 @@
1
- import {buildPathLabel, buildCheckbox, readCheckbox, buildLengthSizeControls, readLengthSizeQuery} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildCheckbox, readCheckbox, buildLengthSizeControls,
3
+ readLengthSizeQuery, buildOptInFieldset, readOptInChecked, wireOptInFieldset,
4
+ buildAtLeastOneSentinel, syncAtLeastOneCheck
5
+ } from '../searchUtils.js';
2
6
  import {makeMapRecordJointLeaf, combineAnd} from '../queryTreeBuilders.js';
3
7
  import {findSearchElement, getQueryViaElement, hasGetQuery} from '../searchElementUtils.js';
4
8
  import {buildSearchWidget} from '../searchDispatch.js';
@@ -7,6 +11,17 @@ 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-map` element
16
+ * @returns {void}
17
+ */
18
+ function syncMapValidity (root) {
19
+ syncAtLeastOneCheck(
20
+ root, () => readOptInChecked(root, 'key') || readOptInChecked(root, 'value') ||
21
+ readLengthSizeQuery(root, '') !== undefined
22
+ );
23
+ }
24
+
10
25
  /**
11
26
  * Map, Record: string-type searches of keys, values; ideally would allow
12
27
  * search to insist on match of key and value (README) - structurally the
@@ -15,7 +30,14 @@ import {buildSearchWidget} from '../searchDispatch.js';
15
30
  * leaf), plus a `lengthSize` control for `min`/`max` (a `Map`, unlike a
16
31
  * `Record`, carries its own size bounds in the schema - the same
17
32
  * affordance `setSearchType.js` gets for the analogous reason), combined
18
- * with the joint leaf via `$and`.
33
+ * with the joint leaf via `$and`. Key and value are each wrapped in their
34
+ * own `buildOptInFieldset` (so leaving either at "no constraint" doesn't
35
+ * force it via a `required` control inside), but *leaving all three*
36
+ * (key, value, and size) unconfigured is itself invalid via
37
+ * `buildAtLeastOneSentinel` - unlike `recordSearchType.js` (which has no
38
+ * size control of its own and so needs key or value specifically), a
39
+ * length/size-only search is already meaningful here, the same as
40
+ * `arraySearchType.js`/`setSearchType.js`.
19
41
  * @type {SearchTypeObject}
20
42
  */
21
43
  const mapSearchType = {
@@ -47,16 +69,25 @@ const mapSearchType = {
47
69
  dataset: {searchPath: path, searchKind: 'map'},
48
70
  title: label,
49
71
  $define: {
72
+ /** @this {HTMLElement} */
73
+ connectedCallback () {
74
+ wireOptInFieldset(this, 'key', () => syncMapValidity(this));
75
+ wireOptInFieldset(this, 'value', () => syncMapValidity(this));
76
+ this.querySelector('input.jsoeSearchSize')?.addEventListener(
77
+ 'input', () => syncMapValidity(this)
78
+ );
79
+ syncMapValidity(this);
80
+ },
50
81
  /** @this {HTMLElement} */
51
82
  getQuery () {
52
83
  const searchPath = this.dataset.searchPath ?? '';
53
84
  const lengthLeaf = readLengthSizeQuery(this, searchPath);
54
85
  const keyEl = findSearchElement(this, `${searchPath}/*key`);
55
86
  const valueEl = findSearchElement(this, `${searchPath}/*value`);
56
- const keyQuery = keyEl && hasGetQuery(keyEl)
87
+ const keyQuery = keyEl && hasGetQuery(keyEl) && readOptInChecked(this, 'key')
57
88
  ? keyEl.getQuery()
58
89
  : undefined;
59
- const valueQuery = valueEl && hasGetQuery(valueEl)
90
+ const valueQuery = valueEl && hasGetQuery(valueEl) && readOptInChecked(this, 'value')
60
91
  ? valueEl.getQuery()
61
92
  : undefined;
62
93
  const jointLeaf = keyQuery || valueQuery
@@ -70,15 +101,14 @@ const mapSearchType = {
70
101
  ...buildLengthSizeControls({
71
102
  name, min: mapSchemaObject.min, max: mapSchemaObject.max, includeSparse: false
72
103
  }),
73
- ['div', {class: 'searchMapKey'}, [
74
- ['span', ['Key matches: ']],
75
- keyArr
76
- ]],
77
- ['div', {class: 'searchMapValue'}, [
78
- ['span', ['Value matches: ']],
79
- valueArr
80
- ]],
81
- buildCheckbox({name: `${name}-joint`, label: 'Require same entry'})
104
+ ...buildOptInFieldset({
105
+ name: `${name}-key`, key: 'key', label: 'Key matches', children: [keyArr]
106
+ }),
107
+ ...buildOptInFieldset({
108
+ name: `${name}-value`, key: 'value', label: 'Value matches', children: [valueArr]
109
+ }),
110
+ buildCheckbox({name: `${name}-joint`, label: 'Require same entry'}),
111
+ buildAtLeastOneSentinel()
82
112
  ]];
83
113
  },
84
114
  getQuery: getQueryViaElement
@@ -1,4 +1,7 @@
1
- import {buildPathLabel, buildRangeInputsPair, readRangeInputsPair, buildTriStateSelect, readTriStateSelect} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildRangeInputsPair, readRangeInputsPair, buildTriStateSelect,
3
+ readTriStateSelect, syncRangeValidity
4
+ } from '../searchUtils.js';
2
5
  import {makeRangeLeaf, makeIntegerCheckLeaf, combineAnd} from '../queryTreeBuilders.js';
3
6
  import {getQueryViaElement} from '../searchElementUtils.js';
4
7
 
@@ -18,6 +21,10 @@ const numberSearchType = {
18
21
  dataset: {searchPath: path, searchKind: 'number'},
19
22
  title: label,
20
23
  $define: {
24
+ /** @this {HTMLElement} */
25
+ connectedCallback () {
26
+ syncRangeValidity(this);
27
+ },
21
28
  /** @this {HTMLElement} */
22
29
  getQuery () {
23
30
  const searchPath = this.dataset.searchPath ?? '';