@carbon/ibm-products
Version:
Carbon for IBM Products
385 lines (383 loc) • 11.3 kB
JavaScript
/**
* 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;