UNPKG

@zag-js/collection

Version:

Utilities to manage a collection of items.

503 lines (501 loc) 16.1 kB
import { __publicField } from "./chunk-QZ7TP4HQ.mjs"; // src/list-collection.ts import { hasProp, isEqual, isObject } from "@zag-js/utils"; var fallback = { itemToValue(item) { if (typeof item === "string") return item; if (isObject(item) && hasProp(item, "value")) return item.value; return ""; }, itemToString(item) { if (typeof item === "string") return item; if (isObject(item) && hasProp(item, "label")) return item.label; return fallback.itemToValue(item); }, isItemDisabled(item) { if (isObject(item) && hasProp(item, "disabled")) return !!item.disabled; return false; } }; var ListCollection = class _ListCollection { constructor(options) { this.options = options; /** * The items in the collection */ __publicField(this, "items"); __publicField(this, "indexMap", null); /** * Copy the collection */ __publicField(this, "copy", (items) => { return new _ListCollection({ ...this.options, items: items ?? [...this.items] }); }); /** * Check if the collection is equal to another collection */ __publicField(this, "isEqual", (other) => { return isEqual(this.items, other.items); }); /** * Function to update the collection items */ __publicField(this, "setItems", (items) => { return this.copy(items); }); /** * Returns all the values in the collection */ __publicField(this, "getValues", (items = this.items) => { const values = []; for (const item of items) { const value = this.getItemValue(item); if (value != null) values.push(value); } return values; }); /** * Get the item based on its value */ __publicField(this, "find", (value) => { if (value == null) return null; const index = this.indexOf(value); return index !== -1 ? this.at(index) : null; }); /** * Get the items based on its values */ __publicField(this, "findMany", (values) => { const result = []; for (const value of values) { const item = this.find(value); if (item != null) result.push(item); } return result; }); /** * Get the item based on its index */ __publicField(this, "at", (index) => { if (!this.options.groupBy && !this.options.groupSort) { return this.items[index] ?? null; } let idx = 0; const groups = this.group(); for (const [, items] of groups) { for (const item of items) { if (idx === index) return item; idx++; } } return null; }); __publicField(this, "sortFn", (valueA, valueB) => { const indexA = this.indexOf(valueA); const indexB = this.indexOf(valueB); return (indexA ?? 0) - (indexB ?? 0); }); /** * Sort the values based on their index */ __publicField(this, "sort", (values) => { return [...values].sort(this.sortFn.bind(this)); }); /** * Convert an item to a value */ __publicField(this, "getItemValue", (item) => { if (item == null) return null; return this.options.itemToValue?.(item) ?? fallback.itemToValue(item); }); /** * Whether an item is disabled */ __publicField(this, "getItemDisabled", (item) => { if (item == null) return false; return this.options.isItemDisabled?.(item) ?? fallback.isItemDisabled(item); }); /** * Convert an item to a string */ __publicField(this, "stringifyItem", (item) => { if (item == null) return null; return this.options.itemToString?.(item) ?? fallback.itemToString(item); }); /** * Convert a value to a string */ __publicField(this, "stringify", (value) => { if (value == null) return null; return this.stringifyItem(this.find(value)); }); /** * Convert an array of items to a string */ __publicField(this, "stringifyItems", (items, separator = ", ") => { const strs = []; for (const item of items) { const str = this.stringifyItem(item); if (str != null) strs.push(str); } return strs.join(separator); }); /** * Convert an array of items to a string */ __publicField(this, "stringifyMany", (value, separator) => { return this.stringifyItems(this.findMany(value), separator); }); /** * Whether the collection has a value */ __publicField(this, "has", (value) => { return this.indexOf(value) !== -1; }); /** * Whether the collection has an item */ __publicField(this, "hasItem", (item) => { if (item == null) return false; return this.has(this.getItemValue(item)); }); /** * Group items by the groupBy function provided in options * Returns an array of [groupKey, items] tuples */ __publicField(this, "group", () => { const { groupBy, groupSort } = this.options; if (!groupBy) return [["", [...this.items]]]; const groups = /* @__PURE__ */ new Map(); this.items.forEach((item, index) => { const groupKey = groupBy(item, index); if (!groups.has(groupKey)) { groups.set(groupKey, []); } groups.get(groupKey).push(item); }); let entries = Array.from(groups.entries()); if (groupSort) { entries.sort(([a], [b]) => { if (typeof groupSort === "function") return groupSort(a, b); if (Array.isArray(groupSort)) { const indexA = groupSort.indexOf(a); const indexB = groupSort.indexOf(b); if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; } if (groupSort === "asc") return a.localeCompare(b); if (groupSort === "desc") return b.localeCompare(a); return 0; }); } return entries; }); /** * Returns the next value in the collection */ __publicField(this, "getNextValue", (value, step = 1, clamp = false) => { let index = this.indexOf(value); if (index === -1) return null; index = clamp ? Math.min(index + step, this.size - 1) : index + step; while (index <= this.size && this.getItemDisabled(this.at(index))) index++; return this.getItemValue(this.at(index)); }); /** * Returns the previous value in the collection */ __publicField(this, "getPreviousValue", (value, step = 1, clamp = false) => { let index = this.indexOf(value); if (index === -1) return null; index = clamp ? Math.max(index - step, 0) : index - step; while (index >= 0 && this.getItemDisabled(this.at(index))) index--; return this.getItemValue(this.at(index)); }); /** * Get the index of an item based on its key */ __publicField(this, "indexOf", (value) => { if (value == null) return -1; if (!this.options.groupBy && !this.options.groupSort) { return this.items.findIndex((item) => this.getItemValue(item) === value); } if (!this.indexMap) { this.indexMap = /* @__PURE__ */ new Map(); let idx = 0; const groups = this.group(); for (const [, items] of groups) { for (const item of items) { const itemValue = this.getItemValue(item); if (itemValue != null) { this.indexMap.set(itemValue, idx); } idx++; } } } return this.indexMap.get(value) ?? -1; }); __publicField(this, "getByText", (text, current) => { const currentIndex = current != null ? this.indexOf(current) : -1; const isSingleKey = text.length === 1; for (let i = 0; i < this.items.length; i++) { const item = this.items[(currentIndex + i + 1) % this.items.length]; if (isSingleKey && this.getItemValue(item) === current) continue; if (this.getItemDisabled(item)) continue; if (match(this.stringifyItem(item), text)) return item; } return void 0; }); /** * Search for a value based on a query */ __publicField(this, "search", (queryString, options) => { const { state, currentValue, timeout = 350 } = options; const search = state.keysSoFar + queryString; const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]); const query = isRepeated ? search[0] : search; const item = this.getByText(query, currentValue); const value = this.getItemValue(item); function cleanup() { clearTimeout(state.timer); state.timer = -1; } function update(value2) { state.keysSoFar = value2; cleanup(); if (value2 !== "") { state.timer = +setTimeout(() => { update(""); cleanup(); }, timeout); } } update(search); return value; }); /** * Update an item in the collection */ __publicField(this, "update", (value, item) => { let index = this.indexOf(value); if (index === -1) return this; return this.copy([...this.items.slice(0, index), item, ...this.items.slice(index + 1)]); }); /** * Update an item in the collection if it exists, otherwise append it */ __publicField(this, "upsert", (value, item, mode = "append") => { let index = this.indexOf(value); if (index === -1) { const fn = mode === "append" ? this.append : this.prepend; return fn(item); } return this.copy([...this.items.slice(0, index), item, ...this.items.slice(index + 1)]); }); /** * Insert items at a specific index */ __publicField(this, "insert", (index, ...items) => { return this.copy(insert(this.items, index, ...items)); }); /** * Insert items before a specific value */ __publicField(this, "insertBefore", (value, ...items) => { let toIndex = this.indexOf(value); if (toIndex === -1) { if (this.items.length === 0) toIndex = 0; else return this; } return this.copy(insert(this.items, toIndex, ...items)); }); /** * Insert items after a specific value */ __publicField(this, "insertAfter", (value, ...items) => { let toIndex = this.indexOf(value); if (toIndex === -1) { if (this.items.length === 0) toIndex = 0; else return this; } return this.copy(insert(this.items, toIndex + 1, ...items)); }); /** * Prepend items to the collection */ __publicField(this, "prepend", (...items) => { return this.copy(insert(this.items, 0, ...items)); }); /** * Append items to the collection */ __publicField(this, "append", (...items) => { return this.copy(insert(this.items, this.items.length, ...items)); }); /** * Filter the collection */ __publicField(this, "filter", (fn) => { const filteredItems = this.items.filter((item, index) => fn(this.stringifyItem(item), index, item)); return this.copy(filteredItems); }); /** * Remove items from the collection */ __publicField(this, "remove", (...itemsOrValues) => { const values = itemsOrValues.map( (itemOrValue) => typeof itemOrValue === "string" ? itemOrValue : this.getItemValue(itemOrValue) ); return this.copy( this.items.filter((item) => { const value = this.getItemValue(item); if (value == null) return false; return !values.includes(value); }) ); }); /** * Move an item to a specific index */ __publicField(this, "move", (value, toIndex) => { const fromIndex = this.indexOf(value); if (fromIndex === -1) return this; return this.copy(move(this.items, [fromIndex], toIndex)); }); /** * Move items before a specific value */ __publicField(this, "moveBefore", (value, ...values) => { let toIndex = this.items.findIndex((item) => this.getItemValue(item) === value); if (toIndex === -1) return this; let indices = values.map((value2) => this.items.findIndex((item) => this.getItemValue(item) === value2)).sort((a, b) => a - b); return this.copy(move(this.items, indices, toIndex)); }); /** * Move items after a specific value */ __publicField(this, "moveAfter", (value, ...values) => { let toIndex = this.items.findIndex((item) => this.getItemValue(item) === value); if (toIndex === -1) return this; let indices = values.map((value2) => this.items.findIndex((item) => this.getItemValue(item) === value2)).sort((a, b) => a - b); return this.copy(move(this.items, indices, toIndex + 1)); }); /** * Reorder items */ __publicField(this, "reorder", (fromIndex, toIndex) => { return this.copy(move(this.items, [fromIndex], toIndex)); }); /** * Compare two values */ __publicField(this, "compareValue", (a, b) => { const indexA = this.indexOf(a); const indexB = this.indexOf(b); if (indexA < indexB) return -1; if (indexA > indexB) return 1; return 0; }); /** * Get the range of values between two values */ __publicField(this, "range", (from, to) => { let keys = []; let key = from; while (key != null) { let item = this.find(key); if (item) keys.push(key); if (key === to) return keys; key = this.getNextValue(key); } return []; }); /** * Get the range of values between two values */ __publicField(this, "getValueRange", (from, to) => { if (from && to) { if (this.compareValue(from, to) <= 0) { return this.range(from, to); } return this.range(to, from); } return []; }); /** * Convert the collection to a string */ __publicField(this, "toString", () => { let result = ""; for (const item of this.items) { const value = this.getItemValue(item); const label = this.stringifyItem(item); const disabled = this.getItemDisabled(item); const itemString = [value, label, disabled].filter(Boolean).join(":"); result += itemString + ","; } return result; }); /** * Convert the collection to a JSON object */ __publicField(this, "toJSON", () => { return { size: this.size, first: this.firstValue, last: this.lastValue }; }); this.items = [...options.items]; } /** * Returns the number of items in the collection */ get size() { return this.items.length; } /** * Returns the first value in the collection */ get firstValue() { let index = 0; while (this.getItemDisabled(this.at(index))) index++; return this.getItemValue(this.at(index)); } /** * Returns the last value in the collection */ get lastValue() { let index = this.size - 1; while (this.getItemDisabled(this.at(index))) index--; return this.getItemValue(this.at(index)); } *[Symbol.iterator]() { yield* this.items; } }; var match = (label, query) => { return !!label?.toLowerCase().startsWith(query.toLowerCase()); }; function isListCollection(v) { return v instanceof ListCollection; } function insert(items, index, ...values) { return [...items.slice(0, index), ...values, ...items.slice(index)]; } function move(items, indices, toIndex) { indices = [...indices].sort((a, b) => a - b); const itemsToMove = indices.map((i) => items[i]); for (let i = indices.length - 1; i >= 0; i--) { items = [...items.slice(0, indices[i]), ...items.slice(indices[i] + 1)]; } toIndex = Math.max(0, toIndex - indices.filter((i) => i < toIndex).length); return [...items.slice(0, toIndex), ...itemsToMove, ...items.slice(toIndex)]; } export { ListCollection, isListCollection };