UNPKG

@dkile/react-multiselect

Version:

React hook wrapper for multiselect-core

663 lines (652 loc) 18.8 kB
// src/use-multiselect.ts import { useRef, useState, useEffect } from "react"; // ../multiselect-core/src/scheduler.ts var rafScheduler = { schedule: (fn) => { requestAnimationFrame(fn); } }; // ../multiselect-core/src/utils.ts var isID = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"; var accessObjectValue = (accessor, item) => { if (typeof accessor === "string") { const pathKeys = accessor.split("."); const value2 = pathKeys.reduce((acc, key) => { if (acc && typeof acc === "object" && key in acc) { return acc[key]; } return void 0; }, item); return isID(value2) ? value2 : void 0; } const value = accessor(item); return value; }; // ../multiselect-core/src/item.ts function createCoreSelectItem(data, idAccessor, metaFn, selectedIds, applyChange) { const rawId = accessObjectValue(idAccessor, data); if (rawId === void 0) { throw new Error( `id_accessor returned undefined for item: ${JSON.stringify(data)}` ); } const id = rawId; const wrapper = { id, data, get isSelected() { return selectedIds.has(id); }, meta: metaFn ? metaFn(data) : void 0, select: () => { if (!selectedIds.has(id)) { applyChange({ added: [id] }); } }, unselect: () => { if (selectedIds.has(id)) { applyChange({ removed: [id] }); } }, toggle: () => { if (selectedIds.has(id)) { wrapper.unselect(); } else { wrapper.select(); } } }; return wrapper; } // ../multiselect-core/src/group.ts var GroupManager = class { constructor(items, groupBy) { this.items = items; this.groupBy = groupBy; } getGroupTree() { return this.groupItemsRecursive(this.items, 0, []); } selectGroup(node) { for (const leaf of this.collectLeaves(node)) { leaf.select(); } } unselectGroup(node) { for (const leaf of this.collectLeaves(node)) { leaf.unselect(); } } toggleGroup(node) { const leaves = this.collectLeaves(node); const allSelected = leaves.every((i) => i.isSelected); if (allSelected) { this.unselectGroup(node); } else { this.selectGroup(node); } } groupItemsRecursive(items, level, path) { if (level >= this.groupBy.length) return []; const accessor = this.groupBy[level]; const map = /* @__PURE__ */ new Map(); for (const item of items) { const rawValue = accessObjectValue(accessor, item.data); if (rawValue === void 0) { if (typeof accessor === "string") { throw new Error( `Grouping by path '${accessor}' returned undefined for item` ); } continue; } const arr = map.get(rawValue) ?? []; arr.push(item); map.set(rawValue, arr); } const nodes = []; for (const [key, groupItems] of map.entries()) { const currentPath = [...path, key]; const children = this.groupItemsRecursive( groupItems, level + 1, currentPath ); const node = new GroupNodeImpl( this, key, level, currentPath, children.length > 0 ? [] : groupItems, children.length > 0 ? children : void 0 ); nodes.push(node); } return nodes; } collectLeaves(node) { if (node.children) { let leaves = []; for (const child of node.children) { leaves = leaves.concat(this.collectLeaves(child)); } return leaves; } return node.items; } }; var GroupNodeImpl = class { constructor(manager, key, level, path, items, children) { this.manager = manager; this.key = key; this.level = level; this.path = path; this.items = items; this.children = children; } select() { this.manager.selectGroup(this); } unselect() { this.manager.unselectGroup(this); } toggle() { this.manager.toggleGroup(this); } hasChildren() { return Array.isArray(this.children) && this.children.length > 0; } isAllSelected() { if (this.hasChildren()) { return this.children.every((child) => child.isAllSelected()); } return this.items.every((item) => item.isSelected); } }; // ../multiselect-core/src/filter.ts var FilterManager = class { constructor(items, initialFilter) { this.items = items; this.filter = initialFilter; } setFilter(filter) { this.filter = filter; } getFilteredItems() { if (!this.filter) return this.items; const { query, predicate } = this.filter; return this.items.filter( (item) => predicate(item.data, query, item.meta) ); } isAllFilteredSelected() { const filtered = this.getFilteredItems(); return filtered.length > 0 && filtered.every((item) => item.isSelected); } selectAllFiltered() { for (const item of this.getFilteredItems()) { item.select(); } } unselectAllFiltered() { for (const item of this.getFilteredItems()) { item.unselect(); } } toggleAllFiltered() { if (this.isAllFilteredSelected()) { this.unselectAllFiltered(); } else { this.selectAllFiltered(); } } getFilter() { return this.filter; } }; // ../multiselect-core/src/group-filter.ts function collectRawLeaves(node) { if (node.children) { let leaves = []; for (const child of node.children) { leaves = leaves.concat(collectRawLeaves(child)); } return leaves; } return node.items; } function unifyGroupNodes(nodes, filteredSet, filteredOnly) { const result = []; for (const node of nodes) { const allLeaves = collectRawLeaves(node); const filteredLeaves = allLeaves.filter((i) => filteredSet.has(i.id)); if (filteredOnly && filteredLeaves.length === 0) continue; const childrenUnified = unifyGroupNodes( node.children ?? [], filteredSet, filteredOnly ); const unified = { key: node.key, level: node.level, path: node.path, getMeta() { return void 0; }, getItems({ filteredOnly: fo } = {}) { return fo ? filteredLeaves : allLeaves; }, getSubGroups() { return childrenUnified; }, getSubGroup(key) { return childrenUnified.find((child) => child.key === key); }, hasSubGroups() { return childrenUnified.length > 0; }, select({ filteredOnly: fo } = {}) { for (const item of fo ? filteredLeaves : allLeaves) item.select(); }, unselect({ filteredOnly: fo } = {}) { for (const item of fo ? filteredLeaves : allLeaves) item.unselect(); }, toggle({ filteredOnly: fo } = {}) { const target = fo ? filteredLeaves : allLeaves; const allSel = target.length > 0 && target.every((i) => i.isSelected); if (allSel) { for (const item of target) { item.unselect(); } } else { for (const item of target) { item.select(); } } }, isAllSelected({ filteredOnly: fo } = {}) { const target = fo ? filteredLeaves : allLeaves; return target.length > 0 && target.every((i) => i.isSelected); }, totalCount: allLeaves.length, filteredCount: filteredLeaves.length }; result.push(unified); } return result; } // ../multiselect-core/src/group-utils.ts function collectRawLeaves2(node) { if (node.children) { let leaves = []; for (const child of node.children) { leaves = leaves.concat(collectRawLeaves2(child)); } return leaves; } return node.items; } function pruneTreeByFilter(nodes, filteredSet, filteredOnly) { if (!filteredOnly) return nodes; const result = []; for (const node of nodes) { const leaves = collectRawLeaves2(node); const hasFiltered = leaves.some((item) => filteredSet.has(item.id)); const childrenPruned = pruneTreeByFilter( node.children ?? [], filteredSet, filteredOnly ); if (hasFiltered || childrenPruned.length > 0) { const clone = Object.assign( Object.create(Object.getPrototypeOf(node)), node ); if (childrenPruned.length > 0) { clone.children = childrenPruned; } result.push(clone); } } return result; } function pruneUnifiedBySelection(nodes, selectionSet, selectedOnly, includePartial) { if (!selectedOnly) return nodes; const result = []; for (const node of nodes) { const allItems = node.getItems({ filteredOnly: false }); const selLeaves = allItems.filter((item) => selectionSet.has(item.id)); if (selectedOnly) { if (includePartial) { if (selLeaves.length === 0) continue; } else { if (selLeaves.length !== allItems.length) continue; } } const childrenPruned = pruneUnifiedBySelection( node.getSubGroups({ filteredOnly: false }), selectionSet, selectedOnly, includePartial ); const clone = Object.assign( Object.create(Object.getPrototypeOf(node)), node ); clone.getSubGroups = () => childrenPruned; clone.hasSubGroups = () => childrenPruned.length > 0; result.push(clone); } return result; } // ../multiselect-core/src/collapse/collapse-strategy.ts var CollapsePositionFirst = (inds) => Math.min(...inds); var CollapsePositionLast = (inds) => Math.max(...inds); function collapseSelection(tree, orderMap, options = {}) { const { filteredOnly = false, positionStrategy = CollapsePositionFirst } = options; const out = []; function walk(nodes) { for (const node of nodes) { const items = node.getItems({ filteredOnly }); const sel = items.filter((i) => i.isSelected); const children = node.getSubGroups({ filteredOnly }); if (sel.length === items.length && sel.length > 0) { const idxs = sel.map( (i) => orderMap.get(i.id) ?? Number.MAX_SAFE_INTEGER ); out.push({ type: "group", node, order: positionStrategy(idxs) }); } else { if (children.length === 0) { for (const item of sel) { const ord = orderMap.get(item.id) ?? Number.MAX_SAFE_INTEGER; out.push({ type: "item", node: item, order: ord }); } } walk(children); } } } walk(tree); return out.sort((a, b) => a.order - b.order); } // ../multiselect-core/src/collapse/collapse-manager.ts var CollapseManager = class { constructor(ms) { this.ms = ms; this.orderMap = /* @__PURE__ */ new Map(); this.counter = 0; ms.subscribe((_state, diff) => { for (const id of diff.added) { this.orderMap.set(id, this.counter++); } for (const id of diff.removed) { this.orderMap.delete(id); } }); } getCollapsedSelection(options = {}) { const filteredOnly = options.filteredOnly; const positionStrategy = options.positionStrategy ?? CollapsePositionFirst; const tree = this.ms.getGroupTree({ filter: { filteredOnly } }); const collapsed = collapseSelection(tree, this.orderMap, { filteredOnly, positionStrategy }); const treeItemIds = /* @__PURE__ */ new Set(); for (const node of tree) { for (const item of node.getItems({ filteredOnly })) { treeItemIds.add(item.id); } } const selectedItems = this.ms.getSelectedItems({ filteredOnly }); for (const item of selectedItems) { if (!treeItemIds.has(item.id)) { const order = this.orderMap.get(item.id) ?? Number.MAX_SAFE_INTEGER; collapsed.push({ type: "item", node: item, order }); } } return collapsed.sort((a, b) => a.order - b.order); } }; // ../multiselect-core/src/multiselect.ts var MultiSelectCore = class { constructor(options) { this.listeners = /* @__PURE__ */ new Set(); this.pendingDiff = { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() }; this.isFlushScheduled = false; this.data = options.data; const idAccessor = options.itemDef.id_accessor; this.selectedIds = new Set(options.initialState?.selectedIds); this.items = this.data.map( (dataItem) => createCoreSelectItem( dataItem, idAccessor, options.itemDef.meta, this.selectedIds, this.applyChange.bind(this) ) ); this.itemMap = /* @__PURE__ */ new Map(); for (const item of this.items) { this.itemMap.set(item.id, item); } this.groupMap = /* @__PURE__ */ new Map(); this.filterManager = new FilterManager(this.items, options.filter); const groupBySpecs = options.groupBy ?? []; const accessors = groupBySpecs.map( (spec) => typeof spec === "object" ? spec.accessor : spec ); this.groupManager = new GroupManager(this.items, accessors); this.metaFns = groupBySpecs.map( (spec) => typeof spec === "object" ? spec.getMeta : void 0 ); this.rawGroupTree = this.groupManager.getGroupTree(); this.scheduler = options.scheduler ?? rafScheduler; this.collapseManager = new CollapseManager(this); } applyChange(change) { if (change.added) { for (const id of change.added) { this.selectedIds.add(id); this.pendingDiff.removed.delete(id); this.pendingDiff.added.add(id); } } if (change.removed) { for (const id of change.removed) { this.selectedIds.delete(id); this.pendingDiff.added.delete(id); this.pendingDiff.removed.add(id); } } this.scheduleFlush(); } emit(diff) { const state = { selectedIds: new Set(this.selectedIds) }; for (const listener of this.listeners) { listener(state, { added: diff.added ?? [], removed: diff.removed ?? [] }); } } getItems() { return this.items; } getSelectedIds({ filteredOnly } = {}) { return this.getSelectedItems({ filteredOnly }).map((item) => item.id); } getSelectedItems({ filteredOnly } = {}) { const items = filteredOnly ? this.filterManager.getFilteredItems() : this.items; return items.filter((item) => item.isSelected); } isAllSelected() { return this.items.length > 0 && this.items.every((item) => item.isSelected); } selectAll() { for (const item of this.items) { item.select(); } } unselectAll() { for (const item of this.items) { item.unselect(); } } toggleAll() { if (this.isAllSelected()) { this.unselectAll(); } else { this.selectAll(); } } subscribe(listener) { this.listeners.add(listener); listener( { selectedIds: new Set(this.selectedIds) }, { added: [], removed: [] } ); return () => { this.listeners.delete(listener); }; } scheduleFlush() { if (this.isFlushScheduled) return; this.isFlushScheduled = true; this.scheduler.schedule(() => this.flush()); } flush() { const diff = { added: Array.from(this.pendingDiff.added), removed: Array.from(this.pendingDiff.removed) }; this.pendingDiff.added.clear(); this.pendingDiff.removed.clear(); this.isFlushScheduled = false; this.emit(diff); } getFilteredItems() { return this.filterManager.getFilteredItems(); } isAllFilteredSelected() { return this.filterManager.isAllFilteredSelected(); } selectAllFiltered() { this.filterManager.selectAllFiltered(); } unselectAllFiltered() { this.filterManager.unselectAllFiltered(); } toggleAllFiltered() { this.filterManager.toggleAllFiltered(); } setFilter(filter) { this.filterManager.setFilter(filter); } getGroupTree(options) { const filteredOnly = options?.filter?.filteredOnly ?? false; const selectedOnly = options?.selection?.selectedOnly ?? false; const includePartial = options?.selection?.includePartial ?? false; const filterQuery = this.filterManager.getFilter()?.query ?? ""; const effectiveFilteredOnly = filteredOnly && filterQuery.length > 0; const cacheKey = `${filterQuery}|${effectiveFilteredOnly}|${selectedOnly}|${includePartial}`; if (this.lastUnifiedTree && cacheKey === this.lastCacheKey) { return this.lastUnifiedTree; } const filteredSet = new Set( this.filterManager.getFilteredItems().map((i) => i.id) ); const prunedFilter = pruneTreeByFilter( this.rawGroupTree, filteredSet, effectiveFilteredOnly ); const unifiedByFilter = unifyGroupNodes( prunedFilter, filteredSet, effectiveFilteredOnly ); const selectionSet = new Set(this.getSelectedIds({ filteredOnly })); const finalTree = pruneUnifiedBySelection( unifiedByFilter, selectionSet, selectedOnly, includePartial ); this.lastUnifiedTree = finalTree; this.lastCacheKey = cacheKey; this.groupMap.clear(); const walk = (nodes) => { for (const node of nodes) { this.groupMap.set(node.key, node); walk(node.getSubGroups({ filteredOnly: false })); } }; walk(finalTree); const attachMeta = (nodes) => { for (const node of nodes) { const fn = this.metaFns[node.level]; node.getMeta = () => fn ? fn( node.key, node.getItems({ filteredOnly: false }).map((i) => i.data), node.level ) : void 0; attachMeta(node.getSubGroups({ filteredOnly: false })); } }; attachMeta(finalTree); return finalTree; } getSelectionSummary(options) { const total = options?.filteredOnly ? this.filterManager.getFilteredItems().length : this.items.length; const selected = this.getSelectedItems(options).length; return { total, selected }; } getCollapsedSelection(options) { return this.collapseManager.getCollapsedSelection(options); } getGroup(key) { this.getGroupTree(); return this.groupMap.get(key); } getItem(key) { return this.itemMap.get(key); } getGroupMeta(groupKey) { return this.getGroup(groupKey)?.getMeta(); } }; function createMultiSelect(options) { return new MultiSelectCore(options); } // src/use-multiselect.ts function useMultiSelect(options) { const coreRef = useRef( createMultiSelect({ ...options, scheduler: rafScheduler }) ); coreRef.current.setFilter(options.filter); const [, setTick] = useState(0); useEffect(() => { const unsubscribe = coreRef.current.subscribe(() => { setTick((t) => t + 1); }); return unsubscribe; }, []); return coreRef.current; } export { CollapseManager, CollapsePositionFirst, CollapsePositionLast, collapseSelection, createMultiSelect, useMultiSelect }; //# sourceMappingURL=index.mjs.map