@es-joy/jsoe 0.30.0 → 0.32.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 (82) hide show
  1. package/CHANGES.md +9 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/search/fundamentalTypes/arraySearchType.d.ts.map +1 -1
  6. package/dist/search/fundamentalTypes/bigintSearchType.d.ts.map +1 -1
  7. package/dist/search/fundamentalTypes/blobSearchType.d.ts.map +1 -1
  8. package/dist/search/fundamentalTypes/booleanSearchType.d.ts.map +1 -1
  9. package/dist/search/fundamentalTypes/catchSearchType.d.ts.map +1 -1
  10. package/dist/search/fundamentalTypes/dateSearchType.d.ts.map +1 -1
  11. package/dist/search/fundamentalTypes/domexceptionSearchType.d.ts.map +1 -1
  12. package/dist/search/fundamentalTypes/enumSearchType.d.ts.map +1 -1
  13. package/dist/search/fundamentalTypes/fileSearchType.d.ts.map +1 -1
  14. package/dist/search/fundamentalTypes/filelistSearchType.d.ts.map +1 -1
  15. package/dist/search/fundamentalTypes/functionSearchType.d.ts.map +1 -1
  16. package/dist/search/fundamentalTypes/mapSearchType.d.ts.map +1 -1
  17. package/dist/search/fundamentalTypes/numberSearchType.d.ts.map +1 -1
  18. package/dist/search/fundamentalTypes/objectSearchType.d.ts.map +1 -1
  19. package/dist/search/fundamentalTypes/promiseSearchType.d.ts.map +1 -1
  20. package/dist/search/fundamentalTypes/regexpSearchType.d.ts.map +1 -1
  21. package/dist/search/fundamentalTypes/setSearchType.d.ts.map +1 -1
  22. package/dist/search/fundamentalTypes/stringSearchType.d.ts.map +1 -1
  23. package/dist/search/fundamentalTypes/symbolSearchType.d.ts.map +1 -1
  24. package/dist/search/index.d.ts +2 -0
  25. package/dist/search/index.d.ts.map +1 -1
  26. package/dist/search/queryTree.d.ts +3 -0
  27. package/dist/search/queryTree.d.ts.map +1 -1
  28. package/dist/search/queryTreeBuilders.d.ts +2 -1
  29. package/dist/search/queryTreeBuilders.d.ts.map +1 -1
  30. package/dist/search/searchDispatch.d.ts +11 -3
  31. package/dist/search/searchDispatch.d.ts.map +1 -1
  32. package/dist/search/searchElementUtils.d.ts +31 -1
  33. package/dist/search/searchElementUtils.d.ts.map +1 -1
  34. package/dist/search/searchUtils.d.ts +197 -10
  35. package/dist/search/searchUtils.d.ts.map +1 -1
  36. package/dist/search/subTypes/blobHTMLSearchType.d.ts +11 -4
  37. package/dist/search/subTypes/blobHTMLSearchType.d.ts.map +1 -1
  38. package/dist/search/subTypes/recordSearchType.d.ts.map +1 -1
  39. package/dist/search/subTypes/tupleSearchType.d.ts.map +1 -1
  40. package/dist/search/superTypes/SpecialRealNumberSearchType.d.ts.map +1 -1
  41. package/dist/search/superTypes/buffersourceSearchType.d.ts.map +1 -1
  42. package/dist/search/unions/unionFamilySearchType.d.ts.map +1 -1
  43. package/dist/utils/json6Stringify.d.ts +2 -0
  44. package/dist/utils/json6Stringify.d.ts.map +1 -0
  45. package/dist/utils/rawTypesonEditor.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/pnpm-workspace.yaml +26 -26
  48. package/src/search/fundamentalTypes/arraySearchType.js +28 -5
  49. package/src/search/fundamentalTypes/bigintSearchType.js +14 -3
  50. package/src/search/fundamentalTypes/blobSearchType.js +21 -3
  51. package/src/search/fundamentalTypes/booleanSearchType.js +15 -3
  52. package/src/search/fundamentalTypes/catchSearchType.js +20 -3
  53. package/src/search/fundamentalTypes/dateSearchType.js +42 -3
  54. package/src/search/fundamentalTypes/domexceptionSearchType.js +18 -4
  55. package/src/search/fundamentalTypes/enumSearchType.js +15 -3
  56. package/src/search/fundamentalTypes/fileSearchType.js +15 -3
  57. package/src/search/fundamentalTypes/filelistSearchType.js +25 -5
  58. package/src/search/fundamentalTypes/functionSearchType.js +31 -4
  59. package/src/search/fundamentalTypes/mapSearchType.js +33 -5
  60. package/src/search/fundamentalTypes/numberSearchType.js +15 -3
  61. package/src/search/fundamentalTypes/objectSearchType.js +94 -5
  62. package/src/search/fundamentalTypes/promiseSearchType.js +20 -3
  63. package/src/search/fundamentalTypes/regexpSearchType.js +24 -4
  64. package/src/search/fundamentalTypes/setSearchType.js +25 -5
  65. package/src/search/fundamentalTypes/stringSearchType.js +25 -3
  66. package/src/search/fundamentalTypes/symbolSearchType.js +21 -3
  67. package/src/search/index.js +22 -0
  68. package/src/search/queryTree.js +4 -1
  69. package/src/search/queryTreeBuilders.js +3 -2
  70. package/src/search/searchDispatch.js +14 -5
  71. package/src/search/searchElementUtils.js +98 -10
  72. package/src/search/searchUtils.js +440 -12
  73. package/src/search/subTypes/blobHTMLSearchType.js +186 -10
  74. package/src/search/subTypes/recordSearchType.js +30 -4
  75. package/src/search/subTypes/tupleSearchType.js +43 -5
  76. package/src/search/superTypes/SpecialRealNumberSearchType.js +15 -3
  77. package/src/search/superTypes/buffersourceSearchType.js +14 -3
  78. package/src/search/unions/unionFamilySearchType.js +45 -3
  79. package/src/utils/json6Stringify.js +50 -0
  80. package/src/utils/rawTypesonEditor.js +1 -51
  81. package/vendor/zod/classic/schemas.d.ts +3 -1
  82. package/vendor/zod/core/versions.js +1 -1
