rich-input 1.2.2 → 1.4.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 +127 -98
- package/assets/rich-input-parts.svg +92 -61
- package/components/rich-input.js +614 -54
- package/index.js +7 -1
- package/package.json +5 -2
- package/utils/highlights.js +75 -0
- package/utils/query-parser.js +379 -52
package/components/rich-input.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
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, isDatalistValue } from '../utils/query-parser.js';
|
|
6
|
+
import { DEFAULT_OPERATORS, normalizeOperators, DEFAULT_COMBINATORS, normalizeCombinators, DEFAULT_DELIMITERS, normalizeDelimiters, 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';
|
|
10
10
|
|
|
11
|
+
const KEYWORD_SUGGESTION_DELAY = 300;
|
|
12
|
+
|
|
11
13
|
const TEMPLATE = document.createElement('template');
|
|
12
14
|
TEMPLATE.innerHTML = `
|
|
13
15
|
<style>
|
|
@@ -57,25 +59,35 @@ TEMPLATE.innerHTML = `
|
|
|
57
59
|
cursor: not-allowed;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
.search-icon {
|
|
61
63
|
display: inline-flex;
|
|
62
64
|
align-items: center;
|
|
63
65
|
justify-content: center;
|
|
64
66
|
flex-shrink: 0;
|
|
67
|
+
width: 1.125rem;
|
|
68
|
+
height: 1.125rem;
|
|
65
69
|
margin-right: 0.5rem;
|
|
66
70
|
color: var(--ri-icon-color, var(--rs-icon-color, #9ca3af));
|
|
71
|
+
background-image: var(--_icon-default-bg, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%239ca3af'%3E%3Cpath fill-rule='evenodd' d='M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z' clip-rule='evenodd'/%3E%3C/svg%3E"));
|
|
72
|
+
background-repeat: no-repeat;
|
|
73
|
+
background-position: center;
|
|
74
|
+
background-size: contain;
|
|
75
|
+
font-size: 1rem;
|
|
76
|
+
line-height: 1;
|
|
77
|
+
pointer-events: none;
|
|
78
|
+
user-select: none;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
.search-icon {
|
|
70
|
-
|
|
71
|
-
height: 1.125rem;
|
|
72
|
-
color: inherit;
|
|
73
|
-
pointer-events: none;
|
|
81
|
+
.search-icon[data-has-content] {
|
|
82
|
+
background-image: none;
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
.search-icon[data-content-none] {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.search-icon::before {
|
|
90
|
+
content: var(--_icon-content, none);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
.search-input {
|
|
@@ -113,14 +125,32 @@ TEMPLATE.innerHTML = `
|
|
|
113
125
|
display: inline-block;
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
/* Generic prefix highlight for operators */
|
|
129
|
+
:host::highlight(rich-input-operator) {
|
|
130
|
+
color: var(--ri-operator-color, var(--ri-keyword-color, #64748b));
|
|
131
|
+
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Generic highlight for combinators */
|
|
135
|
+
:host::highlight(rich-input-combinator) {
|
|
136
|
+
color: var(--ri-combinator-color, var(--ri-operator-color, var(--ri-keyword-color, #64748b)));
|
|
137
|
+
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Generic highlight for delimiters */
|
|
141
|
+
:host::highlight(rich-input-delimiter) {
|
|
142
|
+
color: var(--ri-delimiter-color, var(--ri-operator-color, var(--ri-keyword-color, #64748b)));
|
|
143
|
+
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/* Generic prefix highlight for keywords */
|
|
117
|
-
::highlight(rich-input-keyword) {
|
|
147
|
+
:host::highlight(rich-input-keyword) {
|
|
118
148
|
color: var(--ri-keyword-color, #64748b);
|
|
119
149
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
/* Squiggly line underneath invalid keyword values */
|
|
123
|
-
::highlight(rich-input-invalid) {
|
|
153
|
+
:host::highlight(rich-input-invalid) {
|
|
124
154
|
text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
125
155
|
-webkit-text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
126
156
|
text-decoration-line: underline;
|
|
@@ -132,12 +162,6 @@ TEMPLATE.innerHTML = `
|
|
|
132
162
|
text-decoration-skip-ink: none;
|
|
133
163
|
}
|
|
134
164
|
|
|
135
|
-
/* Visually hide datalists and custom style tags in slot */
|
|
136
|
-
::slotted(datalist),
|
|
137
|
-
::slotted(style) {
|
|
138
|
-
display: none !important;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
165
|
.clear-button {
|
|
142
166
|
flex-shrink: 0;
|
|
143
167
|
display: inline-flex;
|
|
@@ -180,6 +204,7 @@ TEMPLATE.innerHTML = `
|
|
|
180
204
|
max-height: 290px;
|
|
181
205
|
overflow-y: auto;
|
|
182
206
|
background-color: var(--ri-popover-bg, var(--rs-popover-bg, #ffffff));
|
|
207
|
+
color: var(--ri-item-title-color, var(--rs-item-title-color, #0f172a));
|
|
183
208
|
border: 1px solid var(--ri-popover-border, var(--rs-popover-border, #e2e8f0));
|
|
184
209
|
border-radius: var(--ri-popover-radius, var(--rs-popover-radius, 8px));
|
|
185
210
|
box-shadow: var(--ri-popover-shadow, var(--rs-popover-shadow, 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)));
|
|
@@ -221,7 +246,11 @@ TEMPLATE.innerHTML = `
|
|
|
221
246
|
line-height: 1.4;
|
|
222
247
|
transition: background-color 0.1s ease;
|
|
223
248
|
user-select: none;
|
|
224
|
-
color: var(--ri-item-title-color, var(--rs-item-title-color,
|
|
249
|
+
color: var(--ri-item-title-color, var(--rs-item-title-color, inherit));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.suggestion-item.selected {
|
|
253
|
+
background-color: var(--ri-item-selected-bg, rgba(37, 99, 235, 0.06));
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
.suggestion-item:hover,
|
|
@@ -239,7 +268,7 @@ TEMPLATE.innerHTML = `
|
|
|
239
268
|
|
|
240
269
|
.suggestion-title {
|
|
241
270
|
font-weight: 600;
|
|
242
|
-
color: var(--ri-item-title-color, var(--rs-item-title-color,
|
|
271
|
+
color: var(--ri-item-title-color, var(--rs-item-title-color, inherit));
|
|
243
272
|
white-space: nowrap;
|
|
244
273
|
overflow: hidden;
|
|
245
274
|
text-overflow: ellipsis;
|
|
@@ -260,19 +289,16 @@ TEMPLATE.innerHTML = `
|
|
|
260
289
|
vertical-align: middle;
|
|
261
290
|
}
|
|
262
291
|
|
|
263
|
-
/* Visually hide datalists in slot */
|
|
264
|
-
::slotted(datalist)
|
|
292
|
+
/* Visually hide datalists and custom style tags in slot */
|
|
293
|
+
::slotted(datalist),
|
|
294
|
+
::slotted(style) {
|
|
265
295
|
display: none !important;
|
|
266
296
|
}
|
|
267
297
|
</style>
|
|
268
298
|
|
|
269
299
|
<div part="wrapper" class="wrapper">
|
|
270
300
|
<div part="control" class="control">
|
|
271
|
-
<
|
|
272
|
-
<svg part="icon" class="search-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
273
|
-
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
|
274
|
-
</svg>
|
|
275
|
-
</slot>
|
|
301
|
+
<span part="icon" class="search-icon" aria-hidden="true"></span>
|
|
276
302
|
<input
|
|
277
303
|
part="input"
|
|
278
304
|
class="search-input"
|
|
@@ -290,7 +316,6 @@ TEMPLATE.innerHTML = `
|
|
|
290
316
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
|
|
291
317
|
</svg>
|
|
292
318
|
</button>
|
|
293
|
-
<slot name="trailing"></slot>
|
|
294
319
|
</div>
|
|
295
320
|
|
|
296
321
|
<div
|
|
@@ -356,6 +381,79 @@ function syncDocumentHighlightStyles(shadowRoot) {
|
|
|
356
381
|
export class RichInput extends HTMLElement {
|
|
357
382
|
static formAssociated = true;
|
|
358
383
|
|
|
384
|
+
static _globalOperators = [...DEFAULT_OPERATORS];
|
|
385
|
+
static _globalCombinators = [...DEFAULT_COMBINATORS];
|
|
386
|
+
static _globalDelimiters = [...DEFAULT_DELIMITERS];
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Global default operators inherited by newly created <rich-input> instances.
|
|
390
|
+
*/
|
|
391
|
+
static get operators() {
|
|
392
|
+
return [...RichInput._globalOperators];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
static set operators(val) {
|
|
396
|
+
if (val === null || val === undefined) {
|
|
397
|
+
RichInput._globalOperators = [...DEFAULT_OPERATORS];
|
|
398
|
+
} else {
|
|
399
|
+
RichInput._globalOperators = normalizeOperators(val);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
static get defaultOperators() {
|
|
404
|
+
return RichInput.operators;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
static set defaultOperators(val) {
|
|
408
|
+
RichInput.operators = val;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Global default combinators inherited by newly created <rich-input> instances.
|
|
413
|
+
*/
|
|
414
|
+
static get combinators() {
|
|
415
|
+
return [...RichInput._globalCombinators];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static set combinators(val) {
|
|
419
|
+
if (val === null || val === undefined) {
|
|
420
|
+
RichInput._globalCombinators = [...DEFAULT_COMBINATORS];
|
|
421
|
+
} else {
|
|
422
|
+
RichInput._globalCombinators = normalizeCombinators(val);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
static get defaultCombinators() {
|
|
427
|
+
return RichInput.combinators;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
static set defaultCombinators(val) {
|
|
431
|
+
RichInput.combinators = val;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Global default delimiters inherited by newly created <rich-input> instances.
|
|
436
|
+
*/
|
|
437
|
+
static get delimiters() {
|
|
438
|
+
return [...RichInput._globalDelimiters];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
static set delimiters(val) {
|
|
442
|
+
if (val === null || val === undefined) {
|
|
443
|
+
RichInput._globalDelimiters = [...DEFAULT_DELIMITERS];
|
|
444
|
+
} else {
|
|
445
|
+
RichInput._globalDelimiters = normalizeDelimiters(val);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
static get defaultDelimiters() {
|
|
450
|
+
return RichInput.delimiters;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static set defaultDelimiters(val) {
|
|
454
|
+
RichInput.delimiters = val;
|
|
455
|
+
}
|
|
456
|
+
|
|
359
457
|
static get observedAttributes() {
|
|
360
458
|
return [
|
|
361
459
|
'value',
|
|
@@ -366,6 +464,9 @@ export class RichInput extends HTMLElement {
|
|
|
366
464
|
'autofocus',
|
|
367
465
|
'required',
|
|
368
466
|
'highlight-quotes',
|
|
467
|
+
'operators',
|
|
468
|
+
'combinators',
|
|
469
|
+
'delimiters',
|
|
369
470
|
'aria-label',
|
|
370
471
|
'aria-labelledby',
|
|
371
472
|
];
|
|
@@ -403,14 +504,26 @@ export class RichInput extends HTMLElement {
|
|
|
403
504
|
this._popoverTitle = this.shadowRoot.querySelector('.popover-title');
|
|
404
505
|
this._suggestionsList = this.shadowRoot.querySelector('.suggestions-list');
|
|
405
506
|
this._clearBtn = this.shadowRoot.querySelector('.clear-button');
|
|
507
|
+
this._icon = this.shadowRoot.querySelector('.search-icon');
|
|
406
508
|
this._slot = this.shadowRoot.querySelector('slot:not([name])');
|
|
509
|
+
this._lastIconColor = null;
|
|
407
510
|
|
|
408
511
|
this._configuredKeywords = new Map();
|
|
512
|
+
this._operators = [...RichInput.operators];
|
|
513
|
+
this._settingOperatorsAttribute = false;
|
|
514
|
+
this._combinators = [...RichInput.combinators];
|
|
515
|
+
this._settingCombinatorsAttribute = false;
|
|
516
|
+
this._delimiters = [...RichInput.delimiters];
|
|
517
|
+
this._settingDelimitersAttribute = false;
|
|
409
518
|
this._activeSuggestions = [];
|
|
410
519
|
this._selectedIndex = -1;
|
|
411
520
|
this._context = null;
|
|
521
|
+
this._suggestionTimeout = null;
|
|
412
522
|
this._ownedRanges = [];
|
|
413
523
|
this._invalidRanges = [];
|
|
524
|
+
this._operatorRanges = [];
|
|
525
|
+
this._combinatorRanges = [];
|
|
526
|
+
this._delimiterRanges = [];
|
|
414
527
|
this._activeKeywordHighlightMap = new Map();
|
|
415
528
|
this._isFocused = false;
|
|
416
529
|
this._lastCaretPosition = -1;
|
|
@@ -432,6 +545,10 @@ export class RichInput extends HTMLElement {
|
|
|
432
545
|
|
|
433
546
|
connectedCallback() {
|
|
434
547
|
this._syncInjectedStyles();
|
|
548
|
+
this._syncIconStyle();
|
|
549
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
550
|
+
requestAnimationFrame(() => this._syncIconStyle());
|
|
551
|
+
}
|
|
435
552
|
syncDocumentHighlightStyles(this.shadowRoot);
|
|
436
553
|
highlightManager.register(this);
|
|
437
554
|
|
|
@@ -448,9 +565,15 @@ export class RichInput extends HTMLElement {
|
|
|
448
565
|
|
|
449
566
|
// Listen to changes on light DOM datalists and style tags
|
|
450
567
|
this._slot.addEventListener('slotchange', this._onSlotChange);
|
|
451
|
-
this._mutationObserver = new MutationObserver(() => {
|
|
568
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
569
|
+
this._syncIconStyle();
|
|
570
|
+
const hasLightDomChange = mutations.some(
|
|
571
|
+
(m) => !(m.target === this && m.type === 'attributes')
|
|
572
|
+
);
|
|
573
|
+
if (!hasLightDomChange) return;
|
|
452
574
|
this._loadDatalists();
|
|
453
575
|
this._syncInjectedStyles();
|
|
576
|
+
this._syncIconStyle();
|
|
454
577
|
this.updateHighlights();
|
|
455
578
|
});
|
|
456
579
|
this._mutationObserver.observe(this, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
@@ -472,6 +595,48 @@ export class RichInput extends HTMLElement {
|
|
|
472
595
|
window.addEventListener('scroll', this._onGlobalResizeOrScroll, { passive: true });
|
|
473
596
|
|
|
474
597
|
// Sync initial attributes
|
|
598
|
+
if (this.hasAttribute('operators')) {
|
|
599
|
+
const rawOps = this.getAttribute('operators');
|
|
600
|
+
this._operators = normalizeOperators(rawOps);
|
|
601
|
+
const normalizedAttr = this._operators.join(' ');
|
|
602
|
+
if (rawOps !== normalizedAttr) {
|
|
603
|
+
this._settingOperatorsAttribute = true;
|
|
604
|
+
this.setAttribute('operators', normalizedAttr);
|
|
605
|
+
this._settingOperatorsAttribute = false;
|
|
606
|
+
}
|
|
607
|
+
} else if (this._operators.length > 0) {
|
|
608
|
+
this._settingOperatorsAttribute = true;
|
|
609
|
+
this.setAttribute('operators', this._operators.join(' '));
|
|
610
|
+
this._settingOperatorsAttribute = false;
|
|
611
|
+
}
|
|
612
|
+
if (this.hasAttribute('combinators')) {
|
|
613
|
+
const rawCombs = this.getAttribute('combinators');
|
|
614
|
+
this._combinators = normalizeCombinators(rawCombs);
|
|
615
|
+
const normalizedAttr = this._combinators.join(' ');
|
|
616
|
+
if (rawCombs !== normalizedAttr) {
|
|
617
|
+
this._settingCombinatorsAttribute = true;
|
|
618
|
+
this.setAttribute('combinators', normalizedAttr);
|
|
619
|
+
this._settingCombinatorsAttribute = false;
|
|
620
|
+
}
|
|
621
|
+
} else if (this._combinators.length > 0) {
|
|
622
|
+
this._settingCombinatorsAttribute = true;
|
|
623
|
+
this.setAttribute('combinators', this._combinators.join(' '));
|
|
624
|
+
this._settingCombinatorsAttribute = false;
|
|
625
|
+
}
|
|
626
|
+
if (this.hasAttribute('delimiters')) {
|
|
627
|
+
const rawDelims = this.getAttribute('delimiters');
|
|
628
|
+
this._delimiters = normalizeDelimiters(rawDelims);
|
|
629
|
+
const normalizedAttr = this._delimiters.join(' ');
|
|
630
|
+
if (rawDelims !== normalizedAttr) {
|
|
631
|
+
this._settingDelimitersAttribute = true;
|
|
632
|
+
this.setAttribute('delimiters', normalizedAttr);
|
|
633
|
+
this._settingDelimitersAttribute = false;
|
|
634
|
+
}
|
|
635
|
+
} else if (this._delimiters.length > 0) {
|
|
636
|
+
this._settingDelimitersAttribute = true;
|
|
637
|
+
this.setAttribute('delimiters', this._delimiters.join(' '));
|
|
638
|
+
this._settingDelimitersAttribute = false;
|
|
639
|
+
}
|
|
475
640
|
if (this.hasAttribute('value')) {
|
|
476
641
|
this._input.value = this.getAttribute('value');
|
|
477
642
|
}
|
|
@@ -547,6 +712,72 @@ export class RichInput extends HTMLElement {
|
|
|
547
712
|
}
|
|
548
713
|
} else if (name === 'highlight-quotes') {
|
|
549
714
|
this.updateHighlights();
|
|
715
|
+
} else if (name === 'operators') {
|
|
716
|
+
if (this._settingOperatorsAttribute) return;
|
|
717
|
+
if (newValue === null) {
|
|
718
|
+
this._operators = [...RichInput.operators];
|
|
719
|
+
if (this._operators.length > 0) {
|
|
720
|
+
this._settingOperatorsAttribute = true;
|
|
721
|
+
this.setAttribute('operators', this._operators.join(' '));
|
|
722
|
+
this._settingOperatorsAttribute = false;
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
this._operators = normalizeOperators(newValue);
|
|
726
|
+
const normalizedAttr = this._operators.join(' ');
|
|
727
|
+
if (newValue !== normalizedAttr) {
|
|
728
|
+
this._settingOperatorsAttribute = true;
|
|
729
|
+
this.setAttribute('operators', normalizedAttr);
|
|
730
|
+
this._settingOperatorsAttribute = false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
this.updateHighlights();
|
|
734
|
+
if (this._isPopoverOpen()) {
|
|
735
|
+
this.updateSuggestions('operators-changed');
|
|
736
|
+
}
|
|
737
|
+
} else if (name === 'combinators') {
|
|
738
|
+
if (this._settingCombinatorsAttribute) return;
|
|
739
|
+
if (newValue === null) {
|
|
740
|
+
this._combinators = [...RichInput.combinators];
|
|
741
|
+
if (this._combinators.length > 0) {
|
|
742
|
+
this._settingCombinatorsAttribute = true;
|
|
743
|
+
this.setAttribute('combinators', this._combinators.join(' '));
|
|
744
|
+
this._settingCombinatorsAttribute = false;
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
this._combinators = normalizeCombinators(newValue);
|
|
748
|
+
const normalizedAttr = this._combinators.join(' ');
|
|
749
|
+
if (newValue !== normalizedAttr) {
|
|
750
|
+
this._settingCombinatorsAttribute = true;
|
|
751
|
+
this.setAttribute('combinators', normalizedAttr);
|
|
752
|
+
this._settingCombinatorsAttribute = false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.updateHighlights();
|
|
756
|
+
if (this._isPopoverOpen()) {
|
|
757
|
+
this.updateSuggestions('combinators-changed');
|
|
758
|
+
}
|
|
759
|
+
} else if (name === 'delimiters') {
|
|
760
|
+
if (this._settingDelimitersAttribute) return;
|
|
761
|
+
if (newValue === null) {
|
|
762
|
+
this._delimiters = [...RichInput.delimiters];
|
|
763
|
+
if (this._delimiters.length > 0) {
|
|
764
|
+
this._settingDelimitersAttribute = true;
|
|
765
|
+
this.setAttribute('delimiters', this._delimiters.join(' '));
|
|
766
|
+
this._settingDelimitersAttribute = false;
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
this._delimiters = normalizeDelimiters(newValue);
|
|
770
|
+
const normalizedAttr = this._delimiters.join(' ');
|
|
771
|
+
if (newValue !== normalizedAttr) {
|
|
772
|
+
this._settingDelimitersAttribute = true;
|
|
773
|
+
this.setAttribute('delimiters', normalizedAttr);
|
|
774
|
+
this._settingDelimitersAttribute = false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
this.updateHighlights();
|
|
778
|
+
if (this._isPopoverOpen()) {
|
|
779
|
+
this.updateSuggestions('delimiters-changed');
|
|
780
|
+
}
|
|
550
781
|
}
|
|
551
782
|
}
|
|
552
783
|
|
|
@@ -662,6 +893,108 @@ export class RichInput extends HTMLElement {
|
|
|
662
893
|
this._input.selectionDirection = val;
|
|
663
894
|
}
|
|
664
895
|
|
|
896
|
+
get operators() {
|
|
897
|
+
return [...this._operators];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
set operators(val) {
|
|
901
|
+
if (val === null || val === undefined) {
|
|
902
|
+
this._operators = [...RichInput.operators];
|
|
903
|
+
this._settingOperatorsAttribute = true;
|
|
904
|
+
if (this._operators.length > 0) {
|
|
905
|
+
this.setAttribute('operators', this._operators.join(' '));
|
|
906
|
+
} else {
|
|
907
|
+
this.removeAttribute('operators');
|
|
908
|
+
}
|
|
909
|
+
this._settingOperatorsAttribute = false;
|
|
910
|
+
} else {
|
|
911
|
+
this._operators = normalizeOperators(val);
|
|
912
|
+
this._settingOperatorsAttribute = true;
|
|
913
|
+
this.setAttribute('operators', this._operators.join(' '));
|
|
914
|
+
this._settingOperatorsAttribute = false;
|
|
915
|
+
}
|
|
916
|
+
this.updateHighlights();
|
|
917
|
+
if (this._isPopoverOpen()) {
|
|
918
|
+
this.updateSuggestions('operators-changed');
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
getOperators() {
|
|
923
|
+
return this.operators;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
setOperators(val) {
|
|
927
|
+
this.operators = val;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
get combinators() {
|
|
931
|
+
return [...this._combinators];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
set combinators(val) {
|
|
935
|
+
if (val === null || val === undefined) {
|
|
936
|
+
this._combinators = [...RichInput.combinators];
|
|
937
|
+
this._settingCombinatorsAttribute = true;
|
|
938
|
+
if (this._combinators.length > 0) {
|
|
939
|
+
this.setAttribute('combinators', this._combinators.join(' '));
|
|
940
|
+
} else {
|
|
941
|
+
this.removeAttribute('combinators');
|
|
942
|
+
}
|
|
943
|
+
this._settingCombinatorsAttribute = false;
|
|
944
|
+
} else {
|
|
945
|
+
this._combinators = normalizeCombinators(val);
|
|
946
|
+
this._settingCombinatorsAttribute = true;
|
|
947
|
+
this.setAttribute('combinators', this._combinators.join(' '));
|
|
948
|
+
this._settingCombinatorsAttribute = false;
|
|
949
|
+
}
|
|
950
|
+
this.updateHighlights();
|
|
951
|
+
if (this._isPopoverOpen()) {
|
|
952
|
+
this.updateSuggestions('combinators-changed');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
getCombinators() {
|
|
957
|
+
return this.combinators;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
setCombinators(val) {
|
|
961
|
+
this.combinators = val;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
get delimiters() {
|
|
965
|
+
return [...this._delimiters];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
set delimiters(val) {
|
|
969
|
+
if (val === null || val === undefined) {
|
|
970
|
+
this._delimiters = [...RichInput.delimiters];
|
|
971
|
+
this._settingDelimitersAttribute = true;
|
|
972
|
+
if (this._delimiters.length > 0) {
|
|
973
|
+
this.setAttribute('delimiters', this._delimiters.join(' '));
|
|
974
|
+
} else {
|
|
975
|
+
this.removeAttribute('delimiters');
|
|
976
|
+
}
|
|
977
|
+
this._settingDelimitersAttribute = false;
|
|
978
|
+
} else {
|
|
979
|
+
this._delimiters = normalizeDelimiters(val);
|
|
980
|
+
this._settingDelimitersAttribute = true;
|
|
981
|
+
this.setAttribute('delimiters', this._delimiters.join(' '));
|
|
982
|
+
this._settingDelimitersAttribute = false;
|
|
983
|
+
}
|
|
984
|
+
this.updateHighlights();
|
|
985
|
+
if (this._isPopoverOpen()) {
|
|
986
|
+
this.updateSuggestions('delimiters-changed');
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
getDelimiters() {
|
|
991
|
+
return this.delimiters;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
setDelimiters(val) {
|
|
995
|
+
this.delimiters = val;
|
|
996
|
+
}
|
|
997
|
+
|
|
665
998
|
// --- Public Methods ---
|
|
666
999
|
focus(options) {
|
|
667
1000
|
this._isFocused = true;
|
|
@@ -682,7 +1015,7 @@ export class RichInput extends HTMLElement {
|
|
|
682
1015
|
}
|
|
683
1016
|
|
|
684
1017
|
getParsedQuery() {
|
|
685
|
-
return parseSearchQuery(this._input.value);
|
|
1018
|
+
return parseSearchQuery(this._input.value, this.operators, this.combinators, this.delimiters);
|
|
686
1019
|
}
|
|
687
1020
|
|
|
688
1021
|
getKeywords() {
|
|
@@ -769,9 +1102,66 @@ export class RichInput extends HTMLElement {
|
|
|
769
1102
|
_onSlotChange() {
|
|
770
1103
|
this._loadDatalists();
|
|
771
1104
|
this._syncInjectedStyles();
|
|
1105
|
+
this._syncIconStyle();
|
|
772
1106
|
this.updateHighlights();
|
|
773
1107
|
}
|
|
774
1108
|
|
|
1109
|
+
_syncIconStyle() {
|
|
1110
|
+
if (!this._icon || typeof window === 'undefined' || typeof getComputedStyle !== 'function') return;
|
|
1111
|
+
|
|
1112
|
+
const cs = getComputedStyle(this._icon);
|
|
1113
|
+
const content = cs.content;
|
|
1114
|
+
const color = cs.color || '#9ca3af';
|
|
1115
|
+
|
|
1116
|
+
if (this._lastIconColor !== color) {
|
|
1117
|
+
this._lastIconColor = color;
|
|
1118
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${color}"><path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"/></svg>`;
|
|
1119
|
+
this._icon.style.setProperty('--_icon-default-bg', `url("data:image/svg+xml,${encodeURIComponent(svg)}")`);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (content === 'none') {
|
|
1123
|
+
if (!this._icon.hasAttribute('data-has-content')) {
|
|
1124
|
+
this._icon.setAttribute('data-has-content', '');
|
|
1125
|
+
}
|
|
1126
|
+
if (!this._icon.hasAttribute('data-content-none')) {
|
|
1127
|
+
this._icon.setAttribute('data-content-none', '');
|
|
1128
|
+
}
|
|
1129
|
+
this._icon.style.removeProperty('--_icon-content');
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (this._icon.hasAttribute('data-content-none')) {
|
|
1134
|
+
this._icon.removeAttribute('data-content-none');
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (content && content !== 'normal') {
|
|
1138
|
+
if (!this._icon.hasAttribute('data-has-content')) {
|
|
1139
|
+
this._icon.setAttribute('data-has-content', '');
|
|
1140
|
+
}
|
|
1141
|
+
if (content.startsWith('"') || content.startsWith("'")) {
|
|
1142
|
+
if (this._icon.style.getPropertyValue('--_icon-content') !== content) {
|
|
1143
|
+
this._icon.style.setProperty('--_icon-content', content);
|
|
1144
|
+
}
|
|
1145
|
+
} else {
|
|
1146
|
+
this._icon.style.removeProperty('--_icon-content');
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (this._icon.style.getPropertyValue('--_icon-content')) {
|
|
1152
|
+
this._icon.style.removeProperty('--_icon-content');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const beforeContent = getComputedStyle(this._icon, '::before').content;
|
|
1156
|
+
if (beforeContent && beforeContent !== 'none' && beforeContent !== 'normal') {
|
|
1157
|
+
if (!this._icon.hasAttribute('data-has-content')) {
|
|
1158
|
+
this._icon.setAttribute('data-has-content', '');
|
|
1159
|
+
}
|
|
1160
|
+
} else if (this._icon.hasAttribute('data-has-content')) {
|
|
1161
|
+
this._icon.removeAttribute('data-has-content');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
775
1165
|
// --- Public Properties ---
|
|
776
1166
|
get inputElement() {
|
|
777
1167
|
return this._input;
|
|
@@ -786,6 +1176,9 @@ export class RichInput extends HTMLElement {
|
|
|
786
1176
|
}
|
|
787
1177
|
this._ownedRanges = [];
|
|
788
1178
|
this._invalidRanges = [];
|
|
1179
|
+
this._operatorRanges = [];
|
|
1180
|
+
this._combinatorRanges = [];
|
|
1181
|
+
this._delimiterRanges = [];
|
|
789
1182
|
this._activeKeywordHighlightMap.clear();
|
|
790
1183
|
}
|
|
791
1184
|
|
|
@@ -808,7 +1201,7 @@ export class RichInput extends HTMLElement {
|
|
|
808
1201
|
return;
|
|
809
1202
|
}
|
|
810
1203
|
|
|
811
|
-
const tokens = parseSearchTokens(text);
|
|
1204
|
+
const tokens = parseSearchTokens(text, this.operators, this.combinators, this.delimiters);
|
|
812
1205
|
const highlightQuotes = this.getAttribute('highlight-quotes') !== 'exclude';
|
|
813
1206
|
|
|
814
1207
|
const isFocused = this._isFocused;
|
|
@@ -832,6 +1225,15 @@ export class RichInput extends HTMLElement {
|
|
|
832
1225
|
|
|
833
1226
|
const bucket = this._activeKeywordHighlightMap.get(kw);
|
|
834
1227
|
|
|
1228
|
+
// 0. Operator prefix range (for ::highlight(rich-input-operator))
|
|
1229
|
+
if (token.operator && token.operatorEnd > token.operatorStart && token.operatorEnd <= text.length) {
|
|
1230
|
+
try {
|
|
1231
|
+
const opRange = this._input.createValueRange(token.operatorStart, token.operatorEnd);
|
|
1232
|
+
this._ownedRanges.push(opRange);
|
|
1233
|
+
this._operatorRanges.push(opRange);
|
|
1234
|
+
} catch (e) {}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
835
1237
|
// 1. Value range
|
|
836
1238
|
const start = highlightQuotes ? token.valueStart : token.innerStart;
|
|
837
1239
|
const end = highlightQuotes ? token.valueEnd : token.innerEnd;
|
|
@@ -890,6 +1292,39 @@ export class RichInput extends HTMLElement {
|
|
|
890
1292
|
}
|
|
891
1293
|
}
|
|
892
1294
|
}
|
|
1295
|
+
} else if (token.type === 'combinator') {
|
|
1296
|
+
if (token.end > token.start && token.end <= text.length) {
|
|
1297
|
+
try {
|
|
1298
|
+
const combRange = this._input.createValueRange(token.start, token.end);
|
|
1299
|
+
this._ownedRanges.push(combRange);
|
|
1300
|
+
this._combinatorRanges.push(combRange);
|
|
1301
|
+
} catch (e) {}
|
|
1302
|
+
}
|
|
1303
|
+
} else if (token.type === 'delimiter') {
|
|
1304
|
+
if (token.end > token.start && token.end <= text.length) {
|
|
1305
|
+
try {
|
|
1306
|
+
const delimRange = this._input.createValueRange(token.start, token.end);
|
|
1307
|
+
this._ownedRanges.push(delimRange);
|
|
1308
|
+
this._delimiterRanges.push(delimRange);
|
|
1309
|
+
} catch (e) {}
|
|
1310
|
+
}
|
|
1311
|
+
} else if (token.type === 'text' && token.operator) {
|
|
1312
|
+
// Highlight operator while typing an operator prefix before colon (e.g. "-" or "-sty")
|
|
1313
|
+
const remainder = token.raw.slice(token.operator.length).toLowerCase();
|
|
1314
|
+
const matchesKeywordPrefix =
|
|
1315
|
+
remainder.length === 0 ||
|
|
1316
|
+
Array.from(this._configuredKeywords.keys()).some((k) => k.startsWith(remainder));
|
|
1317
|
+
if (
|
|
1318
|
+
matchesKeywordPrefix &&
|
|
1319
|
+
token.operatorEnd > token.operatorStart &&
|
|
1320
|
+
token.operatorEnd <= text.length
|
|
1321
|
+
) {
|
|
1322
|
+
try {
|
|
1323
|
+
const opRange = this._input.createValueRange(token.operatorStart, token.operatorEnd);
|
|
1324
|
+
this._ownedRanges.push(opRange);
|
|
1325
|
+
this._operatorRanges.push(opRange);
|
|
1326
|
+
} catch (e) {}
|
|
1327
|
+
}
|
|
893
1328
|
}
|
|
894
1329
|
}
|
|
895
1330
|
|
|
@@ -900,6 +1335,18 @@ export class RichInput extends HTMLElement {
|
|
|
900
1335
|
return this._activeKeywordHighlightMap;
|
|
901
1336
|
}
|
|
902
1337
|
|
|
1338
|
+
getActiveOperatorRanges() {
|
|
1339
|
+
return this._operatorRanges;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
getActiveCombinatorRanges() {
|
|
1343
|
+
return this._combinatorRanges;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
getActiveDelimiterRanges() {
|
|
1347
|
+
return this._delimiterRanges;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
903
1350
|
getActiveInvalidRanges() {
|
|
904
1351
|
return this._invalidRanges;
|
|
905
1352
|
}
|
|
@@ -949,8 +1396,9 @@ export class RichInput extends HTMLElement {
|
|
|
949
1396
|
}
|
|
950
1397
|
}
|
|
951
1398
|
|
|
952
|
-
// Enter when popover closed triggers search/change
|
|
1399
|
+
// Enter when popover closed (or no suggestion selected) triggers search/change
|
|
953
1400
|
if (e.key === 'Enter') {
|
|
1401
|
+
this.hideSuggestions();
|
|
954
1402
|
this.dispatchEvent(new CustomEvent('search', {
|
|
955
1403
|
bubbles: true,
|
|
956
1404
|
composed: true,
|
|
@@ -971,15 +1419,16 @@ export class RichInput extends HTMLElement {
|
|
|
971
1419
|
this._isFocused = true;
|
|
972
1420
|
this._lastCaretPosition = this._input.selectionStart;
|
|
973
1421
|
this.updateHighlights();
|
|
974
|
-
|
|
975
|
-
if (this._input.value.length > 0) {
|
|
976
|
-
this.updateSuggestions('focus');
|
|
977
|
-
}
|
|
1422
|
+
this.updateSuggestions('focus');
|
|
978
1423
|
}
|
|
979
1424
|
|
|
980
1425
|
_onBlur() {
|
|
981
1426
|
this._isFocused = false;
|
|
982
1427
|
this._lastCaretPosition = -1;
|
|
1428
|
+
if (this._suggestionTimeout) {
|
|
1429
|
+
clearTimeout(this._suggestionTimeout);
|
|
1430
|
+
this._suggestionTimeout = null;
|
|
1431
|
+
}
|
|
983
1432
|
this.updateHighlights();
|
|
984
1433
|
// Delay closing so click events on popover items can fire
|
|
985
1434
|
setTimeout(() => {
|
|
@@ -1012,7 +1461,7 @@ export class RichInput extends HTMLElement {
|
|
|
1012
1461
|
_onClearClick() {
|
|
1013
1462
|
this.value = '';
|
|
1014
1463
|
this._input.focus();
|
|
1015
|
-
this.
|
|
1464
|
+
this.updateSuggestions('clear');
|
|
1016
1465
|
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
1017
1466
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
1018
1467
|
}
|
|
@@ -1087,8 +1536,13 @@ export class RichInput extends HTMLElement {
|
|
|
1087
1536
|
}
|
|
1088
1537
|
|
|
1089
1538
|
updateSuggestions(trigger = 'input') {
|
|
1539
|
+
if (this._suggestionTimeout) {
|
|
1540
|
+
clearTimeout(this._suggestionTimeout);
|
|
1541
|
+
this._suggestionTimeout = null;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1090
1544
|
const caretPos = this._input.selectionStart;
|
|
1091
|
-
const context = getCaretContext(this._input.value, caretPos, this._configuredKeywords);
|
|
1545
|
+
const context = getCaretContext(this._input.value, caretPos, this._configuredKeywords, this.operators, this.combinators, this.delimiters);
|
|
1092
1546
|
|
|
1093
1547
|
// If trigger is arrow and context is 'none', allow suggesting all keywords
|
|
1094
1548
|
if (trigger === 'arrow' && context.mode === 'none') {
|
|
@@ -1099,15 +1553,69 @@ export class RichInput extends HTMLElement {
|
|
|
1099
1553
|
}
|
|
1100
1554
|
|
|
1101
1555
|
this._context = context;
|
|
1102
|
-
const suggestions = getSuggestions(context, this._configuredKeywords);
|
|
1556
|
+
const suggestions = getSuggestions(context, this._configuredKeywords, this.combinators);
|
|
1103
1557
|
|
|
1104
1558
|
if (suggestions.length === 0) {
|
|
1105
1559
|
this.hideSuggestions();
|
|
1106
1560
|
return;
|
|
1107
1561
|
}
|
|
1108
1562
|
|
|
1563
|
+
// Value suggestions always show immediately.
|
|
1564
|
+
// Keyword suggestions show immediately when:
|
|
1565
|
+
// - focusing/clearing an empty field
|
|
1566
|
+
// - typing characters that match a keyword (Boolean(context.query))
|
|
1567
|
+
// - typing an operator (Boolean(context.operator))
|
|
1568
|
+
// - explicitly triggered via the Down Arrow key
|
|
1569
|
+
// In other cases (at the start of a new token after a space when the field is not empty),
|
|
1570
|
+
// show after a small delay to prevent constant popups when typing regular sentences with spaces.
|
|
1571
|
+
const isEmptyField = this._input.value.length === 0;
|
|
1572
|
+
const shouldShowImmediately =
|
|
1573
|
+
context.mode === 'value' ||
|
|
1574
|
+
isEmptyField ||
|
|
1575
|
+
Boolean(context.query) ||
|
|
1576
|
+
Boolean(context.operator) ||
|
|
1577
|
+
trigger === 'arrow';
|
|
1578
|
+
|
|
1579
|
+
if (shouldShowImmediately) {
|
|
1580
|
+
this._showSuggestions(suggestions, context, trigger);
|
|
1581
|
+
} else {
|
|
1582
|
+
if (this._isPopoverOpen()) {
|
|
1583
|
+
this.hideSuggestions();
|
|
1584
|
+
}
|
|
1585
|
+
this._suggestionTimeout = setTimeout(() => {
|
|
1586
|
+
this._suggestionTimeout = null;
|
|
1587
|
+
const isFocused =
|
|
1588
|
+
this._isFocused ||
|
|
1589
|
+
this.shadowRoot?.activeElement === this._input ||
|
|
1590
|
+
document.activeElement === this ||
|
|
1591
|
+
document.activeElement === this._input ||
|
|
1592
|
+
Boolean(this.matches?.(':focus-within'));
|
|
1593
|
+
if (!isFocused) return;
|
|
1594
|
+
|
|
1595
|
+
const currentCaretPos = this._input.selectionStart;
|
|
1596
|
+
const currentContext = getCaretContext(this._input.value, currentCaretPos, this._configuredKeywords, this.operators, this.combinators, this.delimiters);
|
|
1597
|
+
this._context = currentContext;
|
|
1598
|
+
const currentSuggestions = getSuggestions(currentContext, this._configuredKeywords, this.combinators);
|
|
1599
|
+
if (currentSuggestions.length === 0) {
|
|
1600
|
+
this.hideSuggestions();
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
this._showSuggestions(currentSuggestions, currentContext, trigger);
|
|
1604
|
+
}, KEYWORD_SUGGESTION_DELAY);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
_showSuggestions(suggestions, context, trigger) {
|
|
1109
1609
|
this._activeSuggestions = suggestions;
|
|
1110
|
-
|
|
1610
|
+
|
|
1611
|
+
// Pre-select first item for quick Enter/Tab when filtering or in value mode or triggered via Down Arrow,
|
|
1612
|
+
// but leave unselected (-1) when showing full keyword list at the start of a new token so Enter/Tab aren't hijacked.
|
|
1613
|
+
const shouldPreselect =
|
|
1614
|
+
context.mode === 'value' ||
|
|
1615
|
+
Boolean(context.query) ||
|
|
1616
|
+
trigger === 'arrow';
|
|
1617
|
+
const existingSelectedIdx = suggestions.findIndex((s) => this._isSuggestionSelected(s));
|
|
1618
|
+
this._selectedIndex = existingSelectedIdx !== -1 ? existingSelectedIdx : (shouldPreselect ? 0 : -1); // Highlight matching item or first item for quick Enter/Tab
|
|
1111
1619
|
this._renderSuggestions();
|
|
1112
1620
|
|
|
1113
1621
|
// Show popover
|
|
@@ -1126,6 +1634,10 @@ export class RichInput extends HTMLElement {
|
|
|
1126
1634
|
}
|
|
1127
1635
|
|
|
1128
1636
|
hideSuggestions() {
|
|
1637
|
+
if (this._suggestionTimeout) {
|
|
1638
|
+
clearTimeout(this._suggestionTimeout);
|
|
1639
|
+
this._suggestionTimeout = null;
|
|
1640
|
+
}
|
|
1129
1641
|
if (this._isPopoverOpen()) {
|
|
1130
1642
|
try {
|
|
1131
1643
|
this._popover.hidePopover();
|
|
@@ -1137,6 +1649,35 @@ export class RichInput extends HTMLElement {
|
|
|
1137
1649
|
this._selectedIndex = -1;
|
|
1138
1650
|
}
|
|
1139
1651
|
|
|
1652
|
+
_isSuggestionSelected(sug) {
|
|
1653
|
+
if (!this._context || !sug) return false;
|
|
1654
|
+
|
|
1655
|
+
if (sug.type === 'value' && this._context.mode === 'value' && this._context.token) {
|
|
1656
|
+
const currentVal = (this._context.token.innerValue ?? '').trim().toLowerCase();
|
|
1657
|
+
if (!currentVal) return false;
|
|
1658
|
+
const sugVal = (sug.value ?? '').trim().toLowerCase();
|
|
1659
|
+
const sugLabel = (sug.label ?? '').trim().toLowerCase();
|
|
1660
|
+
const sugDisplay = (sug.display ?? '').trim().toLowerCase();
|
|
1661
|
+
return currentVal === sugVal || currentVal === sugLabel || currentVal === sugDisplay;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (sug.type === 'keyword' && this._context.mode === 'keyword' && this._context.token?.type === 'keyword') {
|
|
1665
|
+
const currentKw = (this._context.token.keywordLower ?? '').trim().toLowerCase();
|
|
1666
|
+
if (!currentKw) return false;
|
|
1667
|
+
const sugId = (sug.id ?? '').trim().toLowerCase();
|
|
1668
|
+
return currentKw === sugId;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (sug.type === 'combinator' && this._context.mode === 'keyword' && this._context.token?.type === 'combinator') {
|
|
1672
|
+
const currentComb = (this._context.token.combinator ?? '').trim().toLowerCase();
|
|
1673
|
+
if (!currentComb) return false;
|
|
1674
|
+
const sugComb = (sug.combinator ?? sug.value ?? '').trim().toLowerCase();
|
|
1675
|
+
return currentComb === sugComb;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1140
1681
|
_renderSuggestions() {
|
|
1141
1682
|
if (this._popoverTitle) {
|
|
1142
1683
|
let title = 'Suggestions';
|
|
@@ -1152,12 +1693,17 @@ export class RichInput extends HTMLElement {
|
|
|
1152
1693
|
this._suggestionsList.replaceChildren();
|
|
1153
1694
|
|
|
1154
1695
|
this._activeSuggestions.forEach((sug, idx) => {
|
|
1696
|
+
const isAct = idx === this._selectedIndex;
|
|
1697
|
+
const isSel = this._isSuggestionSelected(sug);
|
|
1155
1698
|
const li = document.createElement('li');
|
|
1156
|
-
li.className = `suggestion-item${
|
|
1699
|
+
li.className = `suggestion-item${isAct ? ' active' : ''}${isSel ? ' selected' : ''}`;
|
|
1157
1700
|
li.id = `ri-opt-${idx}`;
|
|
1158
1701
|
li.setAttribute('role', 'option');
|
|
1159
|
-
li.setAttribute('aria-selected',
|
|
1160
|
-
|
|
1702
|
+
li.setAttribute('aria-selected', isSel ? 'true' : 'false');
|
|
1703
|
+
const parts = ['suggestion-item'];
|
|
1704
|
+
if (isAct) parts.push('suggestion-item-active');
|
|
1705
|
+
if (isSel) parts.push('suggestion-item-selected');
|
|
1706
|
+
li.setAttribute('part', parts.join(' '));
|
|
1161
1707
|
|
|
1162
1708
|
if (sug.image) {
|
|
1163
1709
|
const img = sug.image.cloneNode(true);
|
|
@@ -1173,7 +1719,13 @@ export class RichInput extends HTMLElement {
|
|
|
1173
1719
|
|
|
1174
1720
|
const title = document.createElement('span');
|
|
1175
1721
|
title.className = 'suggestion-title';
|
|
1176
|
-
|
|
1722
|
+
const titlePart =
|
|
1723
|
+
sug.type === 'keyword'
|
|
1724
|
+
? 'suggestion-keyword'
|
|
1725
|
+
: sug.type === 'combinator'
|
|
1726
|
+
? 'suggestion-combinator'
|
|
1727
|
+
: 'suggestion-value';
|
|
1728
|
+
title.setAttribute('part', titlePart);
|
|
1177
1729
|
title.textContent = sug.display;
|
|
1178
1730
|
|
|
1179
1731
|
const desc = document.createElement('span');
|
|
@@ -1209,7 +1761,11 @@ export class RichInput extends HTMLElement {
|
|
|
1209
1761
|
_moveSelection(delta) {
|
|
1210
1762
|
if (this._activeSuggestions.length === 0) return;
|
|
1211
1763
|
const len = this._activeSuggestions.length;
|
|
1212
|
-
|
|
1764
|
+
if (this._selectedIndex === -1) {
|
|
1765
|
+
this._selectedIndex = delta > 0 ? 0 : len - 1;
|
|
1766
|
+
} else {
|
|
1767
|
+
this._selectedIndex = (this._selectedIndex + delta + len) % len;
|
|
1768
|
+
}
|
|
1213
1769
|
this._updateActiveSuggestion();
|
|
1214
1770
|
|
|
1215
1771
|
// Scroll active item into view
|
|
@@ -1223,9 +1779,14 @@ export class RichInput extends HTMLElement {
|
|
|
1223
1779
|
const items = this._suggestionsList.children;
|
|
1224
1780
|
for (let i = 0; i < items.length; i++) {
|
|
1225
1781
|
const isAct = i === this._selectedIndex;
|
|
1782
|
+
const isSel = this._isSuggestionSelected(this._activeSuggestions[i]);
|
|
1226
1783
|
items[i].classList.toggle('active', isAct);
|
|
1227
|
-
items[i].
|
|
1228
|
-
items[i].setAttribute('
|
|
1784
|
+
items[i].classList.toggle('selected', isSel);
|
|
1785
|
+
items[i].setAttribute('aria-selected', isSel ? 'true' : 'false');
|
|
1786
|
+
const parts = ['suggestion-item'];
|
|
1787
|
+
if (isAct) parts.push('suggestion-item-active');
|
|
1788
|
+
if (isSel) parts.push('suggestion-item-selected');
|
|
1789
|
+
items[i].setAttribute('part', parts.join(' '));
|
|
1229
1790
|
if (isAct) {
|
|
1230
1791
|
this._input.setAttribute('aria-activedescendant', items[i].id);
|
|
1231
1792
|
}
|
|
@@ -1253,7 +1814,9 @@ export class RichInput extends HTMLElement {
|
|
|
1253
1814
|
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
1254
1815
|
const selectDetail = {
|
|
1255
1816
|
type: suggestion.type,
|
|
1256
|
-
|
|
1817
|
+
operator: this._context?.operator || null,
|
|
1818
|
+
combinator: suggestion.combinator || (suggestion.type === 'combinator' ? suggestion.value : null),
|
|
1819
|
+
keyword: suggestion.keyword || (suggestion.type === 'keyword' ? suggestion.id : null),
|
|
1257
1820
|
value: suggestion.value || null,
|
|
1258
1821
|
label: suggestion.label || null,
|
|
1259
1822
|
query: newValue,
|
|
@@ -1263,12 +1826,9 @@ export class RichInput extends HTMLElement {
|
|
|
1263
1826
|
composed: true,
|
|
1264
1827
|
detail: selectDetail,
|
|
1265
1828
|
}));
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
} else {
|
|
1270
|
-
this.hideSuggestions();
|
|
1271
|
-
}
|
|
1829
|
+
// Immediately show value suggestions when a keyword was selected (e.g. `mix:`),
|
|
1830
|
+
// or keyword suggestions when a value or combinator was selected (since cursor is now after a space at a new token)
|
|
1831
|
+
this.updateSuggestions(suggestion.type === 'keyword' ? 'keyword-selected' : 'value-selected');
|
|
1272
1832
|
}
|
|
1273
1833
|
}
|
|
1274
1834
|
|