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.
@@ -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
- slot[name="leading"] {
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
- width: 1.125rem;
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
- ::slotted([slot="trailing"]) {
79
- flex-shrink: 0;
80
- margin-left: 0.375rem;
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, #0f172a));
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, #0f172a));
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
- <slot name="leading">
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
- title.setAttribute('part', sug.type === 'keyword' ? 'suggestion-keyword' : 'suggestion-value');
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
- keyword: suggestion.keyword || suggestion.id,
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
  }