@@ -1,5 +1,6 @@
1
1
  import {schemaLabel} from '../utils/schemaMeta.js';
2
2
  import {getJSONPointerParts} from '../utils/jsonPointer.js';
3
+ import {combineAnd} from './queryTreeBuilders.js';
3
4
 
4
5
  /**
5
6
  * @typedef {import('../types.js').JamilihArray} JamilihArray
@@ -453,16 +454,23 @@ export function readCheckbox (el) {
453
454
  * "Matches regex" is the chosen mode.
454
455
  *
455
456
  * `flagOptions`, when given, adds a Flags multi-select (options passed in
456
- * by the caller - `stringSearchType.js` passes `regexpType.js`'s own
457
- * `allowedFlags`, the same list `regexpSearchType.js` uses for the actual
458
- * regexp's own flags - kept out of this generic module to avoid it
459
- * depending on a specific fundamental type), shown only while "Matches
460
- * regex" is the chosen mode, the same `$options` a Mongo-flavored `$regex`
461
- * accepts alongside it - a plain literal/substring match has no regex to
462
- * apply flags to, so it stays hidden otherwise. Every other caller
463
- * (`errorSearchType.js`'s props, `fileSearchType.js`'s name/type,
464
- * `domexceptionSearchType.js`'s message) leaves this unset, matching the
465
- * README's flags-for-regexp-only-plus-string bullet.
457
+ * by the caller - most callers pass `regexpType.js`'s own `allowedFlags`,
458
+ * the same list `regexpSearchType.js` uses for the actual regexp's own
459
+ * flags - kept out of this generic module to avoid it depending on a
460
+ * specific fundamental type), shown only while "Matches regex" is the
461
+ * chosen mode, the same `$options` a Mongo-flavored `$regex` accepts
462
+ * alongside it - a plain literal/substring match has no regex to apply
463
+ * flags to, so it stays hidden otherwise. `regexpSearchType.js`'s own call
464
+ * (matching against the regexp's `.source` text, not to be confused with
465
+ * its separate, bespoke Flags control for the regexp's *actual* flags)
466
+ * leaves this unset, since a flag there would have no real regex of its own
467
+ * to apply to.
468
+ *
469
+ * While "Matches regex" is chosen, the Value input is also live syntax-
470
+ * checked (`syncLiteralRegexValidity`, below) against
471
+ * `new RegExp(value, flags)` - an unparsable pattern (or one only invalid
472
+ * for the currently-selected flags, e.g. `u`/`v`'s stricter escape rules)
473
+ * sets a custom validity message rather than silently accepting it.
466
474
  * @param {{
467
475
  * name: string, key?: string, onModeChange?: (this: HTMLElement) => void,
468
476
  * flagOptions?: string[]
@@ -470,6 +478,46 @@ export function readCheckbox (el) {
470
478
  * @returns {JamilihArray}
471
479
  */
