rich-input 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bramus Van Damme <bramus@bram.us> (https://www.bram.us/)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,419 @@
1
+ # rich-input
2
+
3
+ > A rich input field `<rich-input>` with keyword-based autocomplete and in-input highlighting powered by `<datalist>`, the **OpaqueRange API**, and the **CSS Custom Highlight API**.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Custom Elements](https://img.shields.io/badge/Web_Components-Custom_Elements_v1-orange.svg)](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
7
+
8
+ The `<rich-input>` component is a rich input field that acts like a standard `<input type="text">` so users can type ordinary text or search terms, but enhances it with contextual autocomplete and in-input highlighting for structured `keyword:value` entries (such as `label:"We Play House Recordings" year:2026 playlist:"WPH Classics"`).
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Standard Input Ergonomics**: Acts and feels like a regular `<input type="text">` with standard value access, selection ranges, and events.
15
+ - **Dual Contextual Autocomplete**:
16
+ - **Keywords**: Typing at the start of a token (e.g. typing `a`) suggests configured keywords like `artist:` and `style:`.
17
+ - **Values**: Typing within a keyword value (e.g. `label:"K` or `label:K`) suggests matching options like `"Keinemusik"` and `"Kranky"`.
18
+ - **Range-Based Positioning via OpaqueRange**: Positions autocomplete dropdown popovers anchored to the start of the active `OpaqueRange` (e.g. at the opening quotation mark of a value) using `range.getBoundingClientRect()`, rather than shifting with the cursor.
19
+ - **Native In-Input Highlighting via CSS Custom Highlight API**: Highlights keyword values inside the `<input>` control using standard CSS rules like `::highlight(label)` or `::highlight(year)` without brittle mirror-div overlays.
20
+ - **Declarative Configuration via `<datalist>`**: Configure keywords and options purely in HTML by nesting standard `<datalist>` elements with `<option>` tags inside `<rich-input>`.
21
+ - **Rich Option Markup**: Embed custom HTML markup (such as logos, images, icons, and avatars) directly inside `<option>` elements for rich, visual suggestion popovers.
22
+ - **Form Associated**: Implements `static formAssociated = true` and `ElementInternals` to participate seamlessly in `<form>` submission, `FormData`, and form reset lifecycles.
23
+ - **Shadow Parts Theming (`::part`)**: Full CSS customizability using `::part(input)`, `::part(control)`, `::part(popover)`, `::part(suggestion-item)`, etc.
24
+ - **Accessible (W3C Combobox Pattern)**: ARIA 1.2 compliant combobox with keyboard navigation (`ArrowUp`, `ArrowDown`, `Enter`, `Tab`, `Escape`), `aria-expanded`, and `aria-activedescendant`.
25
+ - **Graceful Fallback**: Automatically feature-detects `createValueRange` and gracefully falls back to control-aligned popovers in environments without OpaqueRange.
26
+
27
+ ---
28
+
29
+ ## Component Anatomy & Shadow Parts
30
+
31
+ The visual below illustrates the internal Shadow DOM elements, exposed CSS Shadow Parts (`::part`), and CSS Custom Highlight pseudo-elements (`::highlight`), showing how they relate to one another:
32
+
33
+ <p align="center">
34
+ <img src="assets/rich-input-parts.svg" alt="<rich-input> Component Anatomy, Shadow Parts, and Highlight Pseudos" width="100%">
35
+ </p>
36
+
37
+ - `<rich-input>`: The host custom element wrapping the control, datalists, and suggestions popover.
38
+ - `::part(control)`: The outer input container enclosing the icon, input, and clear button.
39
+ - `::part(icon)`: The default leading search magnifying glass SVG icon (fallback in `slot="leading"`).
40
+ - `::part(input)`: The native `<input type="text">` where users type.
41
+ - `::highlight(<keyword>)`: Target pseudo-element for styling keyword values via the CSS Custom Highlight API (e.g. `::highlight(label)`, `::highlight(year)`).
42
+ - `::part(clear-button)`: The clear button (visible when text is present).
43
+ - `::part(popover)`: The autocomplete dropdown popover container anchored to the start of the active range via `OpaqueRange`.
44
+ - `::part(suggestions-header)`: The header bar at the top of the suggestions popover.
45
+ - `::part(suggestions-list)`: The `<ul>` container holding autocomplete suggestion items.
46
+ - `::part(suggestion-item)`: Each suggestion `<li>` row.
47
+ - `::part(suggestion-item-active)`: The currently selected / keyboard-focused suggestion row.
48
+ - `::part(suggestion-image)`: The circular logo, icon, or avatar image prepended to rich suggestions.
49
+ - `::part(suggestion-keyword)`: The keyword label text inside a keyword suggestion.
50
+ - `::part(suggestion-value)`: The value label text inside a value suggestion.
51
+
52
+ ---
53
+
54
+ ## Quick Start
55
+
56
+ ### 1. Installation
57
+
58
+ Install via npm:
59
+
60
+ ```bash
61
+ npm install rich-input
62
+ ```
63
+
64
+ Or import directly in your HTML/JavaScript bundle:
65
+
66
+ ```javascript
67
+ import 'rich-input';
68
+ ```
69
+
70
+ Or via CDN:
71
+
72
+ ```html
73
+ <script type="module" src="https://esm.sh/rich-input"></script>
74
+ ```
75
+
76
+ ### 2. Basic Usage
77
+
78
+ Nest `<datalist>` elements inside `<rich-input>` to configure keywords and autocomplete suggestions:
79
+
80
+ ```html
81
+ <rich-input placeholder="Search music catalog...">
82
+ <datalist id="label" label="Record Label">
83
+ <option value="Defected"></option>
84
+ <option value="Keinemusik"></option>
85
+ <option value="Kranky"></option>
86
+ <option value="Madhouse Records"></option>
87
+ <option value="Ninja Tune"></option>
88
+ <option value="We Play House Recordings"></option>
89
+ <option value="XL Recordings"></option>
90
+ </datalist>
91
+
92
+ <datalist id="year" label="Release Year" data-type="number">
93
+ <option value="2026"></option>
94
+ <option value="2025"></option>
95
+ <option value="2024"></option>
96
+ </datalist>
97
+
98
+ <datalist id="playlist" label="Playlist">
99
+ <option value="WPH Classics"></option>
100
+ <option value="Late Night Grooves"></option>
101
+ </datalist>
102
+ </rich-input>
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Datalist Configuration
108
+
109
+ Configuration is defined by standard HTML `<datalist>` elements placed inside the `<rich-input>` element:
110
+
111
+ | Element / Attribute | Type | Description |
112
+ |---|---|---|
113
+ | `<datalist id="...">` | `string` | **Required.** The keyword identifier used in queries (e.g. `id="artist"` produces `artist:`). Case-insensitive. |
114
+ | `<datalist label="...">` | `string` | Human-readable label displayed in suggestion headers. Defaults to capitalized `id`. |
115
+ | `<datalist data-type="...">` | `string` | Optional data type (`"string"` or `"number"`). |
116
+ | `<option value="...">` | `string` | The suggested value. If the value contains spaces, quotes are automatically added when inserted (e.g. `"We Play House Recordings"`). |
117
+ | `<option label="...">` | `string` | Optional descriptive label shown alongside the value. |
118
+ | `<option>` children | `Node` | Optional image (`<img>`) prepended to the suggested value. |
119
+
120
+ Datalists can be added, updated, or removed dynamically at runtime; `<rich-input>` observes changes via `slotchange` and `MutationObserver`.
121
+
122
+ ---
123
+
124
+ ## Rich Option Markup
125
+
126
+ `<rich-input>` supports rich HTML markup inside `<option>` elements. For example, for record labels or artists, you can prepend a logo image:
127
+
128
+ ```html
129
+ <rich-input placeholder="Search...">
130
+ <datalist id="label" label="Record Label">
131
+ <option value="Defected">
132
+ <img src="assets/defected.jpg" height="50" width="50" alt="Defected Logo">
133
+ Defected
134
+ </option>
135
+ <option value="Keinemusik">
136
+ <img src="assets/keinemusik.jpg" height="50" width="50" alt="Keinemusik Logo">
137
+ Keinemusik
138
+ </option>
139
+ <option value="Kranky">
140
+ <img src="assets/kranky.jpg" height="50" width="50" alt="Kranky Logo">
141
+ Kranky
142
+ </option>
143
+ <option value="Madhouse Records">
144
+ <img src="assets/madhouse-records.jpg" height="50" width="50" alt="Madhouse Records Logo">
145
+ Madhouse Records
146
+ </option>
147
+ <option value="Ninja Tune">
148
+ <img src="assets/ninja-tune.jpg" height="50" width="50" alt="Ninja Tune Logo">
149
+ Ninja Tune
150
+ </option>
151
+ <option value="We Play House Recordings">
152
+ <img src="assets/we-play-house-recordings.jpg" height="50" width="50" alt="We Play House Recordings Logo">
153
+ We Play House Recordings
154
+ </option>
155
+ <option value="XL Recordings">
156
+ <img src="assets/xl-recordings.jpg" height="50" width="50" alt="XL Recordings Logo">
157
+ XL Recordings
158
+ </option>
159
+ </datalist>
160
+ </rich-input>
161
+ ```
162
+
163
+ When suggesting values for `label:`, `<rich-input>` sniffs the image inside the `<option>`, renders it alongside the option's text content, and exposes `::part(suggestion-image)` for external CSS styling (e.g. as a `1em` circular icon):
164
+
165
+ ```css
166
+ rich-input::part(suggestion-image) {
167
+ width: 1em;
168
+ height: 1em;
169
+ border-radius: 50%;
170
+ object-fit: cover;
171
+ }
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Custom Leading Icon & Slots
177
+
178
+ `<rich-input>` provides named slots to customize elements inside the control:
179
+
180
+ - **`slot="leading"`**: Replace the leading icon. The default magnifying glass SVG is provided as fallback content inside the slot, so passing a custom element into `slot="leading"` automatically replaces it without needing CSS overrides.
181
+ - **`slot="trailing"`**: Add controls or elements after the clear button (e.g. submit button, voice input, keyboard shortcut badge).
182
+
183
+ ### Passing a Custom Leading Icon
184
+
185
+ Provide your own SVG or image with `slot="leading"`:
186
+
187
+ ```html
188
+ <rich-input placeholder="Search music catalog...">
189
+ <!-- Custom leading music icon -->
190
+ <svg slot="leading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
191
+ <path d="M9 18V5l12-2v13"></path>
192
+ <circle cx="6" cy="18" r="3"></circle>
193
+ <circle cx="18" cy="16" r="3"></circle>
194
+ </svg>
195
+
196
+ <datalist id="genre" label="Genre">
197
+ <option value="House"></option>
198
+ <option value="Techno"></option>
199
+ </datalist>
200
+ </rich-input>
201
+ ```
202
+
203
+ When an element with `slot="leading"` is supplied, the default search magnifying glass icon is automatically suppressed. If no slotted element is provided, the default magnifying glass icon renders as fallback.
204
+
205
+ ### Available Slots
206
+
207
+ | Slot Name | Description |
208
+ |---|---|
209
+ | `leading` | Custom leading icon or content. Defaults to the search magnifying glass icon (`::part(icon)`). |
210
+ | `trailing` | Custom content rendered after the clear button. |
211
+ | *(default)* | Unnamed slot where `<datalist>` configuration elements are placed (visually hidden). |
212
+
213
+ ---
214
+
215
+ ## The OpaqueRange API
216
+
217
+ The [OpaqueRange API](https://chromestatus.com/feature/6297362687066112) is a web platform standard introduced in Chromium 152 (Google Chrome, Microsoft Edge) that enables range-based operations over the text content of form controls (`<input>` and `<textarea>`).
218
+
219
+ Before `OpaqueRange`, web authors had to clone form controls into hidden `<div>`s to measure caret coordinates or apply highlights. `OpaqueRange` provides native access:
220
+
221
+ ```javascript
222
+ // Measure exact caret coordinates inside <input>
223
+ const range = input.createValueRange(caretPos, caretPos);
224
+ const rect = range.getBoundingClientRect();
225
+
226
+ // Anchor autocomplete popover at caret
227
+ popover.style.left = `${rect.left}px`;
228
+ popover.style.top = `${rect.bottom + 4}px`;
229
+
230
+ // Highlight syntax directly inside <input>
231
+ const valueRange = input.createValueRange(valStart, valEnd);
232
+ const highlight = new Highlight(valueRange);
233
+ CSS.highlights.set('label', highlight);
234
+ ```
235
+
236
+ `<rich-input>` automatically checks `typeof HTMLInputElement.prototype.createValueRange === 'function'`. On supported browsers, caret tracking and `::highlight()` are applied natively. On browsers without `createValueRange`, `<rich-input>` falls back to input-relative popover positioning.
237
+
238
+ ---
239
+
240
+ ## Styling Highlights with the CSS Custom Highlight API
241
+
242
+ Values corresponding to configured keywords are registered into the global `CSS.highlights` registry. Style them directly using `::highlight(keyword)`:
243
+
244
+ ```css
245
+ /* Style the value set in label:"We Play House Recordings" */
246
+ ::highlight(label) {
247
+ background-color: oklch(0.92 0.08 240);
248
+ color: oklch(0.28 0.14 240);
249
+ text-decoration: 2px underline solid oklch(0.5 0.15 240 / 0.5);
250
+ }
251
+
252
+ /* Style numeric year values like year:2026 */
253
+ ::highlight(year) {
254
+ background-color: oklch(0.93 0.1 85);
255
+ color: oklch(0.35 0.14 85);
256
+ }
257
+
258
+ /* Style artist names */
259
+ ::highlight(artist) {
260
+ background-color: oklch(0.92 0.1 320);
261
+ color: oklch(0.32 0.14 320);
262
+ }
263
+
264
+ /* Style musical style filters */
265
+ ::highlight(style) {
266
+ background-color: oklch(0.93 0.08 190);
267
+ color: oklch(0.3 0.12 190);
268
+ }
269
+
270
+ /* Style genre filters */
271
+ ::highlight(genre) {
272
+ background-color: oklch(0.93 0.1 145);
273
+ color: oklch(0.3 0.14 145);
274
+ }
275
+
276
+ /* Generic prefix highlight for keyword labels (e.g. "label:", "year:") */
277
+ ::highlight(rich-input-keyword) {
278
+ color: #64748b;
279
+ text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
280
+ }
281
+ ```
282
+
283
+ > **Note:** Supported CSS properties on `::highlight()` include `color`, `background-color`, `text-decoration`, `text-shadow`, `-webkit-text-stroke-color`, `-webkit-text-stroke-width`, and `-webkit-text-fill-color`.
284
+
285
+ ---
286
+
287
+ ## Styling the Input with Shadow Parts (`::part`)
288
+
289
+ Every internal element of `<rich-input>` is exposed via `::part()`:
290
+
291
+ ```css
292
+ /* Style the outer control container */
293
+ rich-input::part(control) {
294
+ border-radius: 9999px;
295
+ border: 2px solid #2563eb;
296
+ padding: 0 1.25rem;
297
+ background: #ffffff;
298
+ }
299
+
300
+ /* Style the native text input */
301
+ rich-input::part(input) {
302
+ font-family: 'JetBrains Mono', monospace;
303
+ font-size: 1rem;
304
+ }
305
+
306
+ /* Style the suggestions popover */
307
+ rich-input::part(popover) {
308
+ border-radius: 12px;
309
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
310
+ }
311
+
312
+ /* Style active suggestion item */
313
+ rich-input::part(suggestion-item-active) {
314
+ background-color: #dbeafe;
315
+ }
316
+ ```
317
+
318
+ ### Available Shadow Parts
319
+
320
+ | Part Name | Description |
321
+ |---|---|
322
+ | `::part(control)` | The wrapper container enclosing the search icon, input, and clear button |
323
+ | `::part(input)` | The internal native `<input type="text">` |
324
+ | `::part(icon)` | The default leading search icon SVG (fallback in `slot="leading"`) |
325
+ | `::part(clear-button)` | The clear button (visible when text is present) |
326
+ | `::part(popover)` | The autocomplete popover container |
327
+ | `::part(suggestions-header)` | The header bar at the top of the popover |
328
+ | `::part(suggestions-list)` | The `<ul>` list element |
329
+ | `::part(suggestion-item)` | Each suggestion `<li>` item |
330
+ | `::part(suggestion-item-active)` | The currently selected / hovered suggestion item |
331
+ | `::part(suggestion-keyword)` | Keyword name element in suggestion items |
332
+ | `::part(suggestion-value)` | Value element in suggestion items |
333
+ | `::part(suggestion-content)` | The content container inside each suggestion item |
334
+ | `::part(suggestion-image)` | Image or icon element rendered inside rich suggestion items |
335
+
336
+ ---
337
+
338
+ ## JavaScript API
339
+
340
+ ### Properties
341
+
342
+ - `value` (`string`): Gets or sets the search input value. Updates highlights and form value automatically.
343
+ - `placeholder` (`string`): Gets or sets the input placeholder text.
344
+ - `disabled` (`boolean`): Disables or enables the input control.
345
+ - `name` (`string`): Form field name when submitted inside a `<form>`.
346
+ - `selectionStart` / `selectionEnd` (`number`): Text selection / cursor indices.
347
+
348
+ ### Methods
349
+
350
+ - `getParsedQuery()`: Returns a parsed object representing the search query:
351
+ ```json
352
+ {
353
+ "raw": "label:\"We Play House Recordings\" year:2026 chicago house",
354
+ "text": "chicago house",
355
+ "keywords": {
356
+ "label": ["We Play House Recordings"],
357
+ "year": ["2026"]
358
+ },
359
+ "tokens": [...]
360
+ }
361
+ ```
362
+ - `getKeywords()`: Returns an array of configured keyword definitions from the datalists.
363
+ - `focus(options)`: Focuses the internal input.
364
+ - `blur()`: Removes focus from the internal input.
365
+ - `select()`: Selects all text inside the input.
366
+ - `setSelectionRange(start, end, direction)`: Sets caret or selection range.
367
+
368
+ ### Events
369
+
370
+ - `input`: Dispatched when the search value changes (bubbles, composed).
371
+ - `change`: Dispatched on blur or when a search change is committed.
372
+ - `search`: Dispatched when the user presses `Enter` with suggestions closed.
373
+ - `rich-input-select`: Dispatched when an autocomplete suggestion is selected.
374
+ - `event.detail`: `{ type, keyword, value, label, query }`
375
+
376
+ ---
377
+
378
+ ## Form Integration
379
+
380
+ `<rich-input>` supports native `<form>` submission through standard `ElementInternals`:
381
+
382
+ ```html
383
+ <form id="search-form" action="/search" method="GET">
384
+ <rich-input name="q" placeholder="Search tracks...">
385
+ <datalist id="genre" label="Genre">
386
+ <option value="House"></option>
387
+ <option value="Techno"></option>
388
+ </datalist>
389
+ </rich-input>
390
+ <button type="submit">Search</button>
391
+ </form>
392
+
393
+ <script>
394
+ const form = document.getElementById('search-form');
395
+ form.addEventListener('submit', (e) => {
396
+ e.preventDefault();
397
+ const data = new FormData(form);
398
+ console.log('Submitted query:', data.get('q'));
399
+ });
400
+ </script>
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Building from Source
406
+
407
+ ```bash
408
+ # Build ./dist package
409
+ npm run build
410
+
411
+ # Start local demo server
412
+ npm start
413
+ ```
414
+
415
+ ---
416
+
417
+ ## License
418
+
419
+ [MIT](LICENSE) © [Bramus Van Damme](https://www.bram.us/)