thekselect 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 +21 -0
- package/README.md +338 -0
- package/dist/core/config-utils.d.ts +5 -0
- package/dist/core/dom-renderer.d.ts +38 -0
- package/dist/core/event-emitter.d.ts +7 -0
- package/dist/core/options-logic.d.ts +4 -0
- package/dist/core/selection-logic.d.ts +20 -0
- package/dist/core/state.d.ts +10 -0
- package/dist/core/thekselect.d.ts +42 -0
- package/dist/core/types.d.ts +40 -0
- package/dist/css/base.css +37 -0
- package/dist/css/blue.css +14 -0
- package/dist/css/bootstrap.css +15 -0
- package/dist/css/dark.css +14 -0
- package/dist/css/forest.css +14 -0
- package/dist/css/gray.css +14 -0
- package/dist/css/material.css +15 -0
- package/dist/css/red.css +14 -0
- package/dist/css/tailwind.css +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/thekselect.js +1169 -0
- package/dist/thekselect.min.js +1 -0
- package/dist/thekselect.umd.cjs +1171 -0
- package/dist/thekselect.umd.min.cjs +1 -0
- package/dist/utils/debounce.d.ts +4 -0
- package/dist/utils/dom.d.ts +2 -0
- package/dist/utils/event-manager.d.ts +15 -0
- package/dist/utils/styles.d.ts +2 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thekster
|
|
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,338 @@
|
|
|
1
|
+
# ThekSelect
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic, and accessible select library with native drag-and-drop tag reordering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Headless core with renderer/state separation
|
|
8
|
+
- Search with debounce support
|
|
9
|
+
- Single and multi-select modes
|
|
10
|
+
- User-created tags (`canCreate`)
|
|
11
|
+
- Remote async options (`loadOptions`)
|
|
12
|
+
- Native HTML5 drag-and-drop for selected tags
|
|
13
|
+
- Keyboard-friendly and ARIA-aware behavior
|
|
14
|
+
- Height control via `height` option (`number` or CSS string)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install thekselect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Release
|
|
23
|
+
|
|
24
|
+
Before publishing:
|
|
25
|
+
|
|
26
|
+
1. Ensure the package name and version in `package.json` are correct.
|
|
27
|
+
2. Add an `NPM_TOKEN` repository secret in GitHub.
|
|
28
|
+
3. Push the commit you want to publish.
|
|
29
|
+
|
|
30
|
+
Verify the package locally:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run release:check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Publish through GitHub Actions:
|
|
37
|
+
|
|
38
|
+
1. Bump `package.json` to the version you want to publish.
|
|
39
|
+
2. Push the version commit to GitHub.
|
|
40
|
+
3. Trigger one of these:
|
|
41
|
+
- Create and publish a GitHub Release.
|
|
42
|
+
- Open `Actions` -> `Publish to npm` -> `Run workflow`.
|
|
43
|
+
|
|
44
|
+
Manual fallback from a local terminal:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run release:check
|
|
48
|
+
npm publish --access public
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`prepublishOnly` runs the same release checks before `npm publish`.
|
|
52
|
+
|
|
53
|
+
## CSS Themes
|
|
54
|
+
|
|
55
|
+
Import one of the distributed theme files:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import 'thekselect/css/base.css';
|
|
59
|
+
// or
|
|
60
|
+
import 'thekselect/css/dark.css';
|
|
61
|
+
// or
|
|
62
|
+
import 'thekselect/css/forest.css';
|
|
63
|
+
// or
|
|
64
|
+
import 'thekselect/css/red.css';
|
|
65
|
+
// or
|
|
66
|
+
import 'thekselect/css/blue.css';
|
|
67
|
+
// or
|
|
68
|
+
import 'thekselect/css/gray.css';
|
|
69
|
+
// or
|
|
70
|
+
import 'thekselect/css/bootstrap.css';
|
|
71
|
+
// or
|
|
72
|
+
import 'thekselect/css/tailwind.css';
|
|
73
|
+
// or
|
|
74
|
+
import 'thekselect/css/material.css';
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`base.css` is the system theme: light defaults with automatic dark-mode tokens via `prefers-color-scheme`.
|
|
78
|
+
|
|
79
|
+
Customize tokens with plain CSS:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
:root {
|
|
83
|
+
--thek-primary: #1d4ed8;
|
|
84
|
+
--thek-border-radius: 12px;
|
|
85
|
+
--thek-height-md: 44px;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick Start
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<select id="my-select">
|
|
93
|
+
<option value="">Choose one...</option>
|
|
94
|
+
<option value="react">React</option>
|
|
95
|
+
<option value="vue">Vue</option>
|
|
96
|
+
</select>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { ThekSelect } from 'thekselect';
|
|
101
|
+
|
|
102
|
+
const select = ThekSelect.init('#my-select', {
|
|
103
|
+
placeholder: 'Select an option...'
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Browser `<script>` Tag (No Bundler)
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<link rel="stylesheet" href="./dist/css/base.css" />
|
|
111
|
+
<script src="./dist/thekselect.umd.min.cjs"></script>
|
|
112
|
+
|
|
113
|
+
<select id="my-select">
|
|
114
|
+
<option value="react">React</option>
|
|
115
|
+
<option value="vue">Vue</option>
|
|
116
|
+
</select>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
const { ThekSelect } = window.ThekSelect;
|
|
120
|
+
ThekSelect.init('#my-select');
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If your static server does not serve `.cjs` files as JavaScript, rename the UMD output to `.js` for browser delivery.
|
|
125
|
+
|
|
126
|
+
### Initialize From Existing `<select>`
|
|
127
|
+
|
|
128
|
+
If options already exist in your native `<select>`, initialize directly from it:
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<select id="country-select" multiple>
|
|
132
|
+
<option value="us" selected>United States</option>
|
|
133
|
+
<option value="de">Germany</option>
|
|
134
|
+
<option value="jp" disabled>Japan (disabled)</option>
|
|
135
|
+
</select>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
import { ThekSelect } from 'thekselect';
|
|
140
|
+
|
|
141
|
+
ThekSelect.init('#country-select');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`ThekSelect` reads existing `<option>` values, labels, `selected`, `disabled`, and `multiple` automatically.
|
|
145
|
+
|
|
146
|
+
### Large Datasets (Virtualization)
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<div id="big-list"></div>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
ThekSelect.init('#big-list', {
|
|
154
|
+
options: hugeOptions,
|
|
155
|
+
searchable: true,
|
|
156
|
+
virtualize: true,
|
|
157
|
+
virtualThreshold: 80,
|
|
158
|
+
virtualItemHeight: 40,
|
|
159
|
+
virtualOverscan: 4
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Global Defaults (App-wide)
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<div id="first"></div>
|
|
167
|
+
<div id="second"></div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { ThekSelect } from 'thekselect';
|
|
172
|
+
|
|
173
|
+
ThekSelect.setDefaults({
|
|
174
|
+
height: 40,
|
|
175
|
+
virtualize: true
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Uses global defaults
|
|
179
|
+
ThekSelect.init('#first');
|
|
180
|
+
|
|
181
|
+
// Instance options still override globals
|
|
182
|
+
ThekSelect.init('#second', { height: 52 });
|
|
183
|
+
|
|
184
|
+
// Optional cleanup (tests, app teardown, etc.)
|
|
185
|
+
ThekSelect.resetDefaults();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Development (Repo)
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm install
|
|
192
|
+
npm test
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Scripts
|
|
196
|
+
|
|
197
|
+
| Script | Purpose |
|
|
198
|
+
| ------------------ | ----------------------------------------------------------------- |
|
|
199
|
+
| `npm run dev` | Start Vite dev server for local development/showcase. |
|
|
200
|
+
| `npm run build` | Build library outputs into `dist/` and emit types. |
|
|
201
|
+
| `npm run preview` | Preview built output with Vite. Optional for library development. |
|
|
202
|
+
| `npm test` | Run test suite in watch mode. |
|
|
203
|
+
| `npm run coverage` | Run tests with coverage report. |
|
|
204
|
+
|
|
205
|
+
Notes:
|
|
206
|
+
|
|
207
|
+
- `preview` is optional for day-to-day library work.
|
|
208
|
+
- For this repo, tests are the primary correctness check when bundle preview is unavailable.
|
|
209
|
+
|
|
210
|
+
## Showcase
|
|
211
|
+
|
|
212
|
+
The demo page is `showcase/index.html` and is intended to run through Vite during development.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm run dev
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then open the local URL printed by Vite.
|
|
219
|
+
|
|
220
|
+
## Configuration
|
|
221
|
+
|
|
222
|
+
| Option | Type | Default | Description |
|
|
223
|
+
| ------------------- | ---------------------------------------- | ------------------------ | ------------------------------------------------------ |
|
|
224
|
+
| `options` | `ThekSelectOption[]` | `[]` | Initial options list. |
|
|
225
|
+
| `multiple` | `boolean` | `false` | Enable multi-select mode. |
|
|
226
|
+
| `searchable` | `boolean` | `true` | Enable search input and filtering. |
|
|
227
|
+
| `disabled` | `boolean` | `false` | Disable interaction. |
|
|
228
|
+
| `placeholder` | `string` | `'Select...'` | Placeholder text. |
|
|
229
|
+
| `canCreate` | `boolean` | `false` | Allow creating new options from input. |
|
|
230
|
+
| `createText` | `string` | `"Create '{%t}'..."` | Create row label template (`{%t}` = typed text). |
|
|
231
|
+
| `height` | `number \| string` | `40` | Control control/dropdown input height (`number` = px). |
|
|
232
|
+
| `debounce` | `number` | `300` | Debounce delay for `loadOptions`. |
|
|
233
|
+
| `maxSelectedLabels` | `number` | `3` | Max visible tags before summary text. |
|
|
234
|
+
| `displayField` | `string` | `'label'` | Field used for rendering text. |
|
|
235
|
+
| `valueField` | `string` | `'value'` | Field used as internal value key. |
|
|
236
|
+
| `maxOptions` | `number \| null` | `null` | Limit rendered dropdown items. |
|
|
237
|
+
| `virtualize` | `boolean` | `false` | Enable virtualized option rendering for large lists. |
|
|
238
|
+
| `virtualItemHeight` | `number` | `40` | Row height (px) used by virtualization calculations. |
|
|
239
|
+
| `virtualOverscan` | `number` | `4` | Extra rows rendered above and below viewport. |
|
|
240
|
+
| `virtualThreshold` | `number` | `80` | Minimum option count before virtualization activates. |
|
|
241
|
+
| `loadOptions` | `(query) => Promise<ThekSelectOption[]>` | `undefined` | Async loader for remote search. |
|
|
242
|
+
| `renderOption` | `(option) => string \| HTMLElement` | `(o) => o[displayField]` | Custom dropdown renderer. |
|
|
243
|
+
| `renderSelection` | `(option) => string \| HTMLElement` | `(o) => o[displayField]` | Custom selected-item renderer. |
|
|
244
|
+
|
|
245
|
+
## Option Shape
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
interface ThekSelectOption {
|
|
249
|
+
value: string;
|
|
250
|
+
label: string;
|
|
251
|
+
disabled?: boolean;
|
|
252
|
+
selected?: boolean;
|
|
253
|
+
data?: any;
|
|
254
|
+
[key: string]: any;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Field | Type | Description |
|
|
259
|
+
| --------------- | --------- | ------------------------------------------------------------------ |
|
|
260
|
+
| `value` | `string` | Stable internal value used for selection state and emitted values. |
|
|
261
|
+
| `label` | `string` | Default display text shown in dropdown and selected state. |
|
|
262
|
+
| `disabled` | `boolean` | Prevents selecting the option when `true`. |
|
|
263
|
+
| `selected` | `boolean` | Marks initial selection when options are provided at init time. |
|
|
264
|
+
| `data` | `any` | Optional extra metadata for custom rendering and app logic. |
|
|
265
|
+
| `[key: string]` | `any` | Allows custom fields for `displayField`/`valueField` mapping. |
|
|
266
|
+
|
|
267
|
+
When using custom fields (`valueField`, `displayField`), keep `value` and `label` available if you share the same option objects across default and custom-field instances.
|
|
268
|
+
|
|
269
|
+
## API Methods
|
|
270
|
+
|
|
271
|
+
- `getValue()`: Return selected value (`string`), values (`string[]`), or `undefined` when single-select has no selection.
|
|
272
|
+
- `getSelectedOptions()`: Return selected option object(s), or `undefined` when single-select has no selection.
|
|
273
|
+
- `setValue(value, silent = false)`: Set current selection programmatically.
|
|
274
|
+
- `setHeight(height)`: Update height at runtime (`number` = px or CSS string like `'2.75rem'`).
|
|
275
|
+
- `setMaxOptions(limit)`: Update dropdown item limit.
|
|
276
|
+
- `setRenderOption(callback)`: Update option rendering function.
|
|
277
|
+
- `ThekSelect.setDefaults(defaults)`: Set global defaults for future instances.
|
|
278
|
+
- `ThekSelect.resetDefaults()`: Clear global defaults.
|
|
279
|
+
- `on(event, callback)`: Subscribe to component events. Returns an unsubscribe function.
|
|
280
|
+
- `destroy()`: Tear down and restore original element.
|
|
281
|
+
|
|
282
|
+
## API Reference
|
|
283
|
+
|
|
284
|
+
| Method | Parameters | Returns | Description |
|
|
285
|
+
| --------------------------- | ------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `getValue()` | none | `string \| string[] \| undefined` | Gets current selected value(s) from component state. |
|
|
287
|
+
| `getSelectedOptions()` | none | `ThekSelectOption \| ThekSelectOption[] \| undefined` | Gets current selected option object(s). |
|
|
288
|
+
| `setValue(value, silent?)` | `value: string \| string[]`, `silent?: boolean` | `void` | Sets selected value(s). In single mode keeps first value; in multi mode de-duplicates values. |
|
|
289
|
+
| `setHeight(height)` | `height: number \| string` | `void` | Sets control height for the instance. Numeric input is treated as px. |
|
|
290
|
+
| `setMaxOptions(limit)` | `limit: number \| null` | `void` | Limits filtered options rendered in dropdown (`null` disables limit). |
|
|
291
|
+
| `setRenderOption(callback)` | `callback: (option) => string \| HTMLElement` | `void` | Overrides dropdown option rendering for this instance. |
|
|
292
|
+
| `on(event, callback)` | `event: ThekSelectEvent`, `callback: (payload) => void` | `() => void` | Subscribes to an event and returns an unsubscribe function. |
|
|
293
|
+
| `destroy()` | none | `void` | Removes generated DOM/listeners and restores original element visibility. |
|
|
294
|
+
|
|
295
|
+
| Static Method | Parameters | Returns | Description |
|
|
296
|
+
| ----------------------------------- | ------------------------------------------------------------- | ------------ | ------------------------------------------------ |
|
|
297
|
+
| `ThekSelect.init(element, config?)` | `element: string \| HTMLElement`, `config?: ThekSelectConfig` | `ThekSelect` | Creates and initializes a new instance. |
|
|
298
|
+
| `ThekSelect.setDefaults(defaults)` | `defaults: Partial<ThekSelectConfig>` | `void` | Sets process-wide defaults for future instances. |
|
|
299
|
+
| `ThekSelect.resetDefaults()` | none | `void` | Clears global defaults. |
|
|
300
|
+
|
|
301
|
+
## Events
|
|
302
|
+
|
|
303
|
+
- `change`: Selection changed (value or values).
|
|
304
|
+
- `open`: Dropdown opened.
|
|
305
|
+
- `close`: Dropdown closed.
|
|
306
|
+
- `search`: Search input changed.
|
|
307
|
+
- `tagAdded`: Tag added in multi mode.
|
|
308
|
+
- `tagRemoved`: Tag removed in multi mode.
|
|
309
|
+
- `reordered`: Selected tags reordered by drag-and-drop.
|
|
310
|
+
|
|
311
|
+
## Troubleshooting
|
|
312
|
+
|
|
313
|
+
### `Error: spawn EPERM` on `npm run dev`, `npm run build`, or `npm run preview` (Windows)
|
|
314
|
+
|
|
315
|
+
This is typically an environment policy/permissions issue around spawning build tooling (e.g. esbuild), not a ThekSelect API issue.
|
|
316
|
+
|
|
317
|
+
Recommended checks:
|
|
318
|
+
|
|
319
|
+
1. Reinstall dependencies:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
rmdir /s /q node_modules
|
|
323
|
+
npm install
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
2. Verify esbuild binary can execute:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
node_modules\\@esbuild\\win32-x64\\esbuild.exe --version
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
3. If blocked, review antivirus/EDR/AppLocker/Windows Security rules for child-process execution from your workspace.
|
|
333
|
+
|
|
334
|
+
4. Use `npm test` to continue validating behavior while fixing build environment constraints.
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ThekSelectConfig, ThekSelectOption, ThekSelectState } from './types.js';
|
|
2
|
+
export declare const NOOP_LOAD_OPTIONS: (_query: string) => Promise<ThekSelectOption[]>;
|
|
3
|
+
export declare function parseSelectOptions(select: HTMLSelectElement): ThekSelectOption[];
|
|
4
|
+
export declare function buildConfig<T = unknown>(element: HTMLElement, config: ThekSelectConfig<T>, globalDefaults?: Partial<ThekSelectConfig<T>>): Required<ThekSelectConfig<T>>;
|
|
5
|
+
export declare function buildInitialState<T = unknown>(config: Required<ThekSelectConfig<T>>): ThekSelectState<T>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ThekSelectConfig, ThekSelectState, ThekSelectOption } from './types.js';
|
|
2
|
+
export interface RendererCallbacks {
|
|
3
|
+
onSelect: (option: ThekSelectOption) => void;
|
|
4
|
+
onCreate: (label: string) => void;
|
|
5
|
+
onRemove: (option: ThekSelectOption) => void;
|
|
6
|
+
onReorder: (from: number, to: number) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class DomRenderer {
|
|
9
|
+
private config;
|
|
10
|
+
private id;
|
|
11
|
+
private callbacks;
|
|
12
|
+
wrapper: HTMLElement;
|
|
13
|
+
control: HTMLElement;
|
|
14
|
+
selectionContainer: HTMLElement;
|
|
15
|
+
indicatorsContainer: HTMLElement;
|
|
16
|
+
placeholderElement: HTMLElement;
|
|
17
|
+
input: HTMLInputElement;
|
|
18
|
+
dropdown: HTMLElement;
|
|
19
|
+
optionsList: HTMLElement;
|
|
20
|
+
private lastState;
|
|
21
|
+
private lastFilteredOptions;
|
|
22
|
+
constructor(config: Required<ThekSelectConfig>, id: string, callbacks: RendererCallbacks);
|
|
23
|
+
private normalizeHeight;
|
|
24
|
+
private applyHeight;
|
|
25
|
+
createDom(): void;
|
|
26
|
+
render(state: ThekSelectState, filteredOptions: ThekSelectOption[]): void;
|
|
27
|
+
private renderSelectionContent;
|
|
28
|
+
private renderOptionsContent;
|
|
29
|
+
private createSpacer;
|
|
30
|
+
private createOptionItem;
|
|
31
|
+
private handleOptionsScroll;
|
|
32
|
+
private handleOptionsWheel;
|
|
33
|
+
positionDropdown(): void;
|
|
34
|
+
private setupTagDnd;
|
|
35
|
+
setHeight(height: number | string): void;
|
|
36
|
+
updateConfig(newConfig: Partial<Required<ThekSelectConfig>>): void;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ThekSelectEvent } from './types.js';
|
|
2
|
+
export declare class ThekSelectEventEmitter {
|
|
3
|
+
private listeners;
|
|
4
|
+
on(event: ThekSelectEvent, callback: Function): () => void;
|
|
5
|
+
off(event: ThekSelectEvent, callback: Function): void;
|
|
6
|
+
emit(event: ThekSelectEvent, data: unknown): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ThekSelectConfig, ThekSelectOption, ThekSelectState } from './types.js';
|
|
2
|
+
export declare function isRemoteMode<T = unknown>(config: Required<ThekSelectConfig<T>>): boolean;
|
|
3
|
+
export declare function getFilteredOptions<T = unknown>(config: Required<ThekSelectConfig<T>>, state: ThekSelectState<T>): ThekSelectOption<T>[];
|
|
4
|
+
export declare function mergeSelectedOptionsByValue<T = unknown>(valueField: string, selectedValues: string[], previous: Record<string, ThekSelectOption<T>>, latestOptions: ThekSelectOption<T>[]): Record<string, ThekSelectOption<T>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ThekSelectConfig, ThekSelectOption, ThekSelectState } from './types.js';
|
|
2
|
+
type TagEvent = 'tagAdded' | 'tagRemoved';
|
|
3
|
+
export interface SelectionUpdate<T = unknown> {
|
|
4
|
+
selectedValues: string[];
|
|
5
|
+
selectedOptionsByValue: Record<string, ThekSelectOption<T>>;
|
|
6
|
+
inputValue?: string;
|
|
7
|
+
tagEvent?: TagEvent;
|
|
8
|
+
tagOption?: ThekSelectOption<T>;
|
|
9
|
+
}
|
|
10
|
+
export declare function applySelection<T = unknown>(config: Required<ThekSelectConfig<T>>, state: ThekSelectState<T>, option: ThekSelectOption<T>): SelectionUpdate<T>;
|
|
11
|
+
export declare function createOptionFromLabel<T = unknown>(config: Required<ThekSelectConfig<T>>, label: string): ThekSelectOption<T>;
|
|
12
|
+
export declare function removeLastSelection<T = unknown>(config: Required<ThekSelectConfig<T>>, state: ThekSelectState<T>): {
|
|
13
|
+
selectedValues: string[];
|
|
14
|
+
selectedOptionsByValue: Record<string, ThekSelectOption<T>>;
|
|
15
|
+
removedOption?: ThekSelectOption<T>;
|
|
16
|
+
};
|
|
17
|
+
export declare function reorderSelectedValues(state: ThekSelectState, from: number, to: number): string[];
|
|
18
|
+
export declare function resolveSelectedOptions<T = unknown>(config: Required<ThekSelectConfig<T>>, state: ThekSelectState<T>): ThekSelectOption<T>[];
|
|
19
|
+
export declare function buildSelectedOptionsMapFromValues<T = unknown>(config: Required<ThekSelectConfig<T>>, state: ThekSelectState<T>, values: string[]): Record<string, ThekSelectOption<T>>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type StateListener<T> = (state: T) => void;
|
|
2
|
+
export declare class StateManager<T extends object> {
|
|
3
|
+
private state;
|
|
4
|
+
private listeners;
|
|
5
|
+
constructor(initialState: T);
|
|
6
|
+
getState(): T;
|
|
7
|
+
setState(newState: Partial<T>): void;
|
|
8
|
+
subscribe(listener: StateListener<T>): () => void;
|
|
9
|
+
private notify;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ThekSelectConfig, ThekSelectOption, ThekSelectEvent } from './types.js';
|
|
2
|
+
export declare class ThekSelect<T = unknown> {
|
|
3
|
+
private static globalDefaults;
|
|
4
|
+
private config;
|
|
5
|
+
private stateManager;
|
|
6
|
+
private renderer;
|
|
7
|
+
private id;
|
|
8
|
+
private events;
|
|
9
|
+
private originalElement;
|
|
10
|
+
private unsubscribeEvents;
|
|
11
|
+
private unsubscribeState?;
|
|
12
|
+
private remoteRequestId;
|
|
13
|
+
private isDestroyed;
|
|
14
|
+
private focusTimeoutId;
|
|
15
|
+
private constructor();
|
|
16
|
+
static init<T = unknown>(element: string | HTMLElement, config?: ThekSelectConfig<T>): ThekSelect<T>;
|
|
17
|
+
static setDefaults(defaults: Partial<ThekSelectConfig>): void;
|
|
18
|
+
static resetDefaults(): void;
|
|
19
|
+
private initialize;
|
|
20
|
+
private setupListeners;
|
|
21
|
+
private handleSearch;
|
|
22
|
+
private setupHandleSearch;
|
|
23
|
+
private handleKeyDown;
|
|
24
|
+
private toggleDropdown;
|
|
25
|
+
private openDropdown;
|
|
26
|
+
private closeDropdown;
|
|
27
|
+
private handleSelect;
|
|
28
|
+
private handleCreate;
|
|
29
|
+
private handleRemoveLastSelection;
|
|
30
|
+
private handleReorder;
|
|
31
|
+
private syncOriginalElement;
|
|
32
|
+
private render;
|
|
33
|
+
on(event: ThekSelectEvent, callback: Function): () => void;
|
|
34
|
+
private emit;
|
|
35
|
+
getValue(): string | string[] | undefined;
|
|
36
|
+
getSelectedOptions(): ThekSelectOption<T> | ThekSelectOption<T>[] | undefined;
|
|
37
|
+
setValue(value: string | string[], silent?: boolean): void;
|
|
38
|
+
setHeight(height: number | string): void;
|
|
39
|
+
setRenderOption(callback: (option: ThekSelectOption<T>) => string | HTMLElement): void;
|
|
40
|
+
setMaxOptions(max: number | null): void;
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ThekSelectOption<T = unknown> {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface ThekSelectConfig<T = unknown> {
|
|
10
|
+
options?: ThekSelectOption<T>[];
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
searchable?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
canCreate?: boolean;
|
|
16
|
+
createText?: string;
|
|
17
|
+
height?: number | string;
|
|
18
|
+
debounce?: number;
|
|
19
|
+
maxSelectedLabels?: number;
|
|
20
|
+
displayField?: string;
|
|
21
|
+
valueField?: string;
|
|
22
|
+
maxOptions?: number | null;
|
|
23
|
+
virtualize?: boolean;
|
|
24
|
+
virtualItemHeight?: number;
|
|
25
|
+
virtualOverscan?: number;
|
|
26
|
+
virtualThreshold?: number;
|
|
27
|
+
loadOptions?: (query: string) => Promise<ThekSelectOption<T>[]>;
|
|
28
|
+
renderOption?: (option: ThekSelectOption<T>) => string | HTMLElement;
|
|
29
|
+
renderSelection?: (option: ThekSelectOption<T>) => string | HTMLElement;
|
|
30
|
+
}
|
|
31
|
+
export interface ThekSelectState<T = unknown> {
|
|
32
|
+
options: ThekSelectOption<T>[];
|
|
33
|
+
selectedValues: string[];
|
|
34
|
+
selectedOptionsByValue: Record<string, ThekSelectOption<T>>;
|
|
35
|
+
isOpen: boolean;
|
|
36
|
+
focusedIndex: number;
|
|
37
|
+
inputValue: string;
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type ThekSelectEvent = 'change' | 'open' | 'close' | 'search' | 'tagAdded' | 'tagRemoved' | 'reordered';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--thek-primary: #0f172a;
|
|
3
|
+
--thek-primary-light: #f1f5f9;
|
|
4
|
+
--thek-bg-surface: #ffffff;
|
|
5
|
+
--thek-bg-panel: #f8fafc;
|
|
6
|
+
--thek-bg-subtle: #f1f5f9;
|
|
7
|
+
--thek-border: #e2e8f0;
|
|
8
|
+
--thek-border-strong: #cbd5e1;
|
|
9
|
+
--thek-text-main: #0f172a;
|
|
10
|
+
--thek-text-muted: #64748b;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #ef4444;
|
|
13
|
+
--thek-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
14
|
+
--thek-font-family: inherit;
|
|
15
|
+
--thek-border-radius: 8px;
|
|
16
|
+
--thek-height-sm: 32px;
|
|
17
|
+
--thek-height-md: 40px;
|
|
18
|
+
--thek-height-lg: 48px;
|
|
19
|
+
--thek-item-padding: 8px 10px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root {
|
|
24
|
+
--thek-primary: #38bdf8;
|
|
25
|
+
--thek-primary-light: rgba(56, 189, 248, 0.15);
|
|
26
|
+
--thek-bg-surface: #0f172a;
|
|
27
|
+
--thek-bg-panel: #334155;
|
|
28
|
+
--thek-bg-subtle: #475569;
|
|
29
|
+
--thek-border: #334155;
|
|
30
|
+
--thek-border-strong: #475569;
|
|
31
|
+
--thek-text-main: #f8fafc;
|
|
32
|
+
--thek-text-muted: #94a3b8;
|
|
33
|
+
--thek-text-inverse: #0f172a;
|
|
34
|
+
--thek-danger: #f43f5e;
|
|
35
|
+
--thek-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
html[data-thek-theme='blue'] {
|
|
2
|
+
--thek-primary: #1d4ed8;
|
|
3
|
+
--thek-primary-light: #dbeafe;
|
|
4
|
+
--thek-bg-surface: #f8fbff;
|
|
5
|
+
--thek-bg-panel: #eff6ff;
|
|
6
|
+
--thek-bg-subtle: #dbeafe;
|
|
7
|
+
--thek-border: #bfdbfe;
|
|
8
|
+
--thek-border-strong: #60a5fa;
|
|
9
|
+
--thek-text-main: #1e3a8a;
|
|
10
|
+
--thek-text-muted: #1d4ed8;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #dc2626;
|
|
13
|
+
--thek-shadow: 0 8px 18px -8px rgba(29, 78, 216, 0.3);
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
html[data-thek-theme='bootstrap'] {
|
|
2
|
+
--thek-primary: #0d6efd;
|
|
3
|
+
--thek-primary-light: #cfe2ff;
|
|
4
|
+
--thek-bg-surface: #ffffff;
|
|
5
|
+
--thek-bg-panel: #f8f9fa;
|
|
6
|
+
--thek-bg-subtle: #e9ecef;
|
|
7
|
+
--thek-border: #dee2e6;
|
|
8
|
+
--thek-border-strong: #adb5bd;
|
|
9
|
+
--thek-text-main: #212529;
|
|
10
|
+
--thek-text-muted: #6c757d;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #dc3545;
|
|
13
|
+
--thek-shadow: 0 10px 20px -10px rgba(33, 37, 41, 0.25);
|
|
14
|
+
--thek-border-radius: 6px;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
html[data-thek-theme='dark'] {
|
|
2
|
+
--thek-primary: #38bdf8;
|
|
3
|
+
--thek-primary-light: rgba(56, 189, 248, 0.15);
|
|
4
|
+
--thek-bg-surface: #0f172a;
|
|
5
|
+
--thek-bg-panel: #334155;
|
|
6
|
+
--thek-bg-subtle: #475569;
|
|
7
|
+
--thek-border: #334155;
|
|
8
|
+
--thek-border-strong: #475569;
|
|
9
|
+
--thek-text-main: #f8fafc;
|
|
10
|
+
--thek-text-muted: #94a3b8;
|
|
11
|
+
--thek-text-inverse: #0f172a;
|
|
12
|
+
--thek-danger: #f43f5e;
|
|
13
|
+
--thek-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
html[data-thek-theme='forest'] {
|
|
2
|
+
--thek-primary: #14532d;
|
|
3
|
+
--thek-primary-light: #dcfce7;
|
|
4
|
+
--thek-bg-surface: #f7fee7;
|
|
5
|
+
--thek-bg-panel: #ecfccb;
|
|
6
|
+
--thek-bg-subtle: #d9f99d;
|
|
7
|
+
--thek-border: #a3e635;
|
|
8
|
+
--thek-border-strong: #65a30d;
|
|
9
|
+
--thek-text-main: #1f2937;
|
|
10
|
+
--thek-text-muted: #4b5563;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #dc2626;
|
|
13
|
+
--thek-shadow: 0 8px 18px -6px rgba(20, 83, 45, 0.2);
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
html[data-thek-theme='gray'] {
|
|
2
|
+
--thek-primary: #374151;
|
|
3
|
+
--thek-primary-light: #e5e7eb;
|
|
4
|
+
--thek-bg-surface: #f9fafb;
|
|
5
|
+
--thek-bg-panel: #f3f4f6;
|
|
6
|
+
--thek-bg-subtle: #e5e7eb;
|
|
7
|
+
--thek-border: #d1d5db;
|
|
8
|
+
--thek-border-strong: #9ca3af;
|
|
9
|
+
--thek-text-main: #111827;
|
|
10
|
+
--thek-text-muted: #4b5563;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #ef4444;
|
|
13
|
+
--thek-shadow: 0 8px 18px -8px rgba(55, 65, 81, 0.22);
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
html[data-thek-theme='material'] {
|
|
2
|
+
--thek-primary: #6750a4;
|
|
3
|
+
--thek-primary-light: #eaddff;
|
|
4
|
+
--thek-bg-surface: #fffbfe;
|
|
5
|
+
--thek-bg-panel: #f7f2fa;
|
|
6
|
+
--thek-bg-subtle: #ede7f6;
|
|
7
|
+
--thek-border: #d0c4dc;
|
|
8
|
+
--thek-border-strong: #b39ddb;
|
|
9
|
+
--thek-text-main: #1d1b20;
|
|
10
|
+
--thek-text-muted: #625b71;
|
|
11
|
+
--thek-text-inverse: #ffffff;
|
|
12
|
+
--thek-danger: #b3261e;
|
|
13
|
+
--thek-shadow: 0 10px 20px -10px rgba(103, 80, 164, 0.35);
|
|
14
|
+
--thek-border-radius: 10px;
|
|
15
|
+
}
|