472
480
  export function buildLiteralRegexControls ({name, key = '', onModeChange, flagOptions}) {
481
+ /**
482
+ * Live syntax-checks the Value input against `new RegExp(value, flags)`
483
+ * whenever the current Mode is "Matches regex" - a literal/does-not-
484
+ * contain Value is a plain string with no format to violate, so this only
485
+ * has anything to say once "regex" is chosen, and clears back to valid
486
+ * the moment it isn't. Flags are folded in (read fresh off the Flags
487
+ * multi-select, when this call has one) because they can themselves flip
488
+ * a pattern between valid and invalid - the `u`/`v` flags' stricter escape
489
+ * rules being the main example - so a Flags `change` needs to re-run this
490
+ * exactly like a Value `input` or a Mode `change` does.
491
+ * @param {HTMLElement} el - any one of the Mode/Value/Flags controls
492
+ * @returns {void}
493
+ */
494
+ function syncLiteralRegexValidity (el) {
495
+ const root = el.closest('[data-search-path]');
496
+ const modeEl = /** @type {HTMLSelectElement|undefined} */ (
497
+ root ? findOwnControl(root, `select.jsoeSearchMode--${key}`) : undefined
498
+ );
499
+ const valueEl = /** @type {HTMLInputElement|undefined} */ (
500
+ root ? findOwnControl(root, `input.jsoeSearchValue--${key}`) : undefined
501
+ );
502
+ if (!modeEl || !valueEl) {
503
+ return;
504
+ }
505
+ if (modeEl.value !== 'regex' || !valueEl.value) {
506
+ valueEl.setCustomValidity('');
507
+ return;
508
+ }
509
+ const flagsEl = /** @type {HTMLSelectElement|undefined} */ (
510
+ root ? findOwnControl(root, `select.jsoeSearchRegexFlags--${key}`) : undefined
511
+ );
512
+ const flags = [...(flagsEl?.selectedOptions ?? [])].map((opt) => opt.value).join('');
513
+ try {
514
+ // eslint-disable-next-line no-new -- Testing
515
+ new RegExp(valueEl.value, flags);
516
+ valueEl.setCustomValidity('');
517
+ } catch {
518
+ valueEl.setCustomValidity('Enter a valid regular expression.');
519
+ }
520
+ }
473
521
  /**
474
522
  * @this {HTMLElement}
475
523
  * @returns {void}
@@ -482,6 +530,7 @@ export function buildLiteralRegexControls ({name, key = '', onModeChange, flagOp
482
530
  /** @type {HTMLElement} */ (flagsLabel).hidden =
483
531
  /** @type {HTMLSelectElement} */ (this).value !== 'regex';
484
532
  }
533
+ syncLiteralRegexValidity(this);
485
534
  onModeChange?.call(this);
