@referralgps/selectra 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 +203 -0
- package/README.md +346 -0
- package/dist/selectra.css +1 -0
- package/dist/selectra.es.js +1318 -0
- package/dist/selectra.es.js.map +1 -0
- package/dist/selectra.iife.js +32 -0
- package/dist/selectra.iife.js.map +1 -0
- package/dist/selectra.umd.js +32 -0
- package/dist/selectra.umd.js.map +1 -0
- package/package.json +64 -0
- package/src-alpine/README.md +346 -0
- package/src-alpine/index.d.ts +217 -0
- package/src-alpine/index.js +70 -0
- package/src-alpine/plugins/index.js +219 -0
- package/src-alpine/selectize.js +1022 -0
- package/src-alpine/sifter.js +270 -0
- package/src-alpine/styles/selectra.css +277 -0
- package/src-alpine/utils.js +181 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Selectra
|
|
2
|
+
|
|
3
|
+
> A powerful, extensible `<select>` UI control — rebuilt with **Alpine.js** and **Tailwind CSS**.
|
|
4
|
+
|
|
5
|
+
Selectra is a modern rewrite of [Selectize.js](https://github.com/selectize/selectize.js), designed for tagging, contact lists, country selectors, and autocomplete. It drops the jQuery dependency entirely in favor of Alpine.js reactivity and Tailwind CSS styling.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Selectra?
|
|
10
|
+
|
|
11
|
+
| | Selectize.js (legacy) | **Selectra** |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **UI Framework** | jQuery | Alpine.js |
|
|
14
|
+
| **Styling** | LESS / SCSS + Bootstrap themes | Tailwind CSS utilities |
|
|
15
|
+
| **Build Tool** | Gulp | Vite |
|
|
16
|
+
| **Tests** | Karma + Mocha | Vitest |
|
|
17
|
+
| **Bundle (JS)** | ~88 KB min | ~25 KB min |
|
|
18
|
+
| **CSS** | Multiple theme files | Single 21 KB utility file |
|
|
19
|
+
| **jQuery Required** | Yes | No |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Single & Multi Select** — auto-detected from `maxItems` or explicit `mode`
|
|
26
|
+
- **Tagging / Create** — create new options on the fly with validation
|
|
27
|
+
- **Fuzzy Search** — built-in Sifter engine with diacritics support
|
|
28
|
+
- **Remote Loading** — fetch options from API with debounced requests
|
|
29
|
+
- **Option Groups** — grouped options with sticky headers
|
|
30
|
+
- **Keyboard Navigation** — arrows, enter, escape, tab, backspace, ctrl+A
|
|
31
|
+
- **Copy / Paste** — split pasted text into multiple items via delimiter
|
|
32
|
+
- **Custom Rendering** — templates for options, items, create prompt, no-results, loading
|
|
33
|
+
- **Plugin System** — 9 built-in plugins, easy to create custom ones
|
|
34
|
+
- **Accessible** — focus management, label association, keyboard-first
|
|
35
|
+
- **RTL Support** — auto-detected from CSS `direction`
|
|
36
|
+
- **Tailwind CSS** — fully styled with utilities, trivially customizable
|
|
37
|
+
- **Lightweight** — ~25 KB gzipped JS, zero runtime dependencies beyond Alpine.js
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install selectra alpinejs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CDN
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
|
|
51
|
+
<script src="https://cdn.jsdelivr.net/npm/selectra/dist/selectra.iife.js"></script>
|
|
52
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/selectra/dist/selectra.css">
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### 1. Register the Plugin
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import Alpine from 'alpinejs';
|
|
63
|
+
import Selectra from 'selectra';
|
|
64
|
+
import 'selectra/css';
|
|
65
|
+
|
|
66
|
+
Alpine.plugin(Selectra);
|
|
67
|
+
Alpine.start();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Use in HTML
|
|
71
|
+
|
|
72
|
+
#### Single Select
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<div x-data="selectra({
|
|
76
|
+
mode: 'single',
|
|
77
|
+
placeholder: 'Select a country...',
|
|
78
|
+
options: [
|
|
79
|
+
{ value: 'us', text: 'United States' },
|
|
80
|
+
{ value: 'ca', text: 'Canada' },
|
|
81
|
+
{ value: 'mx', text: 'Mexico' },
|
|
82
|
+
]
|
|
83
|
+
})" class="relative max-w-md">
|
|
84
|
+
|
|
85
|
+
<!-- Control -->
|
|
86
|
+
<div @click="focus()"
|
|
87
|
+
:class="{ 'ring-2 ring-blue-500/20 border-blue-500': isFocused }"
|
|
88
|
+
class="relative flex items-center w-full min-h-[42px] px-3 py-1.5 bg-white border border-gray-300 rounded-lg cursor-pointer hover:border-gray-400">
|
|
89
|
+
|
|
90
|
+
<span x-show="items.length && !isFocused" x-text="currentValueText" class="truncate text-gray-900"></span>
|
|
91
|
+
|
|
92
|
+
<input x-ref="searchInput" x-model="query"
|
|
93
|
+
@input="onInput()" @focus="focus()" @blur.debounce.150ms="blur()"
|
|
94
|
+
@keydown="onKeyDown($event)"
|
|
95
|
+
:placeholder="placeholderText"
|
|
96
|
+
x-show="isFocused || !items.length"
|
|
97
|
+
class="flex-1 min-w-0 bg-transparent outline-none border-none p-0 text-gray-900 placeholder-gray-400">
|
|
98
|
+
|
|
99
|
+
<svg :class="{'rotate-180': isOpen}" class="w-4 h-4 text-gray-400 ml-2 transition-transform"
|
|
100
|
+
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
101
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
102
|
+
</svg>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Dropdown -->
|
|
106
|
+
<div x-show="isOpen" x-ref="dropdown"
|
|
107
|
+
x-transition class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden">
|
|
108
|
+
<div class="max-h-60 overflow-y-auto py-1">
|
|
109
|
+
<template x-for="(option, index) in filteredOptions" :key="optionKey(option)">
|
|
110
|
+
<div @click="selectOption(option)" @mouseenter="activeIndex = index"
|
|
111
|
+
:class="{ 'bg-blue-500 text-white': activeIndex === index }"
|
|
112
|
+
class="px-3 py-2 cursor-pointer"
|
|
113
|
+
x-html="renderOption(option)">
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Multi Select with Tags
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<div x-data="selectra({
|
|
125
|
+
mode: 'multi',
|
|
126
|
+
placeholder: 'Select languages...',
|
|
127
|
+
create: true,
|
|
128
|
+
options: [
|
|
129
|
+
{ value: 'js', text: 'JavaScript' },
|
|
130
|
+
{ value: 'py', text: 'Python' },
|
|
131
|
+
{ value: 'go', text: 'Go' },
|
|
132
|
+
]
|
|
133
|
+
})" class="relative max-w-md">
|
|
134
|
+
|
|
135
|
+
<div @click="focus()"
|
|
136
|
+
:class="{ 'ring-2 ring-blue-500/20 border-blue-500': isFocused }"
|
|
137
|
+
class="relative flex flex-wrap items-center gap-1 w-full min-h-[42px] px-2 py-1.5 bg-white border border-gray-300 rounded-lg cursor-text hover:border-gray-400">
|
|
138
|
+
|
|
139
|
+
<template x-for="val in items" :key="val">
|
|
140
|
+
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-blue-50 text-blue-700 border border-blue-200 text-sm">
|
|
141
|
+
<span x-text="options[val]?.text || val"></span>
|
|
142
|
+
<button @click.stop="removeItem(val)" class="w-4 h-4 rounded-full text-blue-400 hover:text-blue-600">×</button>
|
|
143
|
+
</span>
|
|
144
|
+
</template>
|
|
145
|
+
|
|
146
|
+
<input x-ref="searchInput" x-model="query"
|
|
147
|
+
@input="onInput()" @focus="focus()" @blur.debounce.150ms="blur()"
|
|
148
|
+
@keydown="onKeyDown($event)" @paste="onPaste($event)"
|
|
149
|
+
:placeholder="items.length ? '' : placeholderText"
|
|
150
|
+
class="flex-1 min-w-[60px] bg-transparent outline-none border-none p-0 text-sm">
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div x-show="isOpen" x-ref="dropdown" x-transition
|
|
154
|
+
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden">
|
|
155
|
+
<div class="max-h-60 overflow-y-auto py-1">
|
|
156
|
+
<template x-for="(option, index) in filteredOptions" :key="optionKey(option)">
|
|
157
|
+
<div @click="selectOption(option)" @mouseenter="activeIndex = index"
|
|
158
|
+
:class="{ 'bg-blue-500 text-white': activeIndex === index }"
|
|
159
|
+
class="px-3 py-2 cursor-pointer" x-html="renderOption(option)">
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
<div x-show="canCreate" @click="createItem()"
|
|
163
|
+
class="px-3 py-2 cursor-pointer text-gray-500 border-t border-gray-100"
|
|
164
|
+
x-html="renderOptionCreate()">
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
| Option | Type | Default | Description |
|
|
174
|
+
|--------|------|---------|-------------|
|
|
175
|
+
| `mode` | `'single' \| 'multi'` | auto | Selection mode |
|
|
176
|
+
| `options` | `Array` | `[]` | Available options |
|
|
177
|
+
| `items` | `Array` | `[]` | Pre-selected values |
|
|
178
|
+
| `maxItems` | `number \| null` | `null` | Max selectable items |
|
|
179
|
+
| `maxOptions` | `number` | `1000` | Max dropdown options |
|
|
180
|
+
| `create` | `boolean \| function` | `false` | Allow creating new options |
|
|
181
|
+
| `createOnBlur` | `boolean` | `false` | Create item when field loses focus |
|
|
182
|
+
| `createFilter` | `RegExp \| function` | `null` | Filter for creatable values |
|
|
183
|
+
| `placeholder` | `string` | `''` | Placeholder text |
|
|
184
|
+
| `valueField` | `string` | `'value'` | Property for option value |
|
|
185
|
+
| `labelField` | `string` | `'text'` | Property for display label |
|
|
186
|
+
| `searchField` | `string[]` | `['text']` | Fields to search |
|
|
187
|
+
| `searchConjunction` | `'and' \| 'or'` | `'and'` | Multi-term search logic |
|
|
188
|
+
| `sortField` | `string \| Array` | `'$order'` | Sort field(s) |
|
|
189
|
+
| `highlight` | `boolean` | `true` | Highlight matches |
|
|
190
|
+
| `openOnFocus` | `boolean` | `true` | Open dropdown on focus |
|
|
191
|
+
| `selectOnTab` | `boolean` | `true` | Tab selects active option |
|
|
192
|
+
| `closeAfterSelect` | `boolean` | `false` | Close dropdown after selection |
|
|
193
|
+
| `hideSelected` | `boolean \| null` | auto | Hide selected from dropdown |
|
|
194
|
+
| `delimiter` | `string` | `','` | Value separator |
|
|
195
|
+
| `splitOn` | `RegExp \| string` | `null` | Regex for splitting pasted values |
|
|
196
|
+
| `diacritics` | `boolean` | `true` | International character support |
|
|
197
|
+
| `normalize` | `boolean` | `true` | NFD normalize search queries |
|
|
198
|
+
| `preload` | `boolean \| 'focus'` | `false` | Preload options |
|
|
199
|
+
| `load` | `function \| null` | `null` | Remote data loader |
|
|
200
|
+
| `loadThrottle` | `number` | `300` | Load debounce delay (ms) |
|
|
201
|
+
| `plugins` | `Array` | `[]` | Plugins to activate |
|
|
202
|
+
|
|
203
|
+
### Custom Rendering
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
selectra({
|
|
207
|
+
render: {
|
|
208
|
+
option: (data, escape) => `<div class="flex items-center gap-2">
|
|
209
|
+
<img src="${data.avatar}" class="w-6 h-6 rounded-full">
|
|
210
|
+
<span>${escape(data.text)}</span>
|
|
211
|
+
</div>`,
|
|
212
|
+
item: (data, escape) => escape(data.text),
|
|
213
|
+
optionCreate: (data, escape) => `Add "${escape(data.input)}"`,
|
|
214
|
+
noResults: () => 'Nothing found',
|
|
215
|
+
loading: () => 'Searching...',
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Remote Loading
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
selectra({
|
|
224
|
+
load: (query, callback) => {
|
|
225
|
+
fetch(`/api/search?q=${encodeURIComponent(query)}`)
|
|
226
|
+
.then(res => res.json())
|
|
227
|
+
.then(data => callback(data.results))
|
|
228
|
+
.catch(() => callback([]));
|
|
229
|
+
},
|
|
230
|
+
loadThrottle: 300,
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## API Methods
|
|
235
|
+
|
|
236
|
+
| Method | Description |
|
|
237
|
+
|--------|-------------|
|
|
238
|
+
| `addOption(data)` | Add one or more options |
|
|
239
|
+
| `updateOption(value, data)` | Update an existing option |
|
|
240
|
+
| `removeOption(value)` | Remove an option |
|
|
241
|
+
| `clearOptions()` | Remove all options (keeps selected) |
|
|
242
|
+
| `getOption(value)` | Get option data by value |
|
|
243
|
+
| `addItem(value)` | Select an item |
|
|
244
|
+
| `removeItem(value)` | Deselect an item |
|
|
245
|
+
| `clear()` | Clear all selections |
|
|
246
|
+
| `getValue()` | Get current value(s) |
|
|
247
|
+
| `setValue(value)` | Set selected value(s) |
|
|
248
|
+
| `createItem(input?)` | Create new item from input |
|
|
249
|
+
| `open()` | Open dropdown |
|
|
250
|
+
| `close()` | Close dropdown |
|
|
251
|
+
| `focus()` | Focus the control |
|
|
252
|
+
| `blur()` | Remove focus |
|
|
253
|
+
| `lock()` / `unlock()` | Temporarily disable input |
|
|
254
|
+
| `disable()` / `enable()` | Disable/enable control |
|
|
255
|
+
| `setMaxItems(max)` | Update max items limit |
|
|
256
|
+
|
|
257
|
+
## Events
|
|
258
|
+
|
|
259
|
+
Events are dispatched as custom DOM events on the wrapper element:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
el.addEventListener('selectra:change', (e) => {
|
|
263
|
+
console.log('Value changed:', e.detail);
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| Event | Detail |
|
|
268
|
+
|-------|--------|
|
|
269
|
+
| `selectra:change` | `[value]` |
|
|
270
|
+
| `selectra:itemadd` | `[value, data]` |
|
|
271
|
+
| `selectra:itemremove` | `[value]` |
|
|
272
|
+
| `selectra:clear` | `[]` |
|
|
273
|
+
| `selectra:optionadd` | `[value, data]` |
|
|
274
|
+
| `selectra:optionremove` | `[value]` |
|
|
275
|
+
| `selectra:dropdownopen` | `[]` |
|
|
276
|
+
| `selectra:dropdownclose` | `[]` |
|
|
277
|
+
| `selectra:type` | `[query]` |
|
|
278
|
+
| `selectra:focus` | `[]` |
|
|
279
|
+
| `selectra:blur` | `[]` |
|
|
280
|
+
| `selectra:initialize` | `[]` |
|
|
281
|
+
|
|
282
|
+
## Plugins
|
|
283
|
+
|
|
284
|
+
### Built-in Plugins
|
|
285
|
+
|
|
286
|
+
Activate plugins via the `plugins` config option:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
selectra({
|
|
290
|
+
plugins: ['remove_button', 'clear_button', { name: 'tag_limit', options: { tagLimit: 3 } }]
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
| Plugin | Description |
|
|
295
|
+
|--------|-------------|
|
|
296
|
+
| `remove_button` | Add × button to each selected tag |
|
|
297
|
+
| `clear_button` | Add a clear-all button to the control |
|
|
298
|
+
| `restore_on_backspace` | Restore deleted item text to input |
|
|
299
|
+
| `dropdown_header` | Add a header to the dropdown |
|
|
300
|
+
| `tag_limit` | Limit visible tags with "+N" badge |
|
|
301
|
+
| `auto_select_on_type` | Auto-select first match on blur |
|
|
302
|
+
| `select_on_focus` | Put current value into search on focus |
|
|
303
|
+
| `read_only` | Make component read-only |
|
|
304
|
+
| `auto_position` | Smart dropdown positioning (above/below) |
|
|
305
|
+
|
|
306
|
+
### Custom Plugins
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
import { registerPlugin } from 'selectra';
|
|
310
|
+
|
|
311
|
+
registerPlugin('my_plugin', function(options) {
|
|
312
|
+
// `this` is the selectra component instance
|
|
313
|
+
console.log('Plugin initialized with', options);
|
|
314
|
+
|
|
315
|
+
// Override methods
|
|
316
|
+
const originalOpen = this.open.bind(this);
|
|
317
|
+
this.open = () => {
|
|
318
|
+
console.log('Opening dropdown');
|
|
319
|
+
originalOpen();
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Building
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
npm install
|
|
328
|
+
npm run build # Build dist/
|
|
329
|
+
npm run dev # Dev server with HMR
|
|
330
|
+
npm run test # Run tests
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Migration from jQuery Version
|
|
334
|
+
|
|
335
|
+
| jQuery Selectize | Selectra |
|
|
336
|
+
|-----------------|---------------------|
|
|
337
|
+
| `$('select').selectize({...})` | `x-data="selectra({...})"` |
|
|
338
|
+
| `instance.addItem(val)` | Direct method call in Alpine scope |
|
|
339
|
+
| `$.fn.selectize` plugin | `Alpine.plugin(Selectra)` |
|
|
340
|
+
| jQuery events | Custom DOM events |
|
|
341
|
+
| LESS/SCSS themes | Tailwind CSS utilities |
|
|
342
|
+
| Bootstrap themes | Use Tailwind directly |
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
Apache-2.0
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selectra - Alpine.js + Tailwind CSS
|
|
3
|
+
* TypeScript declarations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Alpine } from 'alpinejs';
|
|
7
|
+
|
|
8
|
+
export interface SelectraOption {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
value?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
optgroup?: string;
|
|
14
|
+
$order?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SelectraOptgroup {
|
|
18
|
+
value: string;
|
|
19
|
+
label: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SelectraRender {
|
|
24
|
+
option?: (data: SelectraOption, escape: (str: string) => string) => string;
|
|
25
|
+
item?: (data: SelectraOption, escape: (str: string) => string) => string;
|
|
26
|
+
optionCreate?: (data: { input: string }, escape: (str: string) => string) => string;
|
|
27
|
+
optgroupHeader?: (data: SelectraOptgroup, escape: (str: string) => string) => string;
|
|
28
|
+
noResults?: (data: { query: string }, escape: (str: string) => string) => string;
|
|
29
|
+
loading?: (data: { query: string }, escape: (str: string) => string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SelectraConfig {
|
|
33
|
+
delimiter?: string;
|
|
34
|
+
splitOn?: RegExp | string | null;
|
|
35
|
+
persist?: boolean;
|
|
36
|
+
diacritics?: boolean;
|
|
37
|
+
create?: boolean | ((input: string, callback: (data?: SelectraOption) => void) => SelectraOption | void);
|
|
38
|
+
showAddOptionOnCreate?: boolean;
|
|
39
|
+
createOnBlur?: boolean;
|
|
40
|
+
createFilter?: RegExp | string | ((input: string) => boolean) | null;
|
|
41
|
+
highlight?: boolean;
|
|
42
|
+
openOnFocus?: boolean;
|
|
43
|
+
maxOptions?: number;
|
|
44
|
+
maxItems?: number | null;
|
|
45
|
+
hideSelected?: boolean | null;
|
|
46
|
+
selectOnTab?: boolean;
|
|
47
|
+
closeAfterSelect?: boolean;
|
|
48
|
+
loadThrottle?: number | null;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
mode?: 'single' | 'multi' | null;
|
|
51
|
+
search?: boolean;
|
|
52
|
+
showArrow?: boolean;
|
|
53
|
+
normalize?: boolean;
|
|
54
|
+
|
|
55
|
+
valueField?: string;
|
|
56
|
+
labelField?: string;
|
|
57
|
+
disabledField?: string;
|
|
58
|
+
optgroupField?: string;
|
|
59
|
+
optgroupLabelField?: string;
|
|
60
|
+
optgroupValueField?: string;
|
|
61
|
+
sortField?: string | Array<{ field: string; direction?: 'asc' | 'desc' }>;
|
|
62
|
+
searchField?: string | string[];
|
|
63
|
+
searchConjunction?: 'and' | 'or';
|
|
64
|
+
respectWordBoundaries?: boolean;
|
|
65
|
+
setFirstOptionActive?: boolean;
|
|
66
|
+
preload?: boolean | 'focus';
|
|
67
|
+
|
|
68
|
+
options?: SelectraOption[];
|
|
69
|
+
optgroups?: SelectraOptgroup[];
|
|
70
|
+
items?: string[];
|
|
71
|
+
plugins?: Array<string | { name: string; options?: Record<string, any> }>;
|
|
72
|
+
|
|
73
|
+
render?: SelectraRender;
|
|
74
|
+
|
|
75
|
+
load?: ((query: string, callback: (results: SelectraOption[]) => void) => void) | null;
|
|
76
|
+
score?: ((search: any) => (item: any) => number) | null;
|
|
77
|
+
|
|
78
|
+
onChange?: (value: string | string[]) => void;
|
|
79
|
+
onItemAdd?: (value: string, data: SelectraOption) => void;
|
|
80
|
+
onItemRemove?: (value: string) => void;
|
|
81
|
+
onClear?: () => void;
|
|
82
|
+
onOptionAdd?: (value: string, data: SelectraOption) => void;
|
|
83
|
+
onOptionRemove?: (value: string) => void;
|
|
84
|
+
onDropdownOpen?: () => void;
|
|
85
|
+
onDropdownClose?: () => void;
|
|
86
|
+
onType?: (query: string) => void;
|
|
87
|
+
onFocus?: () => void;
|
|
88
|
+
onBlur?: () => void;
|
|
89
|
+
onInitialize?: () => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SelectraInstance {
|
|
93
|
+
// State
|
|
94
|
+
isOpen: boolean;
|
|
95
|
+
isFocused: boolean;
|
|
96
|
+
isDisabled: boolean;
|
|
97
|
+
isLocked: boolean;
|
|
98
|
+
isLoading: boolean;
|
|
99
|
+
isInvalid: boolean;
|
|
100
|
+
query: string;
|
|
101
|
+
activeIndex: number;
|
|
102
|
+
items: string[];
|
|
103
|
+
options: Record<string, SelectraOption>;
|
|
104
|
+
optgroups: Record<string, SelectraOptgroup>;
|
|
105
|
+
|
|
106
|
+
// Computed
|
|
107
|
+
readonly isMultiple: boolean;
|
|
108
|
+
readonly isSingle: boolean;
|
|
109
|
+
readonly isFull: boolean;
|
|
110
|
+
readonly hasOptions: boolean;
|
|
111
|
+
readonly canCreate: boolean;
|
|
112
|
+
readonly selectedItems: SelectraOption[];
|
|
113
|
+
readonly filteredOptions: SelectraOption[];
|
|
114
|
+
readonly placeholderText: string;
|
|
115
|
+
readonly currentValueText: string;
|
|
116
|
+
|
|
117
|
+
// Methods
|
|
118
|
+
init(): void;
|
|
119
|
+
destroy(): void;
|
|
120
|
+
|
|
121
|
+
addOption(data: SelectraOption | SelectraOption[], silent?: boolean): void;
|
|
122
|
+
updateOption(value: string, data: SelectraOption): void;
|
|
123
|
+
removeOption(value: string): void;
|
|
124
|
+
clearOptions(): void;
|
|
125
|
+
getOption(value: string): SelectraOption | null;
|
|
126
|
+
|
|
127
|
+
addItem(value: string, silent?: boolean): void;
|
|
128
|
+
removeItem(value: string, silent?: boolean): void;
|
|
129
|
+
clear(silent?: boolean): void;
|
|
130
|
+
getValue(): string | string[];
|
|
131
|
+
setValue(value: string | string[], silent?: boolean): void;
|
|
132
|
+
createItem(input?: string | null): void;
|
|
133
|
+
|
|
134
|
+
selectOption(option: SelectraOption): void;
|
|
135
|
+
|
|
136
|
+
open(): void;
|
|
137
|
+
close(): void;
|
|
138
|
+
toggle(): void;
|
|
139
|
+
focus(): void;
|
|
140
|
+
blur(): void;
|
|
141
|
+
|
|
142
|
+
lock(): void;
|
|
143
|
+
unlock(): void;
|
|
144
|
+
disable(): void;
|
|
145
|
+
enable(): void;
|
|
146
|
+
setMaxItems(max: number | null): void;
|
|
147
|
+
|
|
148
|
+
addOptionGroup(id: string, data: SelectraOptgroup): void;
|
|
149
|
+
removeOptionGroup(id: string): void;
|
|
150
|
+
getGroupedOptions(): Array<{ id: string | null; label: string | null; options: SelectraOption[] }>;
|
|
151
|
+
|
|
152
|
+
renderOption(option: SelectraOption): string;
|
|
153
|
+
renderItem(option: SelectraOption): string;
|
|
154
|
+
renderOptionCreate(): string;
|
|
155
|
+
renderNoResults(): string;
|
|
156
|
+
|
|
157
|
+
onKeyDown(e: KeyboardEvent): void;
|
|
158
|
+
onInput(): void;
|
|
159
|
+
onPaste(e: ClipboardEvent): void;
|
|
160
|
+
|
|
161
|
+
isSelected(option: SelectraOption): boolean;
|
|
162
|
+
optionKey(option: SelectraOption): string | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Alpine.js plugin for Selectra
|
|
167
|
+
*/
|
|
168
|
+
declare function SelectraPlugin(Alpine: Alpine): void;
|
|
169
|
+
|
|
170
|
+
export default SelectraPlugin;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a selectra component data factory
|
|
174
|
+
*/
|
|
175
|
+
export function createSelectraComponent(config?: SelectraConfig): () => SelectraInstance;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a plugin
|
|
179
|
+
*/
|
|
180
|
+
export function registerPlugin(name: string, fn: (this: SelectraInstance, options?: any) => void): void;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get default configuration
|
|
184
|
+
*/
|
|
185
|
+
export function getDefaults(): SelectraConfig;
|
|
186
|
+
|
|
187
|
+
export const DEFAULTS: SelectraConfig;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Sifter search engine
|
|
191
|
+
*/
|
|
192
|
+
export class Sifter {
|
|
193
|
+
constructor(items: Record<string, any> | any[], settings?: { diacritics?: boolean });
|
|
194
|
+
items: Record<string, any> | any[];
|
|
195
|
+
tokenize(query: string, respectWordBoundaries?: boolean): Array<{ string: string; regex: RegExp }>;
|
|
196
|
+
search(query: string, options?: {
|
|
197
|
+
fields?: string | string[];
|
|
198
|
+
sort?: Array<{ field: string; direction?: 'asc' | 'desc' }>;
|
|
199
|
+
score?: (item: any) => number;
|
|
200
|
+
filter?: boolean;
|
|
201
|
+
limit?: number;
|
|
202
|
+
conjunction?: 'and' | 'or';
|
|
203
|
+
nesting?: boolean;
|
|
204
|
+
respect_word_boundaries?: boolean;
|
|
205
|
+
}): {
|
|
206
|
+
query: string;
|
|
207
|
+
tokens: Array<{ string: string; regex: RegExp }>;
|
|
208
|
+
total: number;
|
|
209
|
+
items: Array<{ score: number; id: string | number }>;
|
|
210
|
+
};
|
|
211
|
+
getScoreFunction(search: any, options?: any): (item: any) => number;
|
|
212
|
+
getSortFunction(search: any, options?: any): ((a: any, b: any) => number) | null;
|
|
213
|
+
prepareSearch(query: string | object, options?: any): any;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function escapeHtml(str: string): string;
|
|
217
|
+
export function hashKey(value: any): string | null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selectra - Alpine.js Plugin
|
|
3
|
+
*
|
|
4
|
+
* A powerful, extensible <select> UI control for tagging, contact lists,
|
|
5
|
+
* country selectors, and autocomplete. Built with Alpine.js and Tailwind CSS.
|
|
6
|
+
*
|
|
7
|
+
* @license Apache-2.0
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
*
|
|
11
|
+
* // Register the plugin
|
|
12
|
+
* import Alpine from 'alpinejs';
|
|
13
|
+
* import Selectra from 'selectra';
|
|
14
|
+
* Alpine.plugin(Selectra);
|
|
15
|
+
* Alpine.start();
|
|
16
|
+
*
|
|
17
|
+
* // In HTML (directive approach):
|
|
18
|
+
* <div x-data="selectra({ options: [...], create: true })">
|
|
19
|
+
* <template x-selectra></template>
|
|
20
|
+
* </div>
|
|
21
|
+
*
|
|
22
|
+
* // Or initialize on existing <select>:
|
|
23
|
+
* <div x-data="selectra()" x-selectra>
|
|
24
|
+
* <select>
|
|
25
|
+
* <option value="1">One</option>
|
|
26
|
+
* <option value="2">Two</option>
|
|
27
|
+
* </select>
|
|
28
|
+
* </div>
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createSelectizeComponent, registerPlugin, getDefaults, DEFAULTS } from './selectize.js';
|
|
32
|
+
import { escapeHtml, hashKey, autoGrow } from './utils.js';
|
|
33
|
+
import Sifter from './sifter.js';
|
|
34
|
+
|
|
35
|
+
// Import Tailwind CSS styles
|
|
36
|
+
import './styles/selectra.css';
|
|
37
|
+
|
|
38
|
+
// Load built-in plugins
|
|
39
|
+
import './plugins/index.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Alpine.js plugin installer
|
|
43
|
+
*/
|
|
44
|
+
function SelectraPlugin(Alpine) {
|
|
45
|
+
// Register the selectra data component
|
|
46
|
+
Alpine.data('selectra', (config = {}) => {
|
|
47
|
+
const componentFactory = createSelectizeComponent(config);
|
|
48
|
+
return componentFactory();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Register the x-selectra directive for auto-rendering
|
|
52
|
+
Alpine.directive('selectra', (el, { expression }, { evaluate, cleanup }) => {
|
|
53
|
+
// The directive is mainly a marker — the template is in the component.
|
|
54
|
+
// It can be used with x-data="selectra({...})" for auto-init.
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
SelectraPlugin.version = '1.0.0';
|
|
59
|
+
|
|
60
|
+
// Named exports
|
|
61
|
+
export {
|
|
62
|
+
SelectraPlugin as default,
|
|
63
|
+
createSelectizeComponent,
|
|
64
|
+
registerPlugin,
|
|
65
|
+
getDefaults,
|
|
66
|
+
DEFAULTS,
|
|
67
|
+
Sifter,
|
|
68
|
+
escapeHtml,
|
|
69
|
+
hashKey,
|
|
70
|
+
};
|