UNPKG

@carbon/ibm-products

Version:
385 lines (383 loc) 11.3 kB
/** * Copyright IBM Corp. 2020, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ //#region ../ibm-products-utilities/src/utils/add-select/add-select-data.ts /** * AddSelectData - A lightweight, framework-agnostic utility for managing hierarchical data * * This utility provides standard APIs for common operations such as setting, selecting, * traversing, searching, and sorting hierarchical data structures. It encapsulates data * logic separate from UI, allowing both React and Web Components to reuse the same * data management functions. */ var AddSelectData = class { constructor() { this.items = []; this.itemMap = /* @__PURE__ */ new Map(); this.parentMap = /* @__PURE__ */ new Map(); this.selectedIds = /* @__PURE__ */ new Set(); this.depthCache = /* @__PURE__ */ new Map(); this.selectedItemsCache = null; } /** * Initialize or replace the hierarchical data * @param items - Array of hierarchical items */ setItems(items) { this.items = items; this._invalidateCaches(); this._buildMaps(items); } /** * Get the full list of items * @returns Array of all items */ getItems() { return this.items; } /** * Retrieve a single item by its id * @param id - The item id * @returns The item or undefined if not found */ getItem(id) { return this.itemMap.get(id); } /** * Update a given item with new properties * @param id - The item id * @param newProperties - Properties to update * @returns true if item was found and updated, false otherwise */ setItem(id, newProperties) { const item = this.itemMap.get(id); if (!item) return false; Object.assign(item, newProperties); if ("selected" in newProperties || "status" in newProperties) this._invalidateSelectionCache(); return true; } /** * Returns an array of items marked as selected (memoized) * @returns Array of selected items */ getSelectedItems() { if (this.selectedItemsCache !== null) return this.selectedItemsCache; const selected = []; this.selectedIds.forEach((id) => { const item = this.itemMap.get(id); if (item) selected.push(item); }); this.selectedItemsCache = selected; return selected; } /** * Mark one or more items (by id) as selected * @param ids - Single id or array of ids to select * @param exclusive - If true, deselect all other items (default: false) */ setSelectedItems(ids, exclusive = false) { const idArray = Array.isArray(ids) ? ids : [ids]; if (exclusive) { this.selectedIds.forEach((id) => { const item = this.itemMap.get(id); if (item) { item.selected = false; item.status = "unchecked"; } }); this.selectedIds.clear(); } idArray.forEach((id) => { const item = this.itemMap.get(id); if (item) { item.selected = true; item.status = "checked"; this.selectedIds.add(id); } }); this._invalidateSelectionCache(); } /** * Get direct children of a node * @param id - The parent item id * @returns Array of child items or empty array if no children */ getItemChildren(id) { const item = this.itemMap.get(id); if (!item) return []; return item.children?.entries ?? []; } /** * Get the parent of a node * @param id - The child item id * @returns The parent item or undefined if no parent (root level) */ getItemParent(id) { const parentId = this.parentMap.get(id); return parentId ? this.itemMap.get(parentId) : void 0; } /** * Get all ancestors (parents up to the root) of a node * @param id - The item id * @returns Array of ancestor items from immediate parent to root */ getItemParents(id) { const parents = []; let currentId = id; while (currentId) { const parentId = this.parentMap.get(currentId); if (!parentId) break; const parent = this.itemMap.get(parentId); if (parent) parents.push(parent); currentId = parentId; } return parents; } /** * Retrieve the selected state status of an item * @param id - The item id * @returns The status or undefined if item not found */ getItemStatus(id) { return this.itemMap.get(id)?.status; } /** * Set or update the status of an item * @param id - The item id * @param status - The new status * @returns true if item was found and updated, false otherwise */ setItemStatus(id, status) { const item = this.itemMap.get(id); if (!item) return false; const wasSelected = item.selected; item.status = status; item.selected = status === "checked"; if (item.selected) this.selectedIds.add(id); else this.selectedIds.delete(id); if (wasSelected !== item.selected) this._invalidateSelectionCache(); return true; } /** * Check whether an item is selected (O(1) lookup) * @param id - The item id * @returns true if selected, false otherwise */ isSelected(id) { return this.selectedIds.has(id); } /** * Search items based on a query and return matching items * @param query - The search query string * @param options - Search options * @returns Array of matching items */ search(query, options = {}) { if (!query) return []; const { caseSensitive = false, searchFields = ["title", "value"], maxResults } = options; const searchTerm = caseSensitive ? query : query.toLowerCase(); const results = []; const shouldContinue = () => { return !maxResults || results.length < maxResults; }; this._traverseItems(this.items, (item) => { if (!shouldContinue()) return; for (const field of searchFields) { const fieldValue = item[field]; if (fieldValue) { if ((caseSensitive ? String(fieldValue) : String(fieldValue).toLowerCase()).includes(searchTerm)) { results.push(item); break; } } } }); return results; } /** * Sort items based on a comparator function * @param compareFn - Comparison function for sorting * @param recursive - If true, sort children recursively (default: false) */ sort(compareFn, recursive = false) { if (recursive) this._sortRecursive(this.items, compareFn); this.items.sort(compareFn); this._invalidateCaches(); this._buildMaps(this.items); } /** * Clear all selections (optimized with Set) */ clearSelections() { this.selectedIds.forEach((id) => { const item = this.itemMap.get(id); if (item) { item.selected = false; item.status = "unchecked"; } }); this.selectedIds.clear(); this._invalidateSelectionCache(); } /** * Get the depth/level of an item in the hierarchy (cached) * @param id - The item id * @returns The depth (0 for root level items) or -1 if not found */ getItemDepth(id) { const cachedDepth = this.depthCache.get(id); if (cachedDepth !== void 0) return cachedDepth; if (!this.itemMap.has(id)) return -1; let depth = 0; let currentId = id; while (currentId) { const parentId = this.parentMap.get(currentId); if (!parentId) break; depth++; currentId = parentId; } this.depthCache.set(id, depth); return depth; } /** * Check if an item has children * @param id - The item id * @returns true if item has children, false otherwise */ hasChildren(id) { return !!this.itemMap.get(id)?.children?.entries?.length; } /** * Get all descendant items of a node * @param id - The parent item id * @returns Array of all descendant items */ getItemDescendants(id) { const item = this.itemMap.get(id); if (!item?.children?.entries) return []; const descendants = []; this._traverseItems(item.children.entries, (child) => { descendants.push(child); }); return descendants; } /** * Check if an item has any selected descendants (optimized) * @param id - The item id * @param selectedIds - Set of selected item IDs (defaults to internal selectedIds) * @returns true if any descendant is selected, false otherwise */ hasSelectedDescendants(id, selectedIds = this.selectedIds) { const children = this.getItemChildren(id); if (!children.length) return false; for (const child of children) if (selectedIds.has(child.id) || this.hasSelectedDescendants(child.id, selectedIds)) return true; return false; } /** * Check if all descendants of an item are selected (optimized) * @param id - The item id * @param selectedIds - Set of selected item IDs (defaults to internal selectedIds) * @returns true if all descendants are selected, false otherwise */ allDescendantsSelected(id, selectedIds = this.selectedIds) { const item = this.itemMap.get(id); if (!item) return false; const children = this.getItemChildren(id); if (!children.length) return selectedIds.has(item.id); for (const child of children) if (!selectedIds.has(child.id) || !this.allDescendantsSelected(child.id, selectedIds)) return false; return true; } /** * Get all descendant IDs from an item (including the item itself) * @param id - The item id * @returns Array of all descendant IDs including the item itself */ getAllDescendantIds(id) { if (!this.itemMap.get(id)) return []; const ids = [id]; const children = this.getItemChildren(id); for (const child of children) ids.push(...this.getAllDescendantIds(child.id)); return ids; } /** * Get only top-level selected items (items without selected ancestors) * @param selectedIds - Set of selected item IDs (defaults to internal selectedIds) * @returns Array of top-level selected items */ getTopLevelSelectedItems(selectedIds = this.selectedIds) { const topLevelItems = []; const processedIds = /* @__PURE__ */ new Set(); const hasSelectedAncestor = (itemId) => { return this.getItemParents(itemId).some((parent) => selectedIds.has(parent.id)); }; selectedIds.forEach((id) => { if (!processedIds.has(id) && !hasSelectedAncestor(id)) { const item = this.itemMap.get(id); if (item) { topLevelItems.push(item); this.getAllDescendantIds(id).forEach((descId) => processedIds.add(descId)); } } }); return topLevelItems; } /** * Invalidate all caches * @private */ _invalidateCaches() { this.selectedItemsCache = null; this.depthCache.clear(); } /** * Invalidate selection cache only * @private */ _invalidateSelectionCache() { this.selectedItemsCache = null; } /** * Build internal maps for efficient lookups (optimized with depth caching) * @private */ _buildMaps(items, parentId, depth = 0) { if (!parentId) { this.itemMap.clear(); this.parentMap.clear(); this.selectedIds.clear(); this.depthCache.clear(); } items.forEach((item) => { this.itemMap.set(item.id, item); this.depthCache.set(item.id, depth); if (parentId) this.parentMap.set(item.id, parentId); if (item.selected) this.selectedIds.add(item.id); if (item.children?.entries) this._buildMaps(item.children.entries, item.id, depth + 1); }); } /** * Traverse all items and apply a callback function * @private */ _traverseItems(items, callback) { for (const item of items) { callback(item); if (item.children?.entries) this._traverseItems(item.children.entries, callback); } } /** * Sort items recursively * @private */ _sortRecursive(items, compareFn) { for (const item of items) if (item.children?.entries) { item.children.entries.sort(compareFn); this._sortRecursive(item.children.entries, compareFn); } } }; //#endregion exports.AddSelectData = AddSelectData;