486
535
  }
487
536
  /** @type {JamilihArray[]} */
@@ -490,7 +539,13 @@ export function buildLiteralRegexControls ({name, key = '', onModeChange, flagOp
490
539
  flagsChildren.push(['label', {class: `jsoeSearchRegexFlagsLabel--${key}`, hidden: true}, [
491
540
  'Flags: ',
492
541
  ['select', {
493
- name: `${name}-flags`, multiple: true, class: `jsoeSearchRegexFlags--${key}`
542
+ name: `${name}-flags`, multiple: true, class: `jsoeSearchRegexFlags--${key}`,
543
+ $on: {
544
+ /** @this {HTMLElement} */
545
+ change () {
546
+ syncLiteralRegexValidity(this);
547
+ }
548
+ }
494
549
  }, flagOptions.map((flag) => ['option', {value: flag}, [flag]])]
495
550
  ]]);
496
551
  }
@@ -510,7 +565,13 @@ export function buildLiteralRegexControls ({name, key = '', onModeChange, flagOp
510
565
  'Value: ',
511
566
  ['input', {
512
567
  type: 'text', name: `${name}-value`, class: `jsoeSearchValue--${key}`,
513
- required: true
568
+ required: true,
569
+ $on: {
570
+ /** @this {HTMLElement} */
571
+ input () {
572
+ syncLiteralRegexValidity(this);
573
+ }
574
+ }
514
575
  }]
515
576
  ]],
516
577
  ...flagsChildren
@@ -845,3 +906,370 @@ export function setDescendantsRequired (root, required) {
845
906
  ).required = required;
846
907
  });
847
908
  }
909
+
910
+ /**
911
+ * @typedef {import('./queryTree.js').QueryNode} QueryNode
912
+ */
913
+
914
+ /**
915
+ * The "Edit raw" round-trip (`SearchChoicesControl.$applyQuery`,
916
+ * `src/search/index.js`) needs the inverse of every `getQuery`: given a
917
+ * previously-serialized (or hand-edited) `QueryNode`, drive the same DOM
918
+ * controls `getQuery` reads back into that state, so the two stay in sync.
919
+ * `unwrapAndClauses`/`nodeTouchesPath`/`extractLeafOfKind`/
920
+ * `extractClauseForPath` below are the shared groundwork every
921
+ * `applyQuery` implementation builds on to undo `combineAnd`'s own
922
+ * flattening; the `apply*` functions below those are the direct inverse of
923
+ * one `build*`/`read*` pair each.
924
+ *
925
+ * A plain leaf becomes a one-element array; `undefined` (no constraint)
926
+ * becomes `[]`; an `$and` node's own `.$and` array is returned as-is (not
927
+ * further flattened - a nested `$and` stays a single clause, so
928
+ * `nodeTouchesPath` below is what actually looks inside one).
929
+ * @param {QueryNode|undefined} queryNode
930
+ * @returns {QueryNode[]}
931
+ */
932
+ export function unwrapAndClauses (queryNode) {
933
+ if (queryNode === undefined) {
934
+ return [];
935
+ }
936
+ if ('$and' in queryNode) {
937
+ return queryNode.$and;
938
+ }
939
+ return [queryNode];
940
+ }
941
+
942
+ /**
943
+ * Whether any leaf reachable from `node` (recursing through `$and`/`$or`
944
+ * combinators and a `not`/`passThrough` leaf's own wrapped `query`) targets
945
+ * `path` itself or somewhere nested under it - used to decide which of a
946
+ * container widget's several recursed children (`tupleSearchType.js`'s
947
+ * positions/rest, `functionSearchType.js`'s args/output,
948
+ * `objectSearchType.js`'s properties, `unionFamilySearchType.js`'s chosen
949
+ * branch) a given top-level clause belongs to, since a recursed child's own
950
+ * `getQuery()` result is threaded through untouched (whatever shape it is)
951
+ * rather than re-wrapped with the parent's path.
952
+ * @param {QueryNode|undefined} node
953
+ * @param {string} path
954
+ * @returns {boolean}
955
+ */
956
+ export function nodeTouchesPath (node, path) {
957
+ // `QueryNotLeaf` is the only leaf kind with no `path` of its own - unwrap
958
+ // its `query` (iteratively, in case of a `not` wrapping another `not`)
959
+ // before dispatching on the combinator/leaf shapes below.
960
+ let current = node;
961
+ while (
962
+ current !== undefined && !('$and' in current) && !('$or' in current) &&
963
+ !('path' in current) && 'query' in current
964
+ ) {
965
+ ({query: current} = current);
966
+ }
967
+ if (current === undefined) {
968
+ return false;
969
+ }
970
+ if ('$and' in current) {
971
+ return current.$and.some((child) => nodeTouchesPath(child, path));
972
+ }
973
+ if ('$or' in current) {
974
+ return current.$or.some((child) => nodeTouchesPath(child, path));
975
+ }
976
+ if ('path' in current) {
977
+ return current.path === path || current.path.startsWith(`${path}/`);
978
+ }
979
+ return false;
980
+ }
981
+
982
+ /**
983
+ * Pulls the (at most one) top-level clause of `queryNode` whose own `kind`
984
+ * matches out of the `$and` it's combined into, leaving the rest re-combined
985
+ * - the inverse half of `combineAnd([ownLeaf, ...])`. Safe to use for any
986
+ * facet whose own leaf kind is unique within its widget (every `kind` this
987
+ * module's callers pass is exactly that: `lengthSize`, `hasProperty`,
988
+ * `mapRecordJoint`, `typeOf`), since a recursed child's own (arbitrarily
989
+ * nested) contribution never surfaces as a *bare* top-level leaf sharing the
990
+ * parent's own facet kind.
991
+ * `kind`'s own type parameter narrows `matched`'s type to exactly the leaf
992
+ * shape that `kind` names (`Extract<QueryLeaf, {kind: K}>`), so a call like
993
+ * `extractLeafOfKind(queryNode, 'range')` gives back a properly-typed
994
+ * `QueryRangeLeaf|undefined` with no cast needed at the call site.
995
+ * @template {import('./queryTree.js').QueryLeaf['kind']} K
996
+ * @param {QueryNode|undefined} queryNode
997
+ * @param {K} kind
998
+ * @returns {{
999
+ * matched: Extract<import('./queryTree.js').QueryLeaf, {kind: K}>|undefined,
1000
+ * rest: QueryNode|undefined
1001
+ * }}
1002
+ */
1003
+ export function extractLeafOfKind (queryNode, kind) {
1004
+ const clauses = unwrapAndClauses(queryNode);
1005
+ const idx = clauses.findIndex((clause) => 'kind' in clause && clause.kind === kind);
1006
+ if (idx === -1) {
1007
+ return {matched: undefined, rest: queryNode};
1008
+ }
1009
+ return {
1010
+ matched: /** @type {Extract<import('./queryTree.js').QueryLeaf, {kind: K}>} */ (
1011
+ clauses[idx]
1012
+ ),
1013
+ rest: combineAnd(clauses.filter((_clause, i) => i !== idx))
1014
+ };
1015
+ }
1016
+
1017
+ /**
1018
+ * Pulls the (at most one) top-level clause of `queryNode` that
1019
+ * `nodeTouchesPath` says belongs to `path` out of the `$and` it's combined
1020
+ * into, leaving the rest re-combined - the path-based counterpart of
1021
+ * `extractLeafOfKind`, for a recursed child's own contribution (whose shape
1022
+ * isn't a single known leaf `kind`, unlike a container's own facets).
1023
+ * @param {QueryNode|undefined} queryNode
1024
+ * @param {string} path
1025
+ * @returns {{matched: QueryNode|undefined, rest: QueryNode|undefined}}
1026
+ */
1027
+ export function extractClauseForPath (queryNode, path) {
1028
+ const clauses = unwrapAndClauses(queryNode);
1029
+ const idx = clauses.findIndex((clause) => nodeTouchesPath(clause, path));
1030
+ if (idx === -1) {
1031
+ return {matched: undefined, rest: queryNode};
1032
+ }
1033
+ return {
1034
+ matched: clauses[idx],
1035
+ rest: combineAnd(clauses.filter((_clause, i) => i !== idx))
1036
+ };
1037
+ }
1038
+
1039
+ /**
1040
+ * The inverse of `readLiteralRegexQuery` - sets `buildLiteralRegexControls`'s
1041
+ * Mode/Value/Flags back from a previously-read leaf (or resets to defaults
1042
+ * for `undefined`, meaning the facet no longer has a constraint at all).
1043
+ * Dispatches `change` on the Mode select (so its own `handleModeChange`
1044
+ * shows/hides the Flags control and re-validates) and `input` on the Value
1045
+ * input (so `syncLiteralRegexValidity` re-runs), the same events a real user
1046
+ * interacting with these controls would fire.
1047
+ * @param {Element} el
1048
+ * @param {import('./queryTree.js').QueryLiteralSetLeaf|
1049
+ * import('./queryTree.js').QueryRegexLeaf|
1050
+ * import('./queryTree.js').QueryNotContainsLeaf|undefined} leaf
1051
+ * @param {string} [key]
1052
+ * @returns {void}
1053
+ */
1054
+ export function applyLiteralRegexQuery (el, leaf, key = '') {
1055
+ const modeEl = /** @type {HTMLSelectElement|undefined} */ (
1056
+ findOwnControl(el, `select.jsoeSearchMode--${key}`)
1057
+ );
1058
+ const valueEl = /** @type {HTMLInputElement|undefined} */ (
1059
+ findOwnControl(el, `input.jsoeSearchValue--${key}`)
1060
+ );
1061
+ if (!modeEl || !valueEl) {
1062
+ return;
1063
+ }
1064
+ const flagsEl = /** @type {HTMLSelectElement|undefined} */ (
1065
+ findOwnControl(el, `select.jsoeSearchRegexFlags--${key}`)
1066
+ );
1067
+ let mode = 'literal';
1068
+ let value = '';
1069
+ let flags = '';
1070
+ switch (leaf?.kind) {
1071
+ case 'regex': {
1072
+ mode = 'regex';
1073
+ ({$regex: value} = leaf);
1074
+ flags = leaf.$options ?? '';
1075
+
1076
+ break;
1077
+ }
1078
+ case 'notContains': {
1079
+ mode = 'notContains';
1080
+ ({value} = leaf);
1081
+
1082
+ break;
1083
+ }
1084
+ case 'literalSet': {
1085
+ value = (leaf.$in ?? []).map(String).join(', ');
1086
+
1087
+ break;
1088
+ }
1089
+ // No default
1090
+ }
1091
+ modeEl.value = mode;
1092
+ valueEl.value = value;
1093
+ if (flagsEl) {
1094
+ const flagChars = new Set(flags.split(''));
1095
+ [...flagsEl.options].forEach((opt) => {
1096
+ opt.selected = flagChars.has(opt.value);
1097
+ });
1098
+ }
1099
+ modeEl.dispatchEvent(new Event('change'));
1100
+ valueEl.dispatchEvent(new Event('input'));
1101
+ }
1102
+
1103
+ /**
1104
+ * The inverse of `readRangeInputsPair` - sets the From/To inputs back from a
1105
+ * previously-read `range` leaf (or clears both for `undefined`). An
1106
+ * exclusive `$gt`/`$lt` bound (never produced by any `getQuery` in this
1107
+ * codebase, but tolerated on a hand-edited raw query) is treated the same as
1108
+ * its inclusive `$gte`/`$lte` counterpart, since the UI has only one plain
1109
+ * bound per side, not a separate inclusive/exclusive toggle.
1110
+ * @param {Element} el
1111
+ * @param {import('./queryTree.js').QueryRangeLeaf|undefined} leaf
1112
+ * @param {string} [key]
1113
+ * @returns {void}
1114
+ */
1115
+ export function applyRangeQuery (el, leaf, key = '') {
1116
+ const gteEl = /** @type {HTMLInputElement|undefined} */ (
1117
+ findOwnControl(el, `input.jsoeSearchRangeGte--${key}`)
1118
+ );
1119
+ const lteEl = /** @type {HTMLInputElement|undefined} */ (
1120
+ findOwnControl(el, `input.jsoeSearchRangeLte--${key}`)
1121
+ );
1122
+ if (!gteEl || !lteEl) {
1123
+ return;
1124
+ }
1125
+ const gte = leaf?.$gte ?? leaf?.$gt;
1126
+ const lte = leaf?.$lte ?? leaf?.$lt;
1127
+ gteEl.value = gte === undefined ? '' : String(gte);
1128
+ lteEl.value = lte === undefined ? '' : String(lte);
1129
+ gteEl.dispatchEvent(new Event('input'));
1130
+ lteEl.dispatchEvent(new Event('input'));
1131
+ }
1132
+
1133
+ /**
1134
+ * The inverse of `readTriStateSelect`/`buildHasPropertyToggle` - `undefined`
1135
+ * maps back to "(any)".
1136
+ * @param {Element} el
1137
+ * @param {boolean|undefined} value
1138
+ * @param {string} [key]
1139
+ * @returns {void}
1140
+ */
1141
+ export function applyTriState (el, value, key = '') {
1142
+ const select = /** @type {HTMLSelectElement|undefined} */ (
1143
+ findOwnControl(el, `select.jsoeSearchTriState--${key}`)
1144
+ );
1145
+ if (!select) {
1146
+ return;
1147
+ }
1148
+ select.value = value === undefined ? '' : String(value);
1149
+ select.dispatchEvent(new Event('change'));
1150
+ }
1151
+
1152
+ /**
1153
+ * The inverse of `readCheckbox`.
1154
+ * @param {Element} el
1155
+ * @param {boolean} checked
1156
+ * @returns {void}
1157
+ */
1158
+ export function applyCheckbox (el, checked) {
1159
+ const checkbox = /** @type {HTMLInputElement|undefined} */ (
1160
+ findOwnControl(el, 'input.jsoeSearchCheckbox')
1161
+ );
1162
+ if (!checkbox) {
1163
+ return;
1164
+ }
1165
+ checkbox.checked = checked;
1166
+ checkbox.dispatchEvent(new Event('change'));
1167
+ }
1168
+
1169
+ /**
1170
+ * The inverse of `readMultiSelect` - `values` is compared against each
1171
+ * `<option>`'s own `value` as a string (`String(item)`), matching how
1172
+ * `enumSearchType.js`/`SpecialRealNumberSearchType.js` build their own
1173
+ * options from stringified values.
1174
+ * @param {Element} el
1175
+ * @param {unknown[]} values
1176
+ * @returns {void}
1177
+ */
1178
+ export function applyMultiSelect (el, values) {
1179
+ const select = /** @type {HTMLSelectElement|undefined} */ (
1180
+ findOwnControl(el, 'select.jsoeSearchMultiSelect')
1181
+ );
1182
+ if (!select) {
1183
+ return;
1184
+ }
1185
+ const strValues = new Set(values.map(String));
1186
+ [...select.options].forEach((opt) => {
1187
+ opt.selected = strValues.has(opt.value);
1188
+ });
1189
+ select.dispatchEvent(new Event('change'));
1190
+ }
1191
+
1192
+ /**
1193
+ * The inverse of `readLengthSizeQuery` - clears both the size input and the
1194
+ * sparse tri-state for `undefined`.
1195
+ * @param {Element} el
1196
+ * @param {import('./queryTree.js').QueryLengthSizeLeaf|undefined} leaf
1197
+ * @returns {void}
1198
+ */
1199
+ export function applyLengthSizeQuery (el, leaf) {
1200
+ const sizeEl = /** @type {HTMLInputElement|undefined} */ (
1201
+ findOwnControl(el, 'input.jsoeSearchSize')
1202
+ );
1203
+ if (sizeEl) {
1204
+ sizeEl.value = leaf?.$size === undefined ? '' : String(leaf.$size);
1205
+ sizeEl.dispatchEvent(new Event('input'));
1206
+ }
1207
+ applyTriState(el, leaf?.sparseCheck);
1208
+ }
1209
+
1210
+ /**
1211
+ * The inverse of `readOptInChecked` - also dispatches `change` so
1212
+ * `wireOptInFieldset`'s own listener toggles the paired fieldset's
1213
+ * `disabled` state and runs whatever `onToggle` it was wired with (e.g. a
1214
+ * widget's own `syncAtLeastOneCheck` re-run).
1215
+ * @param {Element} el
1216
+ * @param {boolean} checked
1217
+ * @param {string} [key]
1218
+ * @returns {void}
1219
+ */
1220
+ export function applyOptIn (el, checked, key = '') {
1221
+ const checkbox = /** @type {HTMLInputElement|undefined} */ (
1222
+ findOwnControl(el, `input.jsoeSearchOptIn--${key}`)
1223
+ );
1224
+ if (!checkbox) {
1225
+ return;
1226
+ }
1227
+ checkbox.checked = checked;
1228
+ checkbox.dispatchEvent(new Event('change'));
1229
+ }
1230
+
1231
+ /**
1232
+ * A `buildOptInFieldset`-wrapped `buildLiteralRegexControls` facet
1233
+ * (`fileSearchType.js`'s name/content-type, `domexceptionSearchType.js`'s
1234
+ * message, `makeErrorFamilySearchType`'s string properties): opts the facet
1235
+ * in exactly when `queryNode` has a clause touching `path`, and applies that
1236
+ * clause (a bare literal/regex/does-not-contain leaf, since none of these
1237
+ * facets recurse any deeper) to its Mode/Value/Flags controls.
1238
+ * @param {Element} root
1239
+ * @param {QueryNode|undefined} queryNode
1240
+ * @param {string} path
1241
+ * @param {string} [key]
1242
+ * @returns {void}
1243
+ */
1244
+ export function applyOptInLiteralRegexFacet (root, queryNode, path, key = '') {
1245
+ const {matched} = extractClauseForPath(queryNode, path);
1246
+ applyOptIn(root, matched !== undefined, key);
1247
+ applyLiteralRegexQuery(
1248
+ root,
1249
+ /**
1250
+ * @type {import('./queryTree.js').QueryLiteralSetLeaf|
1251
+ *import('./queryTree.js').QueryRegexLeaf|
1252
+ import('./queryTree.js').QueryNotContainsLeaf|undefined} */ (matched),
1253
+ key
1254
+ );
1255
+ }
1256
+
1257
+ /**
1258
+ * A `buildOptInFieldset`-wrapped `buildRangeInputsPair` facet
1259
+ * (`makeErrorFamilySearchType`'s number properties,
1260
+ * `makeDomShapeSearchType`'s dimensions): opts the facet in exactly when
1261
+ * `queryNode` has a clause touching `path`, and applies that clause (a bare
1262
+ * `range` leaf) to its From/To controls.
1263
+ * @param {Element} root
1264
+ * @param {QueryNode|undefined} queryNode
1265
+ * @param {string} path
1266
+ * @param {string} [key]
1267
+ * @returns {void}
1268
+ */
1269
+ export function applyOptInRangeFacet (root, queryNode, path, key = '') {
1270
+ const {matched} = extractClauseForPath(queryNode, path);
1271
+ applyOptIn(root, matched !== undefined, key);
1272
+ applyRangeQuery(
1273
+ root, /** @type {import('./queryTree.js').QueryRangeLeaf|undefined} */ (matched), key
1274
+ );
1275
+ }