@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,18 +1,142 @@
1
- import {buildPathLabel, findOwnControl} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, findOwnControl, isExemptedByAncestorHasProperty
3
+ } from '../searchUtils.js';
2
4
  import {makeBlobHTMLLeaf} from '../queryTreeBuilders.js';
3
5
  import {getQueryViaElement} from '../searchElementUtils.js';
6
+ import regexpType from '../../fundamentalTypes/regexpType.js';
4
7
 
5
8
  /**
6
9
  * @typedef {import('../searchDispatch.js').SearchTypeObject} SearchTypeObject
7
10
  */
8
11
 
12
+ /**
13
+ * Live syntax-checks a blobHTML Value against its currently-chosen Mode -
14
+ * a CSS selector and an XPath expression are each tried against a detached,
15
+ * inert target (a fragment for the selector, the live `document` itself,
16
+ * required by `Element.prototype.evaluate`'s own contract, for the XPath -
17
+ * neither actually has to *match* anything, only parse without throwing),
18
+ * and a regex is tried via the same `new RegExp(value, flags)` constructor
19
+ * `readLiteralRegexQuery` relies on elsewhere - a "Full text search" value
20
+ * is a plain phrase with no format to violate, so it always passes.
21
+ * @param {string} mode
22
+ * @param {string} value
23
+ * @param {string} flags
24
+ * @returns {string}
25
+ */
26
+ function computeBlobHTMLValueMessage (mode, value, flags) {
27
+ if (!value) {
28
+ return '';
29
+ }
30
+ try {
31
+ switch (mode) {
32
+ case 'cssSelector':
33
+ document.createDocumentFragment().querySelector(value);
34
+ break;
35
+ case 'xpath':
36
+ document.evaluate(value, document, null, XPathResult.ANY_TYPE, null);
37
+ break;
38
+ case 'rawHTMLRegex':
39
+ // eslint-disable-next-line no-new -- Testing
40
+ new RegExp(value, flags);
41
+ break;
42
+ default:
43
+ break;
44
+ }
45
+ return '';
46
+ } catch {
47
+ switch (mode) {
48
+ case 'cssSelector':
49
+ return 'Enter a valid CSS selector.';
50
+ case 'xpath':
51
+ return 'Enter a valid XPath expression.';
52
+ case 'rawHTMLRegex':
53
+ return 'Enter a valid regular expression.';
54
+ default:
55
+ return '';
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Re-runs `computeBlobHTMLValueMessage` against whichever of the input/
62
+ * textarea pair the current Mode makes active, clearing the other one's
63
+ * custom validity (a hidden-but-still-`required` control would otherwise
64
+ * keep blocking the form, the same reasoning `buildUI`'s own doc gives for
65
+ * toggling `required` alongside `hidden`) - call on every `input` on either
66
+ * value control, `change` on the Mode `<select>`, and `change` on the Flags
67
+ * multi-select (changing flags can itself flip a regex between valid and
68
+ * invalid, e.g. the `u`/`v` flags' stricter escape rules).
69
+ * @param {Element|null} container
70
+ * @returns {void}
71
+ */
72
+ function syncBlobHTMLValueValidity (container) {
73
+ if (!container) {
74
+ return;
75
+ }
76
+ const mode = /** @type {HTMLSelectElement|undefined} */ (
77
+ findOwnControl(container, 'select.jsoeSearchBlobHTMLMode')
78
+ )?.value ?? '';
79
+ const input = /** @type {HTMLInputElement|undefined} */ (
80
+ findOwnControl(container, 'input.jsoeSearchBlobHTMLValue')
81
+ );
82
+ const textarea = /** @type {HTMLTextAreaElement|undefined} */ (
83
+ findOwnControl(container, 'textarea.jsoeSearchBlobHTMLValue')
84
+ );
85
+ const flagsSelect = /** @type {HTMLSelectElement|undefined} */ (
86
+ findOwnControl(container, 'select.jsoeSearchBlobHTMLFlags')
87
+ );
88
+ const flags = mode === 'rawHTMLRegex'
89
+ ? [...(flagsSelect?.selectedOptions ?? [])].map((opt) => opt.value).join('')
90
+ : '';
91
+ const active = mode === 'fullText' ? textarea : input;
92
+ if (input && input !== active) {
93
+ input.setCustomValidity('');
94
+ }
95
+ if (textarea && textarea !== active) {
96
+ textarea.setCustomValidity('');
97
+ }
98
+ if (active) {
99
+ active.setCustomValidity(computeBlobHTMLValueMessage(mode, active.value, flags));
100
+ }
101
+ }
102
+
9
103
  /**
10
104
  * Blob HTML: XPath, CSS selectors, full text search, regex search of raw
11
- * HTML (README) - a mode `<select>` plus one text `<input>`, the same
105
+ * HTML (README) - a mode `<select>` plus one value control, the same
12
106
  * "pick a mode, give it one value" shape as `buildLiteralRegexControls`
13
107
  * (`searchUtils.js`), but not built through it: the four modes and the
14
108
  * `blobHTML` leaf kind they produce are specific to this one type, with no
15
- * second caller to share the helper with.
109
+ * second caller to share the helper with. The value control is a single-line
110
+ * `<input>` for XPath/CSS-selector/regex, but swaps to a `<textarea>` for
111
+ * "Full text search" mode alone, since a search phrase in a body of text is
112
+ * the one mode actually expected to run to more than one line - both share
113
+ * the same class, so the mode `<select>`'s `change` handler (and `getQuery`)
114
+ * only need the tag name to tell them apart. Only the currently-visible one
115
+ * of the pair is ever `required` (toggled alongside `hidden` by the mode
116
+ * `change` handler): a `required` field that's merely hidden, rather than
117
+ * also un-required, still blocks the form's validity even though the user
118
+ * has no way to see or fill it. "CSS selector" is listed ahead of "XPath"
119
+ * (and is the default mode) as the option most jsoe users will recognize.
120
+ *
121
+ * The `change` handler also checks `isExemptedByAncestorHasProperty` before
122
+ * (re-)asserting `required` on the now-visible control: nested as an
123
+ * optional object property's own child widget, an enclosing "Has property"
124
+ * already provides a complete constraint on its own (the same relief
125
+ * `setDescendantsRequired` gives other `required`-attribute controls), but
126
+ * that relief is a one-time toggle applied *before* `revalidateDescendants`
127
+ * re-dispatches a synthetic `change` on this very `<select>` - without this
128
+ * check, that synthetic event would immediately reassert `required` on
129
+ * whichever control the current mode makes visible, undoing the exemption
130
+ * `setDescendantsRequired` had just granted.
131
+ *
132
+ * "Regex search of raw HTML" gets its own Flags multi-select (the same
133
+ * `regexpType.js` `allowedFlags` list `stringSearchType.js`'s literal/regex
134
+ * control passes as `buildLiteralRegexControls`'s `flagOptions`), shown only
135
+ * while that mode is chosen, folded into the `blobHTML` leaf's own
136
+ * `$options` (mirroring `QueryRegexLeaf`'s field of the same name) - and,
137
+ * via `syncBlobHTMLValueValidity`, included when syntax-checking the Value
138
+ * itself, since the chosen flags can affect whether a given pattern is
139
+ * actually valid.
16
140
  * @type {SearchTypeObject}
17
141
  */
18
142
  const blobHTMLSearchType = {
@@ -29,16 +153,25 @@ const blobHTMLSearchType = {
29
153
  const mode = /** @type {HTMLSelectElement|undefined} */ (
30
154
  findOwnControl(this, 'select.jsoeSearchBlobHTMLMode')
31
155
  )?.value;
32
- const value = /** @type {HTMLInputElement|undefined} */ (
33
- findOwnControl(this, 'input.jsoeSearchBlobHTMLValue')
156
+ const value = /** @type {HTMLInputElement|HTMLTextAreaElement|undefined} */ (
157
+ findOwnControl(
158
+ this,
159
+ `${mode === 'fullText' ? 'textarea' : 'input'}.jsoeSearchBlobHTMLValue`
160
+ )
34
161
  )?.value;
35
162
  if (!value || !mode) {
36
163
  return undefined;
37
164
  }
165
+ const flags = mode === 'rawHTMLRegex'
166
+ ? [...(/** @type {HTMLSelectElement|undefined} */ (
167
+ findOwnControl(this, 'select.jsoeSearchBlobHTMLFlags')
168
+ )?.selectedOptions ?? [])].map((opt) => opt.value).join('')
169
+ : '';
38
170
  return makeBlobHTMLLeaf(
39
171
  searchPath,
40
172
  /** @type {import('../queryTree.js').QueryBlobHTMLLeaf['mode']} */ (mode),
41
- value
173
+ value,
174
+ flags || undefined
42
175
  );
43
176
  }
44
177
  }
@@ -46,16 +179,76 @@ const blobHTMLSearchType = {
46
179
  ['span', {class: 'searchLabel'}, [label]],
47
180
  ['label', [
48
181
  'Mode: ',
49
- ['select', {name: `${name}-mode`, class: 'jsoeSearchBlobHTMLMode'}, [
50
- ['option', {value: 'xpath'}, ['XPath']],
182
+ ['select', {
183
+ name: `${name}-mode`,
184
+ class: 'jsoeSearchBlobHTMLMode',
185
+ $on: {
186
+ change () {
187
+ const container = this.closest('jsoe-search-blob-html');
188
+ const input = /** @type {HTMLInputElement|null|undefined} */ (
189
+ container?.querySelector('input.jsoeSearchBlobHTMLValue')
190
+ );
191
+ const textarea = /** @type {HTMLTextAreaElement|null|undefined} */ (
192
+ container?.querySelector('textarea.jsoeSearchBlobHTMLValue')
193
+ );
194
+ const flagsLabel = /** @type {HTMLElement|null|undefined} */ (
195
+ container?.querySelector('.jsoeSearchBlobHTMLFlagsLabel')
196
+ );
197
+ const mode = /** @type {HTMLSelectElement} */ (this).value;
198
+ const isFullText = mode === 'fullText';
199
+ const exempted = container !== null &&
200
+ isExemptedByAncestorHasProperty(container);
201
+ if (input) {
202
+ input.hidden = isFullText;
203
+ input.required = !isFullText && !exempted;
204
+ }
205
+ if (textarea) {
206
+ textarea.hidden = !isFullText;
207
+ textarea.required = isFullText && !exempted;
208
+ }
209
+ if (flagsLabel) {
210
+ flagsLabel.hidden = mode !== 'rawHTMLRegex';
211
+ }
212
+ syncBlobHTMLValueValidity(container);
213
+ }
214
+ }
215
+ }, [
51
216
  ['option', {value: 'cssSelector'}, ['CSS selector']],
217
+ ['option', {value: 'xpath'}, ['XPath']],
52
218
  ['option', {value: 'fullText'}, ['Full text search']],
53
219
  ['option', {value: 'rawHTMLRegex'}, ['Regex search of raw HTML']]
54
220
  ]]
55
221
  ]],
56
222
  ['label', [
57
223
  'Value: ',
58
- ['input', {type: 'text', name: `${name}-value`, class: 'jsoeSearchBlobHTMLValue'}]
224
+ ['input', {
225
+ type: 'text', name: `${name}-value`, class: 'jsoeSearchBlobHTMLValue',
226
+ required: true,
227
+ $on: {
228
+ input () {
229
+ syncBlobHTMLValueValidity(this.closest('jsoe-search-blob-html'));
230
+ }
231
+ }
232
+ }],
233
+ ['textarea', {
234
+ name: `${name}-value`, class: 'jsoeSearchBlobHTMLValue', hidden: true,
235
+ $on: {
236
+ input () {
237
+ syncBlobHTMLValueValidity(this.closest('jsoe-search-blob-html'));
238
+ }
239
+ }
240
+ }]
241
+ ]],
242
+ ['label', {class: 'jsoeSearchBlobHTMLFlagsLabel', hidden: true}, [
243
+ 'Flags: ',
244
+ ['select', {
245
+ name: `${name}-flags`, multiple: true, class: 'jsoeSearchBlobHTMLFlags',
246
+ $on: {
247
+ change () {
248
+ syncBlobHTMLValueValidity(this.closest('jsoe-search-blob-html'));
249
+ }
250
+ }
251
+ }, regexpType.allowedFlags.map((flag) => ['option', {value: flag}, [flag]])]
59
252
  ]]
60
253
  ]];
61
254
  },
@@ -1,4 +1,7 @@
1
- import {buildPathLabel, buildCheckbox, readCheckbox} from '../searchUtils.js';
1
+ import {
2
+ buildPathLabel, buildCheckbox, readCheckbox, buildOptInFieldset,
3
+ readOptInChecked, wireOptInFieldset, buildAtLeastOneSentinel, syncAtLeastOneCheck
4
+ } from '../searchUtils.js';
2
5
  import {makeMapRecordJointLeaf} 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-record` element
15
+ * @returns {void}
16
+ */
17
+ function syncKeyValueValidity (root) {
18
+ syncAtLeastOneCheck(
19
+ root, () => readOptInChecked(root, 'key') || readOptInChecked(root, 'value')
20
+ );
21
+ }
22
+
10
23
  /**
11
24
  * Record search is "key-schema search AND/OR value-schema search,"
12
25
  * structurally unlike object's additive "has property" (search plan §1) -
@@ -15,7 +28,10 @@ import {buildSearchWidget} from '../searchDispatch.js';
15
28
  * whether a match must come from the *same* entry ("has key 2-4 and value
16
29
  * 7-9", README) via the "joint" toggle on the resulting `mapRecordJoint`
17
30
  * leaf - also used for `looseRecord`, aliased to this same module in
18
- * `searchDispatch.js` since the search semantics are identical.
31
+ * `searchDispatch.js` since the search semantics are identical. Key and
32
+ * value each get their own `buildOptInFieldset` (see `mapSearchType.js`'s
33
+ * matching doc for why), but leaving *both* unopted-in is itself invalid via
34
+ * `buildAtLeastOneSentinel`.
19
35
  * @type {SearchTypeObject}
20
36
  */
21
37
  const recordSearchType = {
@@ -47,15 +63,21 @@ const recordSearchType = {
47
63
  dataset: {searchPath: path, searchKind: 'record'},
48
64
  title: label,
49
65
  $define: {
66
+ /** @this {HTMLElement} */
67
+ connectedCallback () {
68
+ wireOptInFieldset(this, 'key', () => syncKeyValueValidity(this));
69
+ wireOptInFieldset(this, 'value', () => syncKeyValueValidity(this));
70
+ syncKeyValueValidity(this);
71
+ },
50
72
  /** @this {HTMLElement} */
51
73
  getQuery () {
52
74
  const searchPath = this.dataset.searchPath ?? '';
53
75
  const keyEl = findSearchElement(this, `${searchPath}/*key`);
54
76
  const valueEl = findSearchElement(this, `${searchPath}/*value`);
55
- const keyQuery = keyEl && hasGetQuery(keyEl)
77
+ const keyQuery = keyEl && hasGetQuery(keyEl) && readOptInChecked(this, 'key')
56
78
  ? keyEl.getQuery()
57
79
  : undefined;
58
- const valueQuery = valueEl && hasGetQuery(valueEl)
80
+ const valueQuery = valueEl && hasGetQuery(valueEl) && readOptInChecked(this, 'value')
59
81
  ? valueEl.getQuery()
60
82
  : undefined;
61
83
  if (!keyQuery && !valueQuery) {
@@ -67,15 +89,14 @@ const recordSearchType = {
67
89
  }
68
90
  }, [
69
91
  ['span', {class: 'searchLabel'}, [label]],
70
- ['div', {class: 'searchRecordKey'}, [
71
- ['span', ['Key matches: ']],
72
- keyArr
73
- ]],
74
- ['div', {class: 'searchRecordValue'}, [
75
- ['span', ['Value matches: ']],
76
- valueArr
77
- ]],
78
- buildCheckbox({name: `${name}-joint`, label: 'Require same entry'})
92
+ ...buildOptInFieldset({
93
+ name: `${name}-key`, key: 'key', label: 'Key matches', children: [keyArr]
94
+ }),
95
+ ...buildOptInFieldset({
96
+ name: `${name}-value`, key: 'value', label: 'Value matches', children: [valueArr]
97
+ }),
98
+ buildCheckbox({name: `${name}-joint`, label: 'Require same entry'}),
99
+ buildAtLeastOneSentinel()
79
100
  ]];
80
101
  },
81
102
  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';
@@ -7,6 +11,21 @@ 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-tuple` element
16
+ * @returns {void}
17
+ */
18
+ function syncTupleValidity (root) {
19
+ const itemCount = Number(/** @type {HTMLElement} */ (root).dataset.itemCount ?? '0');
20
+ const hasRest = /** @type {HTMLElement} */ (root).dataset.hasRest === 'true';
21
+ syncAtLeastOneCheck(root, () => (
22
+ Array.from({length: itemCount}, (_, idx) => String(idx)).some(
23
+ (key) => readOptInChecked(root, key)
24
+ ) ||
25
+ (hasRest && (readOptInChecked(root, 'rest') || readLengthSizeQuery(root, '') !== undefined))
26
+ ));
27
+ }
28
+
10
29
  /**
11
30
  * Each fixed position gets its own control from `.items[i]`, not one
12
31
  * control repeated per element (search plan §1 - unlike the value-editing
@@ -19,6 +38,9 @@ import {buildSearchWidget} from '../searchDispatch.js';
19
38
  * items.length`, no sparse toggle - not applicable to a tuple), and the
20
39
  * `rest` element itself recurses into its own search widget under the same
21
40
  * `*` path-segment convention `arraySearchType.js` uses for its element.
41
+ * Leaving every position, the rest element, and (when present) the length
42
+ * unconfigured at once is invalid (`buildAtLeastOneSentinel`), the same as
43
+ * `arraySearchType.js`/`setSearchType.js`/`filelistSearchType.js`.
22
44
  * @type {SearchTypeObject}
23
45
  */
24
46
  const tupleSearchType = {
@@ -54,22 +76,24 @@ const tupleSearchType = {
54
76
  /** @type {import('../../types.js').JamilihArray[]} */
55
77
  const children = [['span', {class: 'searchLabel'}, [label]]];
56
78
  itemArrs.forEach((itemArr, idx) => {
57
- children.push(['div', {class: 'searchTupleItem'}, [
58
- ['span', [`Position ${idx}: `]],
59
- itemArr
60
- ]]);
79
+ children.push(...buildOptInFieldset({
80
+ name: `${name}-${idx}`, key: String(idx), label: `Position ${idx} matches`,
81
+ children: [itemArr]
82
+ }));
61
83
  });
62
84
  if (rest) {
85
+ const restChild = /** @type {import('../../types.js').JamilihArray} */ (restArr);
63
86
  children.push(
64
87
  ...buildLengthSizeControls({
65
88
  name, min: items.length, max: undefined, includeSparse: false
66
89
  }),
67
- ['div', {class: 'searchTupleRest'}, [
68
- ['span', ['Rest element matches: ']],
69
- /** @type {import('../../types.js').JamilihArray} */ (restArr)
70
- ]]
90
+ ...buildOptInFieldset({
91
+ name: `${name}-rest`, key: 'rest', label: 'Rest element matches',
92
+ children: [restChild]
93
+ })
71
94
  );
72
95
  }
96
+ children.push(buildAtLeastOneSentinel());
73
97
 
74
98
  return ['jsoe-search-tuple', {
75
99
  dataset: {
@@ -84,11 +108,28 @@ const tupleSearchType = {
84
108
  // defined, so a second tuple widget on the same page would
85
109
  // otherwise silently reuse the first tuple's item count/rest-ness.
86
110
  /** @this {HTMLElement} */
111
+ connectedCallback () {
112
+ const itemCount = Number(this.dataset.itemCount ?? '0');
113
+ Array.from({length: itemCount}, (_, idx) => String(idx)).forEach(
114
+ (key) => wireOptInFieldset(this, key, () => syncTupleValidity(this))
115
+ );
116
+ if (this.dataset.hasRest === 'true') {
117
+ wireOptInFieldset(this, 'rest', () => syncTupleValidity(this));
118
+ this.querySelector('input.jsoeSearchSize')?.addEventListener(
119
+ 'input', () => syncTupleValidity(this)
120
+ );
121
+ }
122
+ syncTupleValidity(this);
123
+ },
124
+ /** @this {HTMLElement} */
87
125
  getQuery () {
88
126
  const searchPath = this.dataset.searchPath ?? '';
89
127
  const itemCount = Number(this.dataset.itemCount ?? '0');
90
128
  const hasRest = this.dataset.hasRest === 'true';
91
129
  const itemLeaves = Array.from({length: itemCount}, (_, idx) => {
130
+ if (!readOptInChecked(this, String(idx))) {
131
+ return undefined;
132
+ }
92
133
  const itemEl = findSearchElement(this, `${searchPath}/${idx}`);
93
134
  return itemEl && hasGetQuery(itemEl) ? itemEl.getQuery() : undefined;
94
135
  });
@@ -97,7 +138,7 @@ const tupleSearchType = {
97
138
  }
98
139
  const lengthLeaf = readLengthSizeQuery(this, searchPath);
99
140
  const restEl = findSearchElement(this, `${searchPath}/*`);
100
- const restLeaf = restEl && hasGetQuery(restEl)
141
+ const restLeaf = restEl && hasGetQuery(restEl) && readOptInChecked(this, 'rest')
101
142
  ? restEl.getQuery()
102
143
  : undefined;
103
144
  return combineAnd([...itemLeaves, lengthLeaf, restLeaf]);
@@ -32,7 +32,7 @@ const SpecialRealNumberSearchType = {
32
32
  }, [
33
33
  ['span', {class: 'searchLabel'}, [label]],
34
34
  buildMultiSelect({
35
- name, options: ['Infinity', '-Infinity', '-0']
35
+ name, options: ['Infinity', '-Infinity', '-0'], required: true
36
36
  })
37
37
  ]];
38
38
  },
@@ -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
 
@@ -24,6 +26,10 @@ const buffersourceSearchType = {
24
26
  dataset: {searchPath: path, searchKind: 'buffersource'},
25
27
  title: label,
26
28
  $define: {
29
+ /** @this {HTMLElement} */
30
+ connectedCallback () {
31
+ syncRangeValidity(this);
32
+ },
27
33
  /** @this {HTMLElement} */
28
34
  getQuery () {
29
35
  const {gte, lte} = readRangeInputsPair(this);
@@ -110,6 +110,14 @@ export function makeUnionFamilySearchType ({tagName, discriminated}) {
110
110
  ['select', {
111
111
  name,
112
112
  class: 'jsoeSearchTypeOf',
113
+ // The "(any)" placeholder's `value: ''` below is exactly what
114
+ // native `required` treats as "nothing selected", so choosing
115
+ // any real branch already satisfies it - no custom validity
116
+ // code needed. A union/xor/discriminatedUnion has no other
117
+ // possible constraint (unlike `mapSearchType.js`'s key/value or
118
+ // `objectSearchType.js`'s properties), so this alone is "no
119
+ // absent values" for the whole widget, not just one facet of it.
120
+ required: true,
113
121
  $on: {
114
122
  change () {
115
123
  if (!isSelectElement(this)) {
@@ -788,7 +788,9 @@ export declare const describe: typeof core.describe;
788
788
  export declare const meta: typeof core.meta;
789
789
  type ZodInstanceOfParams = core.Params<ZodCustom, core.$ZodIssueCustom, "type" | "check" | "checks" | "fn" | "abort" | "error" | "params" | "path">;
790
790
  export interface ZodInstanceOf<T = unknown> extends ZodCustom<T, T> {
791
- properties<Shape extends core.$ZodShape>(shape: Shape, params?: string | core.$ZodCheckPropertiesParams): ZodInstanceOf<T & core.$InferObjectInput<Shape, {}>>;
791
+ properties<Shape extends {
792
+ [k in keyof T as T[k] extends Function ? never : k]?: core.$ZodType<unknown, T[k]>;
793
+ }>(shape: Shape, params?: string | core.$ZodCheckPropertiesParams): ZodInstanceOf<T & core.$InferObjectInput<Shape, {}>>;
792
794
  }
793
795
  export declare const ZodInstanceOf: core.$constructor<ZodInstanceOf>;
794
796
  declare function _instanceof<T extends typeof util.Class>(cls: T, params?: ZodInstanceOfParams): ZodInstanceOf<InstanceType<T>>;
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 6,
4
- patch: 4,
4
+ patch: 5,
5
5
  };