rich-input 1.1.0 → 1.2.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.
- package/README.md +8 -1
- package/components/rich-input.js +102 -2
- package/index.js +2 -1
- package/package.json +1 -1
- package/utils/highlights.js +43 -0
- package/utils/query-parser.js +22 -0
package/README.md
CHANGED
|
@@ -37,8 +37,9 @@ The visual below illustrates the internal Shadow DOM elements, exposed CSS Shado
|
|
|
37
37
|
- `<rich-input>`: The host custom element wrapping the control, datalists, and suggestions popover.
|
|
38
38
|
- `::part(control)`: The outer input container enclosing the icon, input, and clear button.
|
|
39
39
|
- `::part(icon)`: The default leading search magnifying glass SVG icon (fallback in `slot="leading"`).
|
|
40
|
-
- `::part(input)`: The native `<input type="text">` where users type.
|
|
41
40
|
- `::highlight(<keyword>)`: Target pseudo-element for styling keyword values via the CSS Custom Highlight API (e.g. `::highlight(label)`, `::highlight(year)`).
|
|
41
|
+
- `::highlight(rich-input-keyword)`: Target pseudo-element for styling keyword prefixes (e.g. `label:`, `year:`).
|
|
42
|
+
- `::highlight(rich-input-invalid)`: Target pseudo-element for marking unrecognized keywords or invalid keyword values (not in datalist) with a squiggly underline.
|
|
42
43
|
- `::part(clear-button)`: The clear button (visible when text is present).
|
|
43
44
|
- `::part(popover)`: The autocomplete dropdown popover container anchored to the start of the active range via `OpaqueRange` (or mirror-div fallback).
|
|
44
45
|
- `::part(suggestions-header)`: The header bar at the top of the suggestions popover.
|
|
@@ -268,6 +269,12 @@ Values corresponding to configured keywords are registered into the global `CSS.
|
|
|
268
269
|
color: #64748b;
|
|
269
270
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
270
271
|
}
|
|
272
|
+
|
|
273
|
+
/* Invalid highlight (squiggly underline for unrecognized keywords or values not present in datalist) */
|
|
274
|
+
::highlight(rich-input-invalid) {
|
|
275
|
+
text-decoration: underline wavy #ef4444;
|
|
276
|
+
text-decoration-skip-ink: none;
|
|
277
|
+
}
|
|
271
278
|
```
|
|
272
279
|
|
|
273
280
|
In browsers using the `[contenteditable]` fallback inside Shadow DOM (such as Safari and Firefox), `<rich-input>` automatically syncs document-level `::highlight()` rules into its shadow stylesheet so that highlighting works across shadow boundaries without extra markup.
|
package/components/rich-input.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keyword-based autocomplete input field powered by OpaqueRange and Custom Highlight API.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { parseSearchTokens, parseSearchQuery, getCaretContext, getSuggestions, applySuggestion } from '../utils/query-parser.js';
|
|
6
|
+
import { parseSearchTokens, parseSearchQuery, getCaretContext, getSuggestions, applySuggestion, isDatalistValue } from '../utils/query-parser.js';
|
|
7
7
|
import { highlightManager, isOpaqueRangeSupported, isHighlightSupported } from '../utils/highlights.js';
|
|
8
8
|
import { getCaretCoordinates, positionPopover } from '../utils/positioning.js';
|
|
9
9
|
import { setupContentEditableAdapter, isContentEditableFallbackActive } from '../utils/contenteditable-adapter.js';
|
|
@@ -119,6 +119,19 @@ TEMPLATE.innerHTML = `
|
|
|
119
119
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* Squiggly line underneath invalid keyword values */
|
|
123
|
+
::highlight(rich-input-invalid) {
|
|
124
|
+
text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
125
|
+
-webkit-text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
126
|
+
text-decoration-line: underline;
|
|
127
|
+
-webkit-text-decoration-line: underline;
|
|
128
|
+
text-decoration-style: wavy;
|
|
129
|
+
-webkit-text-decoration-style: wavy;
|
|
130
|
+
text-decoration-color: var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
131
|
+
-webkit-text-decoration-color: var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
132
|
+
text-decoration-skip-ink: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
/* Visually hide datalists and custom style tags in slot */
|
|
123
136
|
::slotted(datalist),
|
|
124
137
|
::slotted(style) {
|
|
@@ -389,7 +402,10 @@ export class RichInput extends HTMLElement {
|
|
|
389
402
|
this._selectedIndex = -1;
|
|
390
403
|
this._context = null;
|
|
391
404
|
this._ownedRanges = [];
|
|
405
|
+
this._invalidRanges = [];
|
|
392
406
|
this._activeKeywordHighlightMap = new Map();
|
|
407
|
+
this._isFocused = false;
|
|
408
|
+
this._lastCaretPosition = -1;
|
|
393
409
|
|
|
394
410
|
// Bound listeners for easy cleanup
|
|
395
411
|
this._onInput = this._onInput.bind(this);
|
|
@@ -398,6 +414,7 @@ export class RichInput extends HTMLElement {
|
|
|
398
414
|
this._onFocus = this._onFocus.bind(this);
|
|
399
415
|
this._onBlur = this._onBlur.bind(this);
|
|
400
416
|
this._onClick = this._onClick.bind(this);
|
|
417
|
+
this._onSelectionChange = this._onSelectionChange.bind(this);
|
|
401
418
|
this._onClearClick = this._onClearClick.bind(this);
|
|
402
419
|
this._onSlotChange = this._onSlotChange.bind(this);
|
|
403
420
|
this._onGlobalClick = this._onGlobalClick.bind(this);
|
|
@@ -409,6 +426,14 @@ export class RichInput extends HTMLElement {
|
|
|
409
426
|
syncDocumentHighlightStyles(this.shadowRoot);
|
|
410
427
|
highlightManager.register(this);
|
|
411
428
|
|
|
429
|
+
// Track initial focus state
|
|
430
|
+
this._isFocused = Boolean(
|
|
431
|
+
this.shadowRoot?.activeElement === this._input ||
|
|
432
|
+
document.activeElement === this ||
|
|
433
|
+
document.activeElement === this._input ||
|
|
434
|
+
this.matches?.(':focus-within')
|
|
435
|
+
);
|
|
436
|
+
|
|
412
437
|
// Parse initial datalists
|
|
413
438
|
this._loadDatalists();
|
|
414
439
|
|
|
@@ -432,6 +457,7 @@ export class RichInput extends HTMLElement {
|
|
|
432
457
|
|
|
433
458
|
// Global events
|
|
434
459
|
document.addEventListener('click', this._onGlobalClick);
|
|
460
|
+
document.addEventListener('selectionchange', this._onSelectionChange);
|
|
435
461
|
window.addEventListener('resize', this._onGlobalResizeOrScroll);
|
|
436
462
|
window.addEventListener('scroll', this._onGlobalResizeOrScroll, { passive: true });
|
|
437
463
|
|
|
@@ -471,6 +497,7 @@ export class RichInput extends HTMLElement {
|
|
|
471
497
|
this._clearBtn.removeEventListener('click', this._onClearClick);
|
|
472
498
|
|
|
473
499
|
document.removeEventListener('click', this._onGlobalClick);
|
|
500
|
+
document.removeEventListener('selectionchange', this._onSelectionChange);
|
|
474
501
|
window.removeEventListener('resize', this._onGlobalResizeOrScroll);
|
|
475
502
|
window.removeEventListener('scroll', this._onGlobalResizeOrScroll);
|
|
476
503
|
|
|
@@ -603,10 +630,12 @@ export class RichInput extends HTMLElement {
|
|
|
603
630
|
|
|
604
631
|
// --- Public Methods ---
|
|
605
632
|
focus(options) {
|
|
633
|
+
this._isFocused = true;
|
|
606
634
|
this._input.focus(options);
|
|
607
635
|
}
|
|
608
636
|
|
|
609
637
|
blur() {
|
|
638
|
+
this._isFocused = false;
|
|
610
639
|
this._input.blur();
|
|
611
640
|
}
|
|
612
641
|
|
|
@@ -722,6 +751,7 @@ export class RichInput extends HTMLElement {
|
|
|
722
751
|
} catch (e) {}
|
|
723
752
|
}
|
|
724
753
|
this._ownedRanges = [];
|
|
754
|
+
this._invalidRanges = [];
|
|
725
755
|
this._activeKeywordHighlightMap.clear();
|
|
726
756
|
}
|
|
727
757
|
|
|
@@ -747,9 +777,17 @@ export class RichInput extends HTMLElement {
|
|
|
747
777
|
const tokens = parseSearchTokens(text);
|
|
748
778
|
const highlightQuotes = this.getAttribute('highlight-quotes') !== 'exclude';
|
|
749
779
|
|
|
780
|
+
const isFocused = this._isFocused;
|
|
781
|
+
|
|
782
|
+
const caretStart = typeof this._input.selectionStart === 'number' ? this._input.selectionStart : null;
|
|
783
|
+
const caretEnd = typeof this._input.selectionEnd === 'number' ? this._input.selectionEnd : null;
|
|
784
|
+
const selMin = caretStart !== null ? Math.min(caretStart, caretEnd ?? caretStart) : -1;
|
|
785
|
+
const selMax = caretEnd !== null ? Math.max(caretStart ?? caretEnd, caretEnd) : -1;
|
|
786
|
+
|
|
750
787
|
for (const token of tokens) {
|
|
751
788
|
if (token.type === 'keyword' && this._configuredKeywords.has(token.keywordLower)) {
|
|
752
789
|
const kw = token.keywordLower;
|
|
790
|
+
const kwConfig = this._configuredKeywords.get(kw);
|
|
753
791
|
|
|
754
792
|
if (!this._activeKeywordHighlightMap.has(kw)) {
|
|
755
793
|
this._activeKeywordHighlightMap.set(kw, {
|
|
@@ -764,7 +802,7 @@ export class RichInput extends HTMLElement {
|
|
|
764
802
|
const start = highlightQuotes ? token.valueStart : token.innerStart;
|
|
765
803
|
const end = highlightQuotes ? token.valueEnd : token.innerEnd;
|
|
766
804
|
|
|
767
|
-
if (end
|
|
805
|
+
if (end > start && end <= text.length) {
|
|
768
806
|
try {
|
|
769
807
|
const valRange = this._input.createValueRange(start, end);
|
|
770
808
|
this._ownedRanges.push(valRange);
|
|
@@ -782,6 +820,42 @@ export class RichInput extends HTMLElement {
|
|
|
782
820
|
bucket.keywordRanges.push(kwRange);
|
|
783
821
|
} catch (e) {}
|
|
784
822
|
}
|
|
823
|
+
|
|
824
|
+
// 3. Validation: Check if value is part of the datalist
|
|
825
|
+
// Don't mark as invalid if:
|
|
826
|
+
// - Value is empty (user hasn't entered a value yet)
|
|
827
|
+
// - Input is focused and user is currently editing this token (caret is on/within this token)
|
|
828
|
+
const isEditingToken = isFocused && selMin !== -1 && selMax >= token.start && selMin <= token.end;
|
|
829
|
+
const hasValue = Boolean(token.innerValue && token.innerValue.trim().length > 0);
|
|
830
|
+
|
|
831
|
+
if (hasValue && !isEditingToken && !isDatalistValue(kwConfig, token.innerValue)) {
|
|
832
|
+
if (end > start && end <= text.length) {
|
|
833
|
+
try {
|
|
834
|
+
const invRange = this._input.createValueRange(start, end);
|
|
835
|
+
this._ownedRanges.push(invRange);
|
|
836
|
+
this._invalidRanges.push(invRange);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
console.warn('[rich-input] Invalid range creation error:', e);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} else if (token.type === 'keyword') {
|
|
843
|
+
// Unrecognized key:value pair (keyword is not in configured datalists)
|
|
844
|
+
const isEditingToken = isFocused && selMin !== -1 && selMax >= token.start && selMin <= token.end;
|
|
845
|
+
|
|
846
|
+
if (!isEditingToken) {
|
|
847
|
+
const start = token.start;
|
|
848
|
+
const end = token.end;
|
|
849
|
+
if (end > start && end <= text.length) {
|
|
850
|
+
try {
|
|
851
|
+
const invRange = this._input.createValueRange(start, end);
|
|
852
|
+
this._ownedRanges.push(invRange);
|
|
853
|
+
this._invalidRanges.push(invRange);
|
|
854
|
+
} catch (e) {
|
|
855
|
+
console.warn('[rich-input] Invalid range creation error:', e);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
785
859
|
}
|
|
786
860
|
}
|
|
787
861
|
|
|
@@ -792,8 +866,13 @@ export class RichInput extends HTMLElement {
|
|
|
792
866
|
return this._activeKeywordHighlightMap;
|
|
793
867
|
}
|
|
794
868
|
|
|
869
|
+
getActiveInvalidRanges() {
|
|
870
|
+
return this._invalidRanges;
|
|
871
|
+
}
|
|
872
|
+
|
|
795
873
|
// --- Suggestions Popover Handling ---
|
|
796
874
|
_onInput(e) {
|
|
875
|
+
this._lastCaretPosition = this._input.selectionStart;
|
|
797
876
|
this._updateClearButton();
|
|
798
877
|
this.updateHighlights();
|
|
799
878
|
this.updateSuggestions('input');
|
|
@@ -847,12 +926,17 @@ export class RichInput extends HTMLElement {
|
|
|
847
926
|
}
|
|
848
927
|
|
|
849
928
|
_onKeyUp(e) {
|
|
929
|
+
this._lastCaretPosition = this._input.selectionStart;
|
|
850
930
|
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
|
|
931
|
+
this.updateHighlights();
|
|
851
932
|
this.updateSuggestions('caret');
|
|
852
933
|
}
|
|
853
934
|
}
|
|
854
935
|
|
|
855
936
|
_onFocus() {
|
|
937
|
+
this._isFocused = true;
|
|
938
|
+
this._lastCaretPosition = this._input.selectionStart;
|
|
939
|
+
this.updateHighlights();
|
|
856
940
|
// Optionally open suggestions if context matches
|
|
857
941
|
if (this._input.value.length > 0) {
|
|
858
942
|
this.updateSuggestions('focus');
|
|
@@ -860,6 +944,9 @@ export class RichInput extends HTMLElement {
|
|
|
860
944
|
}
|
|
861
945
|
|
|
862
946
|
_onBlur() {
|
|
947
|
+
this._isFocused = false;
|
|
948
|
+
this._lastCaretPosition = -1;
|
|
949
|
+
this.updateHighlights();
|
|
863
950
|
// Delay closing so click events on popover items can fire
|
|
864
951
|
setTimeout(() => {
|
|
865
952
|
this.hideSuggestions();
|
|
@@ -868,12 +955,25 @@ export class RichInput extends HTMLElement {
|
|
|
868
955
|
}
|
|
869
956
|
|
|
870
957
|
_onClick(e) {
|
|
958
|
+
this._isFocused = true;
|
|
871
959
|
if (e && typeof e.clientX === 'number' && typeof this._input?.updateCaretFromPoint === 'function') {
|
|
872
960
|
this._input.updateCaretFromPoint(e.clientX, e.clientY);
|
|
873
961
|
}
|
|
962
|
+
this._lastCaretPosition = this._input.selectionStart;
|
|
963
|
+
this.updateHighlights();
|
|
874
964
|
this.updateSuggestions('click');
|
|
875
965
|
}
|
|
876
966
|
|
|
967
|
+
_onSelectionChange() {
|
|
968
|
+
if (!this._isFocused) return;
|
|
969
|
+
|
|
970
|
+
const caret = this._input.selectionStart;
|
|
971
|
+
if (this._lastCaretPosition !== caret) {
|
|
972
|
+
this._lastCaretPosition = caret;
|
|
973
|
+
this.updateHighlights();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
877
977
|
_onClearClick() {
|
|
878
978
|
this.value = '';
|
|
879
979
|
this._input.focus();
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { RichInput } from './components/rich-input.js';
|
|
7
|
-
import { parseSearchTokens, parseSearchQuery, getCaretContext, getSuggestions, applySuggestion } from './utils/query-parser.js';
|
|
7
|
+
import { parseSearchTokens, parseSearchQuery, getCaretContext, getSuggestions, applySuggestion, isDatalistValue } from './utils/query-parser.js';
|
|
8
8
|
import { highlightManager, isOpaqueRangeSupported, isHighlightSupported } from './utils/highlights.js';
|
|
9
9
|
import { getCaretCoordinates, getRangeCoordinates, positionPopover, getCaretLeftWithMirrorDiv } from './utils/positioning.js';
|
|
10
10
|
import { setupContentEditableAdapter, isContentEditableFallbackActive } from './utils/contenteditable-adapter.js';
|
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
getCaretContext,
|
|
23
23
|
getSuggestions,
|
|
24
24
|
applySuggestion,
|
|
25
|
+
isDatalistValue,
|
|
25
26
|
highlightManager,
|
|
26
27
|
isOpaqueRangeSupported,
|
|
27
28
|
isHighlightSupported,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rich-input",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A rich input field with keyword-based autocomplete and in-input highlighting powered by <datalist>, OpaqueRange, and the Custom Highlight API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/utils/highlights.js
CHANGED
|
@@ -13,6 +13,17 @@ export const isHighlightSupported =
|
|
|
13
13
|
typeof CSS !== 'undefined' &&
|
|
14
14
|
'highlights' in CSS;
|
|
15
15
|
|
|
16
|
+
function isRangeCollapsed(range) {
|
|
17
|
+
if (!range) return true;
|
|
18
|
+
if (range.collapsed === true) return true;
|
|
19
|
+
if (typeof range.startOffset === 'number' && typeof range.endOffset === 'number') {
|
|
20
|
+
if (range.startOffset === range.endOffset && range.startContainer === range.endContainer) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
class HighlightRegistryManager {
|
|
17
28
|
constructor() {
|
|
18
29
|
this.instances = new Set();
|
|
@@ -41,6 +52,7 @@ class HighlightRegistryManager {
|
|
|
41
52
|
const rangesByKeyword = new Map();
|
|
42
53
|
const allKeywordRanges = [];
|
|
43
54
|
const allValueRanges = [];
|
|
55
|
+
const allInvalidRanges = [];
|
|
44
56
|
|
|
45
57
|
for (const inst of this.instances) {
|
|
46
58
|
const instanceRanges = inst.getActiveHighlightRanges();
|
|
@@ -61,6 +73,13 @@ class HighlightRegistryManager {
|
|
|
61
73
|
allKeywordRanges.push(...data.keywordRanges);
|
|
62
74
|
}
|
|
63
75
|
}
|
|
76
|
+
|
|
77
|
+
if (typeof inst.getActiveInvalidRanges === 'function') {
|
|
78
|
+
const invRanges = inst.getActiveInvalidRanges();
|
|
79
|
+
if (invRanges && invRanges.length > 0) {
|
|
80
|
+
allInvalidRanges.push(...invRanges);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
// 1. Set/update individual keyword highlights (e.g. ::highlight(label))
|
|
@@ -77,6 +96,7 @@ class HighlightRegistryManager {
|
|
|
77
96
|
hl.clear();
|
|
78
97
|
for (const r of ranges) {
|
|
79
98
|
try {
|
|
99
|
+
if (isRangeCollapsed(r)) continue;
|
|
80
100
|
hl.add(r);
|
|
81
101
|
} catch (e) {}
|
|
82
102
|
}
|
|
@@ -95,6 +115,7 @@ class HighlightRegistryManager {
|
|
|
95
115
|
kwHl.clear();
|
|
96
116
|
for (const r of allKeywordRanges) {
|
|
97
117
|
try {
|
|
118
|
+
if (isRangeCollapsed(r)) continue;
|
|
98
119
|
kwHl.add(r);
|
|
99
120
|
} catch (e) {}
|
|
100
121
|
}
|
|
@@ -111,10 +132,32 @@ class HighlightRegistryManager {
|
|
|
111
132
|
valHl.clear();
|
|
112
133
|
for (const r of allValueRanges) {
|
|
113
134
|
try {
|
|
135
|
+
if (isRangeCollapsed(r)) continue;
|
|
114
136
|
valHl.add(r);
|
|
115
137
|
} catch (e) {}
|
|
116
138
|
}
|
|
117
139
|
}
|
|
140
|
+
|
|
141
|
+
// 3. Set invalid highlights (rich-input-invalid)
|
|
142
|
+
let invHl = CSS.highlights.get('rich-input-invalid');
|
|
143
|
+
if (!invHl) {
|
|
144
|
+
try {
|
|
145
|
+
invHl = new Highlight();
|
|
146
|
+
CSS.highlights.set('rich-input-invalid', invHl);
|
|
147
|
+
} catch (e) {}
|
|
148
|
+
}
|
|
149
|
+
if (invHl) {
|
|
150
|
+
try {
|
|
151
|
+
invHl.priority = 10;
|
|
152
|
+
} catch (e) {}
|
|
153
|
+
invHl.clear();
|
|
154
|
+
for (const r of allInvalidRanges) {
|
|
155
|
+
try {
|
|
156
|
+
if (isRangeCollapsed(r)) continue;
|
|
157
|
+
invHl.add(r);
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
118
161
|
}
|
|
119
162
|
}
|
|
120
163
|
|
package/utils/query-parser.js
CHANGED
|
@@ -342,3 +342,25 @@ export function applySuggestion(inputStr, suggestion, context) {
|
|
|
342
342
|
const newCaret = before.length + insertText.length;
|
|
343
343
|
return { newValue, newCaret };
|
|
344
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks whether a given value matches any option in the keyword configuration.
|
|
348
|
+
* @param {Object} kwConfig
|
|
349
|
+
* @param {string} value
|
|
350
|
+
* @returns {boolean}
|
|
351
|
+
*/
|
|
352
|
+
export function isDatalistValue(kwConfig, value) {
|
|
353
|
+
if (!kwConfig || !Array.isArray(kwConfig.options) || kwConfig.options.length === 0) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
const val = (value ?? '').trim().toLowerCase();
|
|
357
|
+
if (!val) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return kwConfig.options.some((opt) => {
|
|
361
|
+
const optVal = (opt.value ?? '').trim().toLowerCase();
|
|
362
|
+
const optLabel = (opt.label ?? '').trim().toLowerCase();
|
|
363
|
+
const optText = (opt.text ?? '').trim().toLowerCase();
|
|
364
|
+
return optVal === val || optLabel === val || optText === val;
|
|
365
|
+
});
|
|
366
|
+
}
|