@zag-js/collection
Version:
Utilities to manage a collection of items.
503 lines (501 loc) • 16.1 kB
JavaScript
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
};