svelte-multiselect 11.2.2 → 11.2.4

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.
@@ -1,48 +1,36 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type Item = string | [string, unknown];
3
- declare class __sveltets_Render<T extends Item> {
4
- props(): {
5
- [key: string]: unknown;
6
- items?: T[] | undefined;
7
- node?: string;
8
- current?: string;
9
- log?: `verbose` | `errors` | `silent`;
10
- nav_options?: {
11
- replace_state: boolean;
12
- no_scroll: boolean;
13
- } | undefined;
14
- titles?: {
15
- prev: string;
16
- next: string;
17
- } | undefined;
18
- onkeyup?: ((obj: {
19
- prev: Item;
20
- next: Item;
21
- }) => Record<string, string>) | null | undefined;
22
- prev_snippet?: Snippet<[{
23
- item: Item;
24
- }]> | undefined;
25
- children?: Snippet<[{
26
- kind: `prev` | `next`;
27
- item: Item;
28
- }]> | undefined;
29
- between?: Snippet<[]>;
30
- next_snippet?: Snippet<[{
31
- item: Item;
32
- }]> | undefined;
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ export type Item = [string, unknown];
4
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, `children` | `onkeyup`> {
5
+ items?: (string | Item)[];
6
+ node?: string;
7
+ current?: string;
8
+ log?: `verbose` | `errors` | `silent`;
9
+ nav_options?: {
10
+ replace_state: boolean;
11
+ no_scroll: boolean;
33
12
  };
34
- events(): {};
35
- slots(): {};
36
- bindings(): "";
37
- exports(): {};
38
- }
39
- interface $$IsomorphicComponent {
40
- new <T extends Item>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
41
- $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
42
- } & ReturnType<__sveltets_Render<T>['exports']>;
43
- <T extends Item>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
44
- z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
13
+ titles?: {
14
+ prev: string;
15
+ next: string;
16
+ };
17
+ onkeyup?: ((obj: {
18
+ prev: Item;
19
+ next: Item;
20
+ }) => Record<string, string | undefined>) | null;
21
+ prev_snippet?: Snippet<[{
22
+ item: Item;
23
+ }]>;
24
+ children?: Snippet<[{
25
+ kind: `prev` | `next`;
26
+ item: Item;
27
+ }]>;
28
+ between?: Snippet<[]>;
29
+ next_snippet?: Snippet<[{
30
+ item: Item;
31
+ }]>;
32
+ min_items?: number;
45
33
  }
46
- declare const PrevNext: $$IsomorphicComponent;
47
- type PrevNext<T extends Item> = InstanceType<typeof PrevNext<T>>;
34
+ declare const PrevNext: import("svelte").Component<Props, {}, "">;
35
+ type PrevNext = ReturnType<typeof PrevNext>;
48
36
  export default PrevNext;
@@ -4,7 +4,8 @@ function handle_keydown(event) {
4
4
  onkeydown?.(event);
5
5
  if (event.key === `Enter`) {
6
6
  event.preventDefault();
7
- checked = !checked;
7
+ const target = event.target;
8
+ target.click(); // simulate real user toggle so 'change' is dispatched
8
9
  }
9
10
  }
10
11
  export {};
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
- interface Props {
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLLabelElement> {
3
4
  checked?: boolean;
4
5
  required?: boolean;
5
6
  input_style?: string;
@@ -9,7 +10,6 @@ interface Props {
9
10
  onblur?: (event: FocusEvent) => void;
10
11
  onkeydown?: (event: KeyboardEvent) => void;
11
12
  children?: Snippet<[]>;
12
- [key: string]: unknown;
13
13
  }
14
14
  declare const Toggle: import("svelte").Component<Props, {}, "checked">;
15
15
  type Toggle = ReturnType<typeof Toggle>;
@@ -25,22 +25,25 @@ export declare const sortable: ({ header_selector, asc_class, desc_class, sorted
25
25
  backgroundColor: string;
26
26
  } | undefined;
27
27
  }) => (node: HTMLElement) => () => void;
28
- type HighlightOptions = {
28
+ export type HighlightOptions = {
29
29
  query?: string;
30
30
  disabled?: boolean;
31
+ fuzzy?: boolean;
31
32
  node_filter?: (node: Node) => number;
32
33
  css_class?: string;
33
34
  };
34
- export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => void) | undefined;
35
- export declare const tooltip: (options?: {
35
+ export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => boolean) | undefined;
36
+ export interface TooltipOptions {
36
37
  content?: string;
37
38
  placement?: `top` | `bottom` | `left` | `right`;
38
39
  delay?: number;
39
- }) => Attachment;
40
- type ClickOutsideConfig<T extends HTMLElement> = {
40
+ disabled?: boolean;
41
+ style?: string;
42
+ }
43
+ export declare const tooltip: (options?: TooltipOptions) => Attachment;
44
+ export type ClickOutsideConfig<T extends HTMLElement> = {
41
45
  enabled?: boolean;
42
46
  exclude?: string[];
43
47
  callback?: (node: T, config: ClickOutsideConfig<T>) => void;
44
48
  };
45
49
  export declare const click_outside: <T extends HTMLElement>(config?: ClickOutsideConfig<T>) => (node: T) => () => void;
46
- export {};
@@ -7,7 +7,7 @@ export const draggable = (options = {}) => (element) => {
7
7
  // Use simple variables for maximum performance
8
8
  let dragging = false;
9
9
  let start = { x: 0, y: 0 };
10
- const initial = { left: 0, top: 0, width: 0 };
10
+ const initial = { left: 0, top: 0 };
11
11
  const handle = options.handle_selector
12
12
  ? node.querySelector(options.handle_selector)
13
13
  : node;
@@ -26,17 +26,14 @@ export const draggable = (options = {}) => (element) => {
26
26
  const rect = node.getBoundingClientRect();
27
27
  initial.left = rect.left;
28
28
  initial.top = rect.top;
29
- initial.width = rect.width;
30
29
  }
31
30
  else {
32
31
  // For other positioning, use offset values
33
32
  initial.left = node.offsetLeft;
34
33
  initial.top = node.offsetTop;
35
- initial.width = node.offsetWidth;
36
34
  }
37
35
  node.style.left = `${initial.left}px`;
38
36
  node.style.top = `${initial.top}px`;
39
- node.style.width = `${initial.width}px`;
40
37
  node.style.right = `auto`; // Prevent conflict with left
41
38
  start = { x: event.clientX, y: event.clientY };
42
39
  document.body.style.userSelect = `none`; // Prevent text selection during drag
@@ -161,11 +158,15 @@ export const sortable = ({ header_selector = `thead th`, asc_class = `table-sort
161
158
  };
162
159
  };
163
160
  export const highlight_matches = (ops) => (node) => {
164
- const { query = ``, disabled = false, node_filter = () => NodeFilter.FILTER_ACCEPT, css_class = `highlight-match`, } = ops;
165
- // clear previous ranges from HighlightRegistry
166
- CSS.highlights.clear();
167
- if (!query || disabled || typeof CSS === `undefined` || !CSS.highlights)
168
- return; // abort if CSS highlight API not supported
161
+ const { query = ``, disabled = false, fuzzy = false, node_filter = () => NodeFilter.FILTER_ACCEPT, css_class = `highlight-match`, } = ops;
162
+ // abort if CSS highlight API not supported
163
+ if (typeof CSS === `undefined` || !CSS.highlights)
164
+ return;
165
+ // always clear our own highlight first
166
+ CSS.highlights.delete(css_class);
167
+ // if disabled or empty query, stop after cleanup
168
+ if (!query || disabled)
169
+ return;
169
170
  const tree_walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
170
171
  acceptNode: node_filter,
171
172
  });
@@ -178,28 +179,57 @@ export const highlight_matches = (ops) => (node) => {
178
179
  // iterate over all text nodes and find matches
179
180
  const ranges = text_nodes.map((el) => {
180
181
  const text = el.textContent?.toLowerCase();
181
- const indices = [];
182
- let start_pos = 0;
183
- while (text && start_pos < text.length) {
184
- const index = text.indexOf(query, start_pos);
185
- if (index === -1)
186
- break;
187
- indices.push(index);
188
- start_pos = index + query.length;
182
+ if (!text)
183
+ return [];
184
+ const search = query.toLowerCase();
185
+ if (fuzzy) {
186
+ // Fuzzy highlighting: highlight individual characters that match in order
187
+ const matching_indices = [];
188
+ let search_idx = 0;
189
+ let target_idx = 0;
190
+ // Find matching character indices
191
+ while (search_idx < search.length && target_idx < text.length) {
192
+ if (search[search_idx] === text[target_idx]) {
193
+ matching_indices.push(target_idx);
194
+ search_idx++;
195
+ }
196
+ target_idx++;
197
+ }
198
+ // Only create ranges if we found all characters in order
199
+ if (search_idx === search.length) {
200
+ return matching_indices.map((index) => {
201
+ const range = new Range();
202
+ range.setStart(el, index);
203
+ range.setEnd(el, index + 1); // highlight single character
204
+ return range;
205
+ });
206
+ }
207
+ return [];
208
+ }
209
+ else {
210
+ // Substring highlighting: highlight consecutive substrings
211
+ const indices = [];
212
+ let start_pos = 0;
213
+ while (start_pos < text.length) {
214
+ const index = text.indexOf(search, start_pos);
215
+ if (index === -1)
216
+ break;
217
+ indices.push(index);
218
+ start_pos = index + search.length;
219
+ }
220
+ // create range object for each substring found in the text node
221
+ return indices.map((index) => {
222
+ const range = new Range();
223
+ range.setStart(el, index);
224
+ range.setEnd(el, index + search.length);
225
+ return range;
226
+ });
189
227
  }
190
- // create range object for each str found in the text node
191
- return indices.map((index) => {
192
- const range = new Range();
193
- range.setStart(el, index);
194
- range.setEnd(el, index + query?.length);
195
- return range;
196
- });
197
228
  });
198
229
  // create Highlight object from ranges and add to registry
199
230
  CSS.highlights.set(css_class, new Highlight(...ranges.flat()));
200
- return () => {
201
- CSS.highlights.delete(css_class);
202
- };
231
+ // Return cleanup function
232
+ return () => CSS.highlights.delete(css_class);
203
233
  };
204
234
  // Global tooltip state to ensure only one tooltip is shown at a time
205
235
  let current_tooltip = null;
@@ -223,7 +253,7 @@ export const tooltip = (options = {}) => (node) => {
223
253
  const safe_options = options || {};
224
254
  const cleanup_functions = [];
225
255
  function setup_tooltip(element) {
226
- if (!element)
256
+ if (!element || safe_options.disabled)
227
257
  return;
228
258
  const content = safe_options.content || element.title ||
229
259
  element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
@@ -240,19 +270,133 @@ export const tooltip = (options = {}) => (node) => {
240
270
  show_timeout = setTimeout(() => {
241
271
  const tooltip = document.createElement(`div`);
242
272
  tooltip.className = `custom-tooltip`;
273
+ const placement = safe_options.placement || `bottom`;
274
+ tooltip.setAttribute(`data-placement`, placement);
275
+ // Apply base styles
243
276
  tooltip.style.cssText = `
244
277
  position: absolute; z-index: 9999; opacity: 0;
245
- background: var(--tooltip-bg); color: var(--text-color); border: var(--tooltip-border);
246
- padding: 6px 10px; border-radius: 6px; font-size: 13px; line-height: 1.4;
247
- max-width: 280px; word-wrap: break-word; pointer-events: none;
248
- filter: drop-shadow(0 2px 8px rgba(0,0,0,0.25)); transition: opacity 0.15s ease-out;
278
+ background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
279
+ padding: var(--tooltip-padding, 6px 10px); border-radius: var(--tooltip-radius, 6px); font-size: var(--tooltip-font-size, 13px); line-height: 1.4;
280
+ max-width: var(--tooltip-max-width, 280px); word-wrap: break-word; pointer-events: none;
281
+ filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
249
282
  `;
283
+ // Apply custom styles if provided (these will override base styles due to CSS specificity)
284
+ if (safe_options.style) {
285
+ // Parse and apply custom styles as individual properties for better control
286
+ const custom_styles = safe_options.style.split(`;`).filter((style) => style.trim());
287
+ custom_styles.forEach((style) => {
288
+ const [property, value] = style.split(`:`).map((s) => s.trim());
289
+ if (property && value)
290
+ tooltip.style.setProperty(property, value);
291
+ });
292
+ }
250
293
  tooltip.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
294
+ // Mirror CSS custom properties from the trigger node onto the tooltip element
295
+ const trigger_styles = getComputedStyle(element);
296
+ [
297
+ `--tooltip-bg`,
298
+ `--text-color`,
299
+ `--tooltip-border`,
300
+ `--tooltip-padding`,
301
+ `--tooltip-radius`,
302
+ `--tooltip-font-size`,
303
+ `--tooltip-shadow`,
304
+ `--tooltip-max-width`,
305
+ `--tooltip-opacity`,
306
+ `--tooltip-arrow-size`,
307
+ ].forEach((name) => {
308
+ const value = trigger_styles.getPropertyValue(name).trim();
309
+ if (value)
310
+ tooltip.style.setProperty(name, value);
311
+ });
312
+ // Append early so we can read computed border styles for arrow border
251
313
  document.body.appendChild(tooltip);
314
+ // Arrow elements: optional border triangle behind fill triangle
315
+ const tooltip_styles = getComputedStyle(tooltip);
316
+ const arrow = document.createElement(`div`);
317
+ arrow.className = `custom-tooltip-arrow`;
318
+ arrow.style.cssText =
319
+ `position: absolute; width: 0; height: 0; pointer-events: none;`;
320
+ const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
321
+ .trim();
322
+ const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
323
+ const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
324
+ const border_color = tooltip_styles.borderTopColor;
325
+ const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
326
+ const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
327
+ border_width_num > 0;
328
+ const maybe_append_border_arrow = () => {
329
+ if (!has_border)
330
+ return;
331
+ const border_arrow = document.createElement(`div`);
332
+ border_arrow.className = `custom-tooltip-arrow-border`;
333
+ border_arrow.style.cssText =
334
+ `position: absolute; width: 0; height: 0; pointer-events: none;`;
335
+ const border_size = arrow_px + (border_width_num * 1.4);
336
+ if (placement === `top`) {
337
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
338
+ border_arrow.style.bottom = `-${border_size}px`;
339
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
340
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
341
+ border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
342
+ }
343
+ else if (placement === `left`) {
344
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
345
+ border_arrow.style.right = `-${border_size}px`;
346
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
347
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
348
+ border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
349
+ }
350
+ else if (placement === `right`) {
351
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
352
+ border_arrow.style.left = `-${border_size}px`;
353
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
354
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
355
+ border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
356
+ }
357
+ else { // bottom
358
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
359
+ border_arrow.style.top = `-${border_size}px`;
360
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
361
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
362
+ border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
363
+ }
364
+ tooltip.appendChild(border_arrow);
365
+ };
366
+ // Create the fill arrow on top
367
+ if (placement === `top`) {
368
+ arrow.style.left = `calc(50% - ${arrow_px}px)`;
369
+ arrow.style.bottom = `-${arrow_px}px`;
370
+ arrow.style.borderLeft = `${arrow_px}px solid transparent`;
371
+ arrow.style.borderRight = `${arrow_px}px solid transparent`;
372
+ arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
373
+ }
374
+ else if (placement === `left`) {
375
+ arrow.style.top = `calc(50% - ${arrow_px}px)`;
376
+ arrow.style.right = `-${arrow_px}px`;
377
+ arrow.style.borderTop = `${arrow_px}px solid transparent`;
378
+ arrow.style.borderBottom = `${arrow_px}px solid transparent`;
379
+ arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
380
+ }
381
+ else if (placement === `right`) {
382
+ arrow.style.top = `calc(50% - ${arrow_px}px)`;
383
+ arrow.style.left = `-${arrow_px}px`;
384
+ arrow.style.borderTop = `${arrow_px}px solid transparent`;
385
+ arrow.style.borderBottom = `${arrow_px}px solid transparent`;
386
+ arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
387
+ }
388
+ else { // bottom
389
+ arrow.style.left = `calc(50% - ${arrow_px}px)`;
390
+ arrow.style.top = `-${arrow_px}px`;
391
+ arrow.style.borderLeft = `${arrow_px}px solid transparent`;
392
+ arrow.style.borderRight = `${arrow_px}px solid transparent`;
393
+ arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
394
+ }
395
+ maybe_append_border_arrow();
396
+ tooltip.appendChild(arrow);
252
397
  // Position tooltip
253
398
  const rect = element.getBoundingClientRect();
254
399
  const tooltip_rect = tooltip.getBoundingClientRect();
255
- const placement = safe_options.placement || `bottom`;
256
400
  const margin = 12;
257
401
  let top = 0, left = 0;
258
402
  if (placement === `top`) {
@@ -276,29 +420,29 @@ export const tooltip = (options = {}) => (node) => {
276
420
  top = Math.max(8, Math.min(top, globalThis.innerHeight - tooltip_rect.height - 8));
277
421
  tooltip.style.left = `${left + globalThis.scrollX}px`;
278
422
  tooltip.style.top = `${top + globalThis.scrollY}px`;
279
- tooltip.style.opacity = `1`;
423
+ const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
424
+ tooltip.style.opacity = custom_opacity || `1`;
280
425
  current_tooltip = tooltip;
281
426
  }, safe_options.delay || 100);
282
427
  }
283
428
  function hide_tooltip() {
284
429
  clear_tooltip();
285
- hide_timeout = setTimeout(() => {
430
+ if (current_tooltip) {
431
+ current_tooltip.style.opacity = `0`;
286
432
  if (current_tooltip) {
287
- current_tooltip.style.opacity = `0`;
288
- setTimeout(() => {
289
- if (current_tooltip) {
290
- current_tooltip.remove();
291
- current_tooltip = null;
292
- }
293
- }, 150);
433
+ current_tooltip.remove();
434
+ current_tooltip = null;
294
435
  }
295
- }, 50);
436
+ }
296
437
  }
297
438
  const events = [`mouseenter`, `mouseleave`, `focus`, `blur`];
298
439
  const handlers = [show_tooltip, hide_tooltip, show_tooltip, hide_tooltip];
299
440
  events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
441
+ // Hide tooltip when user scrolls
442
+ globalThis.addEventListener(`scroll`, hide_tooltip, true);
300
443
  return () => {
301
444
  events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
445
+ globalThis.removeEventListener(`scroll`, hide_tooltip, true);
302
446
  const original_title = element.getAttribute(`data-original-title`);
303
447
  if (original_title) {
304
448
  element.setAttribute(`title`, original_title);
package/dist/index.d.ts CHANGED
@@ -8,8 +8,8 @@ export { default as GitHubCorner } from './GitHubCorner.svelte';
8
8
  export { default as Icon } from './Icon.svelte';
9
9
  export { default, default as MultiSelect } from './MultiSelect.svelte';
10
10
  export { default as PrevNext } from './PrevNext.svelte';
11
- export { default as RadioButtons } from './RadioButtons.svelte';
12
11
  export { default as Toggle } from './Toggle.svelte';
13
12
  export * from './types';
13
+ export * from './utils';
14
14
  export { default as Wiggle } from './Wiggle.svelte';
15
15
  export declare function scroll_into_view_if_needed_polyfill(element: Element, centerIfNeeded?: boolean): IntersectionObserver;
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ export { default as GitHubCorner } from './GitHubCorner.svelte';
8
8
  export { default as Icon } from './Icon.svelte';
9
9
  export { default, default as MultiSelect } from './MultiSelect.svelte';
10
10
  export { default as PrevNext } from './PrevNext.svelte';
11
- export { default as RadioButtons } from './RadioButtons.svelte';
12
11
  export { default as Toggle } from './Toggle.svelte';
13
12
  export * from './types';
13
+ export * from './utils';
14
14
  export { default as Wiggle } from './Wiggle.svelte';
15
15
  // Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded).
16
16
  // See https://github.com/janosh/svelte-multiselect/issues/87
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { FocusEventHandler, HTMLInputAttributes, KeyboardEventHandler, MouseEventHandler, TouchEventHandler } from 'svelte/elements';
2
+ import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
3
3
  export type Option = string | number | ObjectOption;
4
4
  export type OptionStyle = string | {
5
5
  option: string;
@@ -16,20 +16,6 @@ export type ObjectOption = {
16
16
  style?: OptionStyle;
17
17
  [key: string]: unknown;
18
18
  };
19
- export interface MultiSelectNativeEvents {
20
- onblur?: FocusEventHandler<HTMLInputElement>;
21
- onclick?: MouseEventHandler<HTMLInputElement>;
22
- onfocus?: FocusEventHandler<HTMLInputElement>;
23
- onkeydown?: KeyboardEventHandler<HTMLInputElement>;
24
- onkeyup?: KeyboardEventHandler<HTMLInputElement>;
25
- onmousedown?: MouseEventHandler<HTMLInputElement>;
26
- onmouseenter?: MouseEventHandler<HTMLInputElement>;
27
- onmouseleave?: MouseEventHandler<HTMLInputElement>;
28
- ontouchcancel?: TouchEventHandler<HTMLInputElement>;
29
- ontouchend?: TouchEventHandler<HTMLInputElement>;
30
- ontouchmove?: TouchEventHandler<HTMLInputElement>;
31
- ontouchstart?: TouchEventHandler<HTMLInputElement>;
32
- }
33
19
  export interface MultiSelectEvents<T extends Option = Option> {
34
20
  onadd?: (data: {
35
21
  option: T;
@@ -55,7 +41,7 @@ export interface MultiSelectEvents<T extends Option = Option> {
55
41
  event: Event;
56
42
  }) => unknown;
57
43
  }
58
- type AfterInputProps = Pick<MultiSelectParameters, `selected` | `disabled` | `invalid` | `id` | `placeholder` | `open` | `required`>;
44
+ type AfterInputProps = Pick<MultiSelectProps, `selected` | `disabled` | `invalid` | `id` | `placeholder` | `open` | `required`>;
59
45
  type UserMsgProps = {
60
46
  searchText: string;
61
47
  msgType: false | `dupe` | `create` | `no-match`;
@@ -87,7 +73,7 @@ export interface PortalParams {
87
73
  target_node?: HTMLElement | null;
88
74
  active?: boolean;
89
75
  }
90
- export interface MultiSelectParameters<T extends Option = Option> {
76
+ export interface MultiSelectProps<T extends Option = Option> extends MultiSelectEvents<T>, MultiSelectSnippets<T>, Omit<HTMLAttributes<HTMLDivElement>, `children` | `onchange` | `onclose`> {
91
77
  activeIndex?: number | null;
92
78
  activeOption?: T | null;
93
79
  createOptionMsg?: string | null;
@@ -101,8 +87,10 @@ export interface MultiSelectParameters<T extends Option = Option> {
101
87
  disabledInputTitle?: string;
102
88
  duplicateOptionMsg?: string;
103
89
  duplicates?: boolean;
90
+ keepSelectedInDropdown?: false | `plain` | `checkboxes`;
104
91
  key?: (opt: T) => unknown;
105
92
  filterFunc?: (opt: T, searchText: string) => boolean;
93
+ fuzzy?: boolean;
106
94
  closeDropdownOnSelect?: boolean | `if-mobile` | `retain-focus`;
107
95
  form_input?: HTMLInputElement | null;
108
96
  highlightMatches?: boolean;
@@ -150,8 +138,5 @@ export interface MultiSelectParameters<T extends Option = Option> {
150
138
  ulOptionsStyle?: string | null;
151
139
  value?: T | T[] | null;
152
140
  portal?: PortalParams;
153
- [key: string]: unknown;
154
- }
155
- export interface MultiSelectProps<T extends Option = Option> extends MultiSelectNativeEvents, MultiSelectEvents<T>, MultiSelectSnippets<T>, MultiSelectParameters<T> {
156
141
  }
157
142
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,9 +1,4 @@
1
- import type { Option, OptionStyle } from './types';
1
+ import type { Option } from './types';
2
2
  export declare const get_label: (opt: Option) => string | number;
3
- export declare function get_style(option: {
4
- style?: OptionStyle;
5
- [key: string]: unknown;
6
- } | string | number, key?: `selected` | `option` | null): string;
7
- export declare function highlight_matching_nodes(element: HTMLElement, // parent element
8
- query?: string, // search query
9
- noMatchingOptionsMsg?: string): void;
3
+ export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
4
+ export declare function fuzzy_match(search_text: string, target_text: string): boolean;
package/dist/utils.js CHANGED
@@ -14,14 +14,15 @@ export const get_label = (opt) => {
14
14
  // object to be used in the style attribute of the option.
15
15
  // If the style is a string, it will be returned as is
16
16
  export function get_style(option, key = null) {
17
+ if (key === undefined)
18
+ key = null;
17
19
  let css_str = ``;
18
- if (![`selected`, `option`, null].includes(key)) {
20
+ const valid_key = key === null || key === `selected` || key === `option`;
21
+ if (!valid_key)
19
22
  console.error(`MultiSelect: Invalid key=${key} for get_style`);
20
- }
21
23
  if (typeof option === `object` && option.style) {
22
- if (typeof option.style === `string`) {
24
+ if (typeof option.style === `string`)
23
25
  css_str = option.style;
24
- }
25
26
  if (typeof option.style === `object`) {
26
27
  if (key && key in option.style)
27
28
  return option.style[key] ?? ``;
@@ -35,49 +36,24 @@ export function get_style(option, key = null) {
35
36
  css_str += `;`;
36
37
  return css_str;
37
38
  }
38
- // Highlights text nodes that matching the string query
39
- export function highlight_matching_nodes(element, // parent element
40
- query, // search query
41
- noMatchingOptionsMsg) {
42
- if (typeof CSS === `undefined` || !CSS.highlights || !query)
43
- return; // abort if CSS highlight API not supported
44
- // clear previous ranges from HighlightRegistry
45
- CSS.highlights.clear();
46
- const tree_walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
47
- acceptNode: (node) => {
48
- // don't highlight text in the "no matching options" message
49
- if (node?.textContent === noMatchingOptionsMsg) {
50
- return NodeFilter.FILTER_REJECT;
51
- }
52
- return NodeFilter.FILTER_ACCEPT;
53
- },
54
- });
55
- const text_nodes = [];
56
- let current_node = tree_walker.nextNode();
57
- while (current_node) {
58
- text_nodes.push(current_node);
59
- current_node = tree_walker.nextNode();
39
+ // Fuzzy string matching function
40
+ // Returns true if the search string can be found as a subsequence in the target string
41
+ // e.g., "tageoo" matches "tasks/geo-opt" because t-a-g-e-o-o appears in order
42
+ export function fuzzy_match(search_text, target_text) {
43
+ // Handle null/undefined inputs first
44
+ if (search_text === null || search_text === undefined || target_text === null ||
45
+ target_text === undefined)
46
+ return false;
47
+ if (!search_text)
48
+ return true;
49
+ if (!target_text)
50
+ return false;
51
+ const [search, target] = [search_text.toLowerCase(), target_text.toLowerCase()];
52
+ let [search_idx, target_idx] = [0, 0];
53
+ while (search_idx < search.length && target_idx < target.length) {
54
+ if (search[search_idx] === target[target_idx])
55
+ search_idx++;
56
+ target_idx++;
60
57
  }
61
- // iterate over all text nodes and find matches
62
- const ranges = text_nodes.map((el) => {
63
- const text = el.textContent?.toLowerCase();
64
- const indices = [];
65
- let start_pos = 0;
66
- while (text && start_pos < text.length) {
67
- const index = text.indexOf(query, start_pos);
68
- if (index === -1)
69
- break;
70
- indices.push(index);
71
- start_pos = index + query.length;
72
- }
73
- // create range object for each str found in the text node
74
- return indices.map((index) => {
75
- const range = new Range();
76
- range.setStart(el, index);
77
- range.setEnd(el, index + query.length);
78
- return range;
79
- });
80
- });
81
- // create Highlight object from ranges and add to registry
82
- CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
58
+ return search_idx === search.length;
83
59
  }