UNPKG

svelte-multiselect

Version:
141 lines (140 loc) 6.33 kB
let uuid_counter = 0; // Generates a UUID for component IDs. Uses native crypto.randomUUID when available. // Fallback uses timestamp+counter - sufficient for DOM IDs (uniqueness, not security). // Cryptographic randomness is unnecessary here since these IDs are only used for // associating labels with inputs and ensuring unique DOM element identifiers. export function get_uuid() { if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); const hex = (Date.now().toString(16) + (uuid_counter++).toString(16)).padStart(32, `0`); return [ hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32), ].join(`-`); } // Type guard for checking if a value is a non-null object export const is_object = (val) => typeof val === `object` && val !== null; // Type guard for checking if an option has a group key export const has_group = (opt) => is_object(opt) && typeof opt.group === `string`; // Get the label key from an option object or the option itself // if it's a string or number export const get_label = (opt) => { if (is_object(opt)) { if (opt.label === undefined) { const opt_str = JSON.stringify(opt); console.error(`MultiSelect: option is an object but has no label key`, opt_str); } return opt.label; } return `${opt}`; }; // Generate a unique key for an option, preserving value identity // For object options: uses value if defined, otherwise label (no case normalization) // For primitives: the primitive itself export const get_option_key = (opt) => is_object(opt) ? (opt.value ?? get_label(opt)) : opt; // This function is used extract CSS strings from a {selected, option} style // object to be used in the style attribute of the option. // If the style is a string, it will be returned as is export function get_style(option, key = null) { let css_str = ``; if (key !== null && key !== `selected` && key !== `option`) { console.error(`MultiSelect: Invalid key=${String(key)} for get_style`); return css_str; } if (typeof option === `object` && option.style) { if (typeof option.style === `string`) css_str = option.style; if (typeof option.style === `object` && key) { if (key in option.style) css_str = option.style[key] ?? ``; // partial style objects (e.g. only `selected`) are fine; flag any keys // other than the known ones, even when a valid key is also present const has_unknown_key = Object.keys(option.style).some((style_key) => style_key !== `option` && style_key !== `selected`); if (has_unknown_key) { console.error(`MultiSelect: invalid style object for option`, option); } } } // ensure css_str ends with a semicolon const trimmed = css_str.trim(); if (trimmed && !trimmed.endsWith(`;`)) css_str += `;`; return css_str; } export function split_shortcut(shortcut) { const parts = shortcut .toLowerCase() .split(`+`) .map((part) => part.trim()); if (parts.at(-1) === `` && parts.at(-2) === ``) parts.splice(-2, 2, `+`); return parts; } // Parse shortcut string into modifier+key parts export function parse_shortcut(shortcut) { const parts = split_shortcut(shortcut); const key = parts.pop() ?? ``; const ctrl = parts.includes(`ctrl`); const shift = parts.includes(`shift`); const alt = parts.includes(`alt`); const meta = parts.includes(`meta`) || parts.includes(`cmd`); return { key, ctrl, shift, alt, meta }; } export function matches_shortcut(event, shortcut) { if (!shortcut) return false; const { key, ctrl, shift, alt, meta } = parse_shortcut(shortcut); // Require non-empty key to prevent "ctrl+" from matching any key with ctrl pressed if (!key) return false; return (event.key.toLowerCase() === key && event.ctrlKey === ctrl && (event.shiftKey === shift || (key === `+` && !shift)) && event.altKey === alt && event.metaKey === meta); } // Compare arrays/values for equality to avoid unnecessary updates. // Prevents infinite loops when value/selected are bound to reactive wrappers // that clone arrays on assignment (e.g. Superforms, Svelte stores). See issue #309. // Treats null/undefined/[] as equivalent empty states to prevent extra updates on init (#369). export function values_equal(val1, val2) { if (val1 === val2) return true; const is_empty = (val) => val === null || val === undefined || (Array.isArray(val) && val.length === 0); if (is_empty(val1) && is_empty(val2)) return true; if (Array.isArray(val1) && Array.isArray(val2)) { return val1.length === val2.length && val1.every((item, idx) => item === val2[idx]); } return false; } // Case-insensitive subsequence match: returns the indices in target_text where // the characters of search_text appear in order, or null if not all characters // can be matched. An empty search matches with no indices. export function fuzzy_match_indices(search_text, target_text) { const [search, target] = [search_text.toLowerCase(), target_text.toLowerCase()]; const indices = []; let [search_idx, target_idx] = [0, 0]; while (search_idx < search.length && target_idx < target.length) { if (search[search_idx] === target[target_idx]) { indices.push(target_idx); search_idx++; } target_idx++; } return search_idx === search.length ? indices : null; } // Fuzzy string matching function // Returns true if the search string can be found as a subsequence in the target string // e.g., "tageoo" matches "tasks/geo-opt" because t-a-g-e-o-o appears in order export function fuzzy_match(search_text, target_text) { // guard null/undefined inputs (fuzzy_match_indices would throw on .toLowerCase()) if (search_text == null || target_text == null) return false; // empty search matches everything, empty target matches nothing - both already // handled by fuzzy_match_indices (empty search -> [], else vs empty target -> null) return fuzzy_match_indices(search_text, target_text) !== null; }