rich-input 1.3.0 → 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 +122 -100
- package/components/rich-input.js +495 -38
- package/index.js +7 -1
- package/package.json +1 -1
- package/utils/highlights.js +75 -0
- package/utils/query-parser.js +370 -49
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, 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';
|
|
@@ -59,25 +59,35 @@ TEMPLATE.innerHTML = `
|
|
|
59
59
|
cursor: not-allowed;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
.search-icon {
|
|
63
63
|
display: inline-flex;
|
|
64
64
|
align-items: center;
|
|
65
65
|
justify-content: center;
|
|
66
66
|
flex-shrink: 0;
|
|
67
|
+
width: 1.125rem;
|
|
68
|
+
height: 1.125rem;
|
|
67
69
|
margin-right: 0.5rem;
|
|
68
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;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
.search-icon {
|
|
72
|
-
|
|
73
|
-
height: 1.125rem;
|
|
74
|
-
color: inherit;
|
|
75
|
-
pointer-events: none;
|
|
81
|
+
.search-icon[data-has-content] {
|
|
82
|
+
background-image: none;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
.search-icon[data-content-none] {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.search-icon::before {
|
|
90
|
+
content: var(--_icon-content, none);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
.search-input {
|
|
@@ -115,14 +125,32 @@ TEMPLATE.innerHTML = `
|
|
|
115
125
|
display: inline-block;
|
|
116
126
|
}
|
|
117
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
|
+
|
|
118
146
|
/* Generic prefix highlight for keywords */
|
|
119
|
-
::highlight(rich-input-keyword) {
|
|
147
|
+
:host::highlight(rich-input-keyword) {
|
|
120
148
|
color: var(--ri-keyword-color, #64748b);
|
|
121
149
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
/* Squiggly line underneath invalid keyword values */
|
|
125
|
-
::highlight(rich-input-invalid) {
|
|
153
|
+
:host::highlight(rich-input-invalid) {
|
|
126
154
|
text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
127
155
|
-webkit-text-decoration: underline wavy var(--ri-invalid-color, var(--rs-invalid-color, #ef4444));
|
|
128
156
|
text-decoration-line: underline;
|
|
@@ -134,12 +162,6 @@ TEMPLATE.innerHTML = `
|
|
|
134
162
|
text-decoration-skip-ink: none;
|
|
135
163
|
}
|
|
136
164
|
|
|
137
|
-
/* Visually hide datalists and custom style tags in slot */
|
|
138
|
-
::slotted(datalist),
|
|
139
|
-
::slotted(style) {
|
|
140
|
-
display: none !important;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
165
|
.clear-button {
|
|
144
166
|
flex-shrink: 0;
|
|
145
167
|
display: inline-flex;
|
|
@@ -182,6 +204,7 @@ TEMPLATE.innerHTML = `
|
|
|
182
204
|
max-height: 290px;
|
|
183
205
|
overflow-y: auto;
|
|
184
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));
|
|
185
208
|
border: 1px solid var(--ri-popover-border, var(--rs-popover-border, #e2e8f0));
|
|
186
209
|
border-radius: var(--ri-popover-radius, var(--rs-popover-radius, 8px));
|
|
187
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)));
|
|
@@ -223,7 +246,7 @@ TEMPLATE.innerHTML = `
|
|
|
223
246
|
line-height: 1.4;
|
|
224
247
|
transition: background-color 0.1s ease;
|
|
225
248
|
user-select: none;
|
|
226
|
-
color: var(--ri-item-title-color, var(--rs-item-title-color,
|
|
249
|
+
color: var(--ri-item-title-color, var(--rs-item-title-color, inherit));
|
|
227
250
|
}
|
|
228
251
|
|
|
229
252
|
.suggestion-item.selected {
|
|
@@ -245,7 +268,7 @@ TEMPLATE.innerHTML = `
|
|
|
245
268
|
|
|
246
269
|
.suggestion-title {
|
|
247
270
|
font-weight: 600;
|
|
248
|
-
color: var(--ri-item-title-color, var(--rs-item-title-color,
|
|
271
|
+
color: var(--ri-item-title-color, var(--rs-item-title-color, inherit));
|
|
249
272
|
white-space: nowrap;
|
|
250
273
|
overflow: hidden;
|
|
251
274
|
text-overflow: ellipsis;
|
|
@@ -266,19 +289,16 @@ TEMPLATE.innerHTML = `
|
|
|
266
289
|
vertical-align: middle;
|
|
267
290
|
}
|
|
268
291
|
|
|
269
|
-
/* Visually hide datalists in slot */
|
|
270
|
-
::slotted(datalist)
|
|
292
|
+
/* Visually hide datalists and custom style tags in slot */
|
|
293
|
+
::slotted(datalist),
|
|
294
|
+
::slotted(style) {
|
|
271
295
|
display: none !important;
|
|
272
296
|
}
|
|
273
297
|
</style>
|
|
274
298
|
|
|
275
299
|
<div part="wrapper" class="wrapper">
|
|
276
300
|
<div part="control" class="control">
|
|
277
|
-
<
|
|
278
|
-
<svg part="icon" class="search-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
279
|
-
<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" />
|
|
280
|
-
</svg>
|
|
281
|
-
</slot>
|
|
301
|
+
<span part="icon" class="search-icon" aria-hidden="true"></span>
|
|
282
302
|
<input
|
|
283
303
|
part="input"
|
|
284
304
|
class="search-input"
|
|
@@ -296,7 +316,6 @@ TEMPLATE.innerHTML = `
|
|
|
296
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"/>
|
|
297
317
|
</svg>
|
|
298
318
|
</button>
|
|
299
|
-
<slot name="trailing"></slot>
|
|
300
319
|
</div>
|
|
301
320
|
|
|
302
321
|
<div
|
|
@@ -362,6 +381,79 @@ function syncDocumentHighlightStyles(shadowRoot) {
|
|
|
362
381
|
export class RichInput extends HTMLElement {
|
|
363
382
|
static formAssociated = true;
|
|
364
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
|
+
|
|
365
457
|
static get observedAttributes() {
|
|
366
458
|
return [
|
|
367
459
|
'value',
|
|
@@ -372,6 +464,9 @@ export class RichInput extends HTMLElement {
|
|
|
372
464
|
'autofocus',
|
|
373
465
|
'required',
|
|
374
466
|
'highlight-quotes',
|
|
467
|
+
'operators',
|
|
468
|
+
'combinators',
|
|
469
|
+
'delimiters',
|
|
375
470
|
'aria-label',
|
|
376
471
|
'aria-labelledby',
|
|
377
472
|
];
|
|
@@ -409,15 +504,26 @@ export class RichInput extends HTMLElement {
|
|
|
409
504
|
this._popoverTitle = this.shadowRoot.querySelector('.popover-title');
|
|
410
505
|
this._suggestionsList = this.shadowRoot.querySelector('.suggestions-list');
|
|
411
506
|
this._clearBtn = this.shadowRoot.querySelector('.clear-button');
|
|
507
|
+
this._icon = this.shadowRoot.querySelector('.search-icon');
|
|
412
508
|
this._slot = this.shadowRoot.querySelector('slot:not([name])');
|
|
509
|
+
this._lastIconColor = null;
|
|
413
510
|
|
|
414
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;
|
|
415
518
|
this._activeSuggestions = [];
|
|
416
519
|
this._selectedIndex = -1;
|
|
417
520
|
this._context = null;
|
|
418
521
|
this._suggestionTimeout = null;
|
|
419
522
|
this._ownedRanges = [];
|
|
420
523
|
this._invalidRanges = [];
|
|
524
|
+
this._operatorRanges = [];
|
|
525
|
+
this._combinatorRanges = [];
|
|
526
|
+
this._delimiterRanges = [];
|
|
421
527
|
this._activeKeywordHighlightMap = new Map();
|
|
422
528
|
this._isFocused = false;
|
|
423
529
|
this._lastCaretPosition = -1;
|
|
@@ -439,6 +545,10 @@ export class RichInput extends HTMLElement {
|
|
|
439
545
|
|
|
440
546
|
connectedCallback() {
|
|
441
547
|
this._syncInjectedStyles();
|
|
548
|
+
this._syncIconStyle();
|
|
549
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
550
|
+
requestAnimationFrame(() => this._syncIconStyle());
|
|
551
|
+
}
|
|
442
552
|
syncDocumentHighlightStyles(this.shadowRoot);
|
|
443
553
|
highlightManager.register(this);
|
|
444
554
|
|
|
@@ -455,9 +565,15 @@ export class RichInput extends HTMLElement {
|
|
|
455
565
|
|
|
456
566
|
// Listen to changes on light DOM datalists and style tags
|
|
457
567
|
this._slot.addEventListener('slotchange', this._onSlotChange);
|
|
458
|
-
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;
|
|
459
574
|
this._loadDatalists();
|
|
460
575
|
this._syncInjectedStyles();
|
|
576
|
+
this._syncIconStyle();
|
|
461
577
|
this.updateHighlights();
|
|
462
578
|
});
|
|
463
579
|
this._mutationObserver.observe(this, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
@@ -479,6 +595,48 @@ export class RichInput extends HTMLElement {
|
|
|
479
595
|
window.addEventListener('scroll', this._onGlobalResizeOrScroll, { passive: true });
|
|
480
596
|
|
|
481
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
|
+
}
|
|
482
640
|
if (this.hasAttribute('value')) {
|
|
483
641
|
this._input.value = this.getAttribute('value');
|
|
484
642
|
}
|
|
@@ -554,6 +712,72 @@ export class RichInput extends HTMLElement {
|
|
|
554
712
|
}
|
|
555
713
|
} else if (name === 'highlight-quotes') {
|
|
556
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
|
+
}
|
|
557
781
|
}
|
|
558
782
|
}
|
|
559
783
|
|
|
@@ -669,6 +893,108 @@ export class RichInput extends HTMLElement {
|
|
|
669
893
|
this._input.selectionDirection = val;
|
|
670
894
|
}
|
|
671
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
|
+
|
|
672
998
|
// --- Public Methods ---
|
|
673
999
|
focus(options) {
|
|
674
1000
|
this._isFocused = true;
|
|
@@ -689,7 +1015,7 @@ export class RichInput extends HTMLElement {
|
|
|
689
1015
|
}
|
|
690
1016
|
|
|
691
1017
|
getParsedQuery() {
|
|
692
|
-
return parseSearchQuery(this._input.value);
|
|
1018
|
+
return parseSearchQuery(this._input.value, this.operators, this.combinators, this.delimiters);
|
|
693
1019
|
}
|
|
694
1020
|
|
|
695
1021
|
getKeywords() {
|
|
@@ -776,9 +1102,66 @@ export class RichInput extends HTMLElement {
|
|
|
776
1102
|
_onSlotChange() {
|
|
777
1103
|
this._loadDatalists();
|
|
778
1104
|
this._syncInjectedStyles();
|
|
1105
|
+
this._syncIconStyle();
|
|
779
1106
|
this.updateHighlights();
|
|
780
1107
|
}
|
|
781
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
|
+
|
|
782
1165
|
// --- Public Properties ---
|
|
783
1166
|
get inputElement() {
|
|
784
1167
|
return this._input;
|
|
@@ -793,6 +1176,9 @@ export class RichInput extends HTMLElement {
|
|
|
793
1176
|
}
|
|
794
1177
|
this._ownedRanges = [];
|
|
795
1178
|
this._invalidRanges = [];
|
|
1179
|
+
this._operatorRanges = [];
|
|
1180
|
+
this._combinatorRanges = [];
|
|
1181
|
+
this._delimiterRanges = [];
|
|
796
1182
|
this._activeKeywordHighlightMap.clear();
|
|
797
1183
|
}
|
|
798
1184
|
|
|
@@ -815,7 +1201,7 @@ export class RichInput extends HTMLElement {
|
|
|
815
1201
|
return;
|
|
816
1202
|
}
|
|
817
1203
|
|
|
818
|
-
const tokens = parseSearchTokens(text);
|
|
1204
|
+
const tokens = parseSearchTokens(text, this.operators, this.combinators, this.delimiters);
|
|
819
1205
|
const highlightQuotes = this.getAttribute('highlight-quotes') !== 'exclude';
|
|
820
1206
|
|
|
821
1207
|
const isFocused = this._isFocused;
|
|
@@ -839,6 +1225,15 @@ export class RichInput extends HTMLElement {
|
|
|
839
1225
|
|
|
840
1226
|
const bucket = this._activeKeywordHighlightMap.get(kw);
|
|
841
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
|
+
|
|
842
1237
|
// 1. Value range
|
|
843
1238
|
const start = highlightQuotes ? token.valueStart : token.innerStart;
|
|
844
1239
|
const end = highlightQuotes ? token.valueEnd : token.innerEnd;
|
|
@@ -897,6 +1292,39 @@ export class RichInput extends HTMLElement {
|
|
|
897
1292
|
}
|
|
898
1293
|
}
|
|
899
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
|
+
}
|
|
900
1328
|
}
|
|
901
1329
|
}
|
|
902
1330
|
|
|
@@ -907,6 +1335,18 @@ export class RichInput extends HTMLElement {
|
|
|
907
1335
|
return this._activeKeywordHighlightMap;
|
|
908
1336
|
}
|
|
909
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
|
+
|
|
910
1350
|
getActiveInvalidRanges() {
|
|
911
1351
|
return this._invalidRanges;
|
|
912
1352
|
}
|
|
@@ -1102,7 +1542,7 @@ export class RichInput extends HTMLElement {
|
|
|
1102
1542
|
}
|
|
1103
1543
|
|
|
1104
1544
|
const caretPos = this._input.selectionStart;
|
|
1105
|
-
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);
|
|
1106
1546
|
|
|
1107
1547
|
// If trigger is arrow and context is 'none', allow suggesting all keywords
|
|
1108
1548
|
if (trigger === 'arrow' && context.mode === 'none') {
|
|
@@ -1113,7 +1553,7 @@ export class RichInput extends HTMLElement {
|
|
|
1113
1553
|
}
|
|
1114
1554
|
|
|
1115
1555
|
this._context = context;
|
|
1116
|
-
const suggestions = getSuggestions(context, this._configuredKeywords);
|
|
1556
|
+
const suggestions = getSuggestions(context, this._configuredKeywords, this.combinators);
|
|
1117
1557
|
|
|
1118
1558
|
if (suggestions.length === 0) {
|
|
1119
1559
|
this.hideSuggestions();
|
|
@@ -1124,6 +1564,7 @@ export class RichInput extends HTMLElement {
|
|
|
1124
1564
|
// Keyword suggestions show immediately when:
|
|
1125
1565
|
// - focusing/clearing an empty field
|
|
1126
1566
|
// - typing characters that match a keyword (Boolean(context.query))
|
|
1567
|
+
// - typing an operator (Boolean(context.operator))
|
|
1127
1568
|
// - explicitly triggered via the Down Arrow key
|
|
1128
1569
|
// In other cases (at the start of a new token after a space when the field is not empty),
|
|
1129
1570
|
// show after a small delay to prevent constant popups when typing regular sentences with spaces.
|
|
@@ -1132,6 +1573,7 @@ export class RichInput extends HTMLElement {
|
|
|
1132
1573
|
context.mode === 'value' ||
|
|
1133
1574
|
isEmptyField ||
|
|
1134
1575
|
Boolean(context.query) ||
|
|
1576
|
+
Boolean(context.operator) ||
|
|
1135
1577
|
trigger === 'arrow';
|
|
1136
1578
|
|
|
1137
1579
|
if (shouldShowImmediately) {
|
|
@@ -1151,9 +1593,9 @@ export class RichInput extends HTMLElement {
|
|
|
1151
1593
|
if (!isFocused) return;
|
|
1152
1594
|
|
|
1153
1595
|
const currentCaretPos = this._input.selectionStart;
|
|
1154
|
-
const currentContext = getCaretContext(this._input.value, currentCaretPos, this._configuredKeywords);
|
|
1596
|
+
const currentContext = getCaretContext(this._input.value, currentCaretPos, this._configuredKeywords, this.operators, this.combinators, this.delimiters);
|
|
1155
1597
|
this._context = currentContext;
|
|
1156
|
-
const currentSuggestions = getSuggestions(currentContext, this._configuredKeywords);
|
|
1598
|
+
const currentSuggestions = getSuggestions(currentContext, this._configuredKeywords, this.combinators);
|
|
1157
1599
|
if (currentSuggestions.length === 0) {
|
|
1158
1600
|
this.hideSuggestions();
|
|
1159
1601
|
return;
|
|
@@ -1226,6 +1668,13 @@ export class RichInput extends HTMLElement {
|
|
|
1226
1668
|
return currentKw === sugId;
|
|
1227
1669
|
}
|
|
1228
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
|
+
|
|
1229
1678
|
return false;
|
|
1230
1679
|
}
|
|
1231
1680
|
|
|
@@ -1270,7 +1719,13 @@ export class RichInput extends HTMLElement {
|
|
|
1270
1719
|
|
|
1271
1720
|
const title = document.createElement('span');
|
|
1272
1721
|
title.className = 'suggestion-title';
|
|
1273
|
-
|
|
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);
|
|
1274
1729
|
title.textContent = sug.display;
|
|
1275
1730
|
|
|
1276
1731
|
const desc = document.createElement('span');
|
|
@@ -1359,7 +1814,9 @@ export class RichInput extends HTMLElement {
|
|
|
1359
1814
|
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
1360
1815
|
const selectDetail = {
|
|
1361
1816
|
type: suggestion.type,
|
|
1362
|
-
|
|
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),
|
|
1363
1820
|
value: suggestion.value || null,
|
|
1364
1821
|
label: suggestion.label || null,
|
|
1365
1822
|
query: newValue,
|
|
@@ -1370,7 +1827,7 @@ export class RichInput extends HTMLElement {
|
|
|
1370
1827
|
detail: selectDetail,
|
|
1371
1828
|
}));
|
|
1372
1829
|
// Immediately show value suggestions when a keyword was selected (e.g. `mix:`),
|
|
1373
|
-
// or keyword suggestions when a value was selected (since cursor is now after a space at a new token)
|
|
1830
|
+
// or keyword suggestions when a value or combinator was selected (since cursor is now after a space at a new token)
|
|
1374
1831
|
this.updateSuggestions(suggestion.type === 'keyword' ? 'keyword-selected' : 'value-selected');
|
|
1375
1832
|
}
|
|
1376
1833
|
}
|