rich-input 1.1.1 → 1.2.1
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 +15 -3
- package/components/rich-input.js +101 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/utils/highlights.js +29 -0
- package/utils/query-parser.js +30 -9
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.
|
|
@@ -85,6 +86,7 @@ Nest `<datalist>` elements inside `<rich-input>` to configure keywords and autoc
|
|
|
85
86
|
<option value="Kranky"></option>
|
|
86
87
|
<option value="Madhouse Records"></option>
|
|
87
88
|
<option value="Ninja Tune"></option>
|
|
89
|
+
<option value="Warp Records"></option>
|
|
88
90
|
<option value="We Play House Recordings"></option>
|
|
89
91
|
<option value="XL Recordings"></option>
|
|
90
92
|
</datalist>
|
|
@@ -148,6 +150,10 @@ Datalists can be added, updated, or removed dynamically at runtime; `<rich-input
|
|
|
148
150
|
<img src="assets/ninja-tune.jpg" height="50" width="50" alt="Ninja Tune Logo">
|
|
149
151
|
Ninja Tune
|
|
150
152
|
</option>
|
|
153
|
+
<option value="Warp Records">
|
|
154
|
+
<img src="assets/warp-records.png" height="50" width="50" alt="Warp Records Logo">
|
|
155
|
+
Warp Records
|
|
156
|
+
</option>
|
|
151
157
|
<option value="We Play House Recordings">
|
|
152
158
|
<img src="assets/we-play-house-recordings.jpg" height="50" width="50" alt="We Play House Recordings Logo">
|
|
153
159
|
We Play House Recordings
|
|
@@ -244,7 +250,7 @@ In browsers without `OpaqueRange` that support the CSS Custom Highlight API (suc
|
|
|
244
250
|
Values corresponding to configured keywords are registered into the global `CSS.highlights` registry and styled using standard CSS `::highlight(keyword)` pseudo-elements in your stylesheet:
|
|
245
251
|
|
|
246
252
|
```css
|
|
247
|
-
/* Style the value set in label:"
|
|
253
|
+
/* Style the value set in label:"Warp Records" */
|
|
248
254
|
::highlight(label) {
|
|
249
255
|
background-color: oklch(0.92 0.08 240);
|
|
250
256
|
color: oklch(0.28 0.14 240);
|
|
@@ -268,6 +274,12 @@ Values corresponding to configured keywords are registered into the global `CSS.
|
|
|
268
274
|
color: #64748b;
|
|
269
275
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
270
276
|
}
|
|
277
|
+
|
|
278
|
+
/* Invalid highlight (squiggly underline for unrecognized keywords or values not present in datalist) */
|
|
279
|
+
::highlight(rich-input-invalid) {
|
|
280
|
+
text-decoration: underline wavy #ef4444;
|
|
281
|
+
text-decoration-skip-ink: none;
|
|
282
|
+
}
|
|
271
283
|
```
|
|
272
284
|
|
|
273
285
|
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.
|
|
@@ -275,7 +287,7 @@ In browsers using the `[contenteditable]` fallback inside Shadow DOM (such as Sa
|
|
|
275
287
|
For self-contained widgets or instance-specific style overrides, `<rich-input>` also supports an optional embedded `<style>` block as a direct child, which is automatically injected into the shadow root:
|
|
276
288
|
|
|
277
289
|
```html
|
|
278
|
-
<rich-input value='artist:"Aphex Twin" label:"
|
|
290
|
+
<rich-input value='artist:"Aphex Twin" label:"Warp Records"'>
|
|
279
291
|
<style>
|
|
280
292
|
::highlight(label) {
|
|
281
293
|
background-color: #dbeafe;
|
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, {
|
|
@@ -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.1",
|
|
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
|
@@ -52,6 +52,7 @@ class HighlightRegistryManager {
|
|
|
52
52
|
const rangesByKeyword = new Map();
|
|
53
53
|
const allKeywordRanges = [];
|
|
54
54
|
const allValueRanges = [];
|
|
55
|
+
const allInvalidRanges = [];
|
|
55
56
|
|
|
56
57
|
for (const inst of this.instances) {
|
|
57
58
|
const instanceRanges = inst.getActiveHighlightRanges();
|
|
@@ -72,6 +73,13 @@ class HighlightRegistryManager {
|
|
|
72
73
|
allKeywordRanges.push(...data.keywordRanges);
|
|
73
74
|
}
|
|
74
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
|
+
}
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
// 1. Set/update individual keyword highlights (e.g. ::highlight(label))
|
|
@@ -129,6 +137,27 @@ class HighlightRegistryManager {
|
|
|
129
137
|
} catch (e) {}
|
|
130
138
|
}
|
|
131
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
|
+
}
|
|
132
161
|
}
|
|
133
162
|
}
|
|
134
163
|
|
package/utils/query-parser.js
CHANGED
|
@@ -181,8 +181,8 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
181
181
|
// 1. Caret is within a keyword token
|
|
182
182
|
if (activeToken.type === 'keyword') {
|
|
183
183
|
if (caretPos <= activeToken.colonIndex) {
|
|
184
|
-
// User is editing the keyword name
|
|
185
|
-
const query =
|
|
184
|
+
// User is editing the keyword name (filter based on full keyword name, not caret position)
|
|
185
|
+
const query = activeToken.keyword;
|
|
186
186
|
return {
|
|
187
187
|
mode: 'keyword',
|
|
188
188
|
query,
|
|
@@ -193,17 +193,16 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
193
193
|
tokens,
|
|
194
194
|
};
|
|
195
195
|
} else {
|
|
196
|
-
// User is editing the keyword value
|
|
196
|
+
// User is editing the keyword value (filter based on full value string, not caret position)
|
|
197
197
|
const isQuoted = activeToken.quoted;
|
|
198
|
-
const
|
|
199
|
-
const innerEnd = activeToken.innerEnd;
|
|
200
|
-
const valuePrefix = inputStr.slice(innerStart, Math.min(caretPos, innerEnd));
|
|
198
|
+
const value = activeToken.innerValue;
|
|
201
199
|
|
|
202
200
|
return {
|
|
203
201
|
mode: 'value',
|
|
204
202
|
keyword: activeToken.keyword,
|
|
205
203
|
keywordLower: activeToken.keywordLower,
|
|
206
|
-
valuePrefix,
|
|
204
|
+
valuePrefix: value,
|
|
205
|
+
query: value,
|
|
207
206
|
quoted: isQuoted,
|
|
208
207
|
quoteChar: activeToken.quoteChar || '"',
|
|
209
208
|
isClosed: activeToken.isClosed,
|
|
@@ -216,9 +215,9 @@ export function getCaretContext(inputStr, caretPos, configuredKeywords) {
|
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
// 2. Caret is within a plain text token
|
|
218
|
+
// 2. Caret is within a plain text token (filter based on full word, not caret position)
|
|
220
219
|
if (activeToken.type === 'text') {
|
|
221
|
-
const query =
|
|
220
|
+
const query = activeToken.raw;
|
|
222
221
|
return {
|
|
223
222
|
mode: 'keyword',
|
|
224
223
|
query,
|
|
@@ -342,3 +341,25 @@ export function applySuggestion(inputStr, suggestion, context) {
|
|
|
342
341
|
const newCaret = before.length + insertText.length;
|
|
343
342
|
return { newValue, newCaret };
|
|
344
343
|
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Checks whether a given value matches any option in the keyword configuration.
|
|
347
|
+
* @param {Object} kwConfig
|
|
348
|
+
* @param {string} value
|
|
349
|
+
* @returns {boolean}
|
|
350
|
+
*/
|
|
351
|
+
export function isDatalistValue(kwConfig, value) {
|
|
352
|
+
if (!kwConfig || !Array.isArray(kwConfig.options) || kwConfig.options.length === 0) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
const val = (value ?? '').trim().toLowerCase();
|
|
356
|
+
if (!val) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
return kwConfig.options.some((opt) => {
|
|
360
|
+
const optVal = (opt.value ?? '').trim().toLowerCase();
|
|
361
|
+
const optLabel = (opt.label ?? '').trim().toLowerCase();
|
|
362
|
+
const optText = (opt.text ?? '').trim().toLowerCase();
|
|
363
|
+
return optVal === val || optLabel === val || optText === val;
|
|
364
|
+
});
|
|
365
|
+
}
|