UNPKG

@zag-js/tree-view

Version:

Core logic for the tree-view widget implemented as a state machine

1,287 lines (1,279 loc) 46.1 kB
'use strict'; var anatomy$1 = require('@zag-js/anatomy'); var collection$1 = require('@zag-js/collection'); var domQuery = require('@zag-js/dom-query'); var utils = require('@zag-js/utils'); var core = require('@zag-js/core'); var types = require('@zag-js/types'); // src/tree-view.anatomy.ts var anatomy = anatomy$1.createAnatomy("tree-view").parts( "branch", "branchContent", "branchControl", "branchIndentGuide", "branchIndicator", "branchText", "branchTrigger", "item", "itemIndicator", "itemText", "label", "nodeCheckbox", "nodeRenameInput", "root", "tree" ); var parts = anatomy.build(); var collection = (options) => { return new collection$1.TreeCollection(options); }; collection.empty = () => { return new collection$1.TreeCollection({ rootNode: { children: [] } }); }; function filePathCollection(paths) { return collection$1.filePathToTree(paths); } // src/tree-view.dom.ts var getRootId = (ctx) => ctx.ids?.root ?? `tree:${ctx.id}:root`; var getLabelId = (ctx) => ctx.ids?.label ?? `tree:${ctx.id}:label`; var getNodeId = (ctx, value) => ctx.ids?.node?.(value) ?? `tree:${ctx.id}:node:${value}`; var getTreeId = (ctx) => ctx.ids?.tree ?? `tree:${ctx.id}:tree`; var focusNode = (ctx, value) => { if (value == null) return; ctx.getById(getNodeId(ctx, value))?.focus(); }; var getRenameInputId = (ctx, value) => `tree:${ctx.id}:rename-input:${value}`; var getRenameInputEl = (ctx, value) => { return ctx.getById(getRenameInputId(ctx, value)); }; function getCheckedState(collection2, node, checkedValue) { const value = collection2.getNodeValue(node); if (!collection2.isBranchNode(node)) { return checkedValue.includes(value); } const childValues = collection2.getDescendantValues(value); const allChecked = childValues.every((v) => checkedValue.includes(v)); const someChecked = childValues.some((v) => checkedValue.includes(v)); return allChecked ? true : someChecked ? "indeterminate" : false; } function toggleBranchChecked(collection2, value, checkedValue) { const childValues = collection2.getDescendantValues(value); const allChecked = childValues.every((child) => checkedValue.includes(child)); return utils.uniq(allChecked ? utils.remove(checkedValue, ...childValues) : utils.add(checkedValue, ...childValues)); } function getCheckedValueMap(collection2, checkedValue) { const map = /* @__PURE__ */ new Map(); collection2.visit({ onEnter: (node) => { const value = collection2.getNodeValue(node); const isBranch = collection2.isBranchNode(node); const checked = getCheckedState(collection2, node, checkedValue); map.set(value, { type: isBranch ? "branch" : "leaf", checked }); } }); return map; } // src/tree-view.connect.ts function connect(service, normalize) { const { context, scope, computed, prop, send } = service; const collection2 = prop("collection"); const expandedValue = Array.from(context.get("expandedValue")); const selectedValue = Array.from(context.get("selectedValue")); const checkedValue = Array.from(context.get("checkedValue")); const isTypingAhead = computed("isTypingAhead"); const focusedValue = context.get("focusedValue"); const loadingStatus = context.get("loadingStatus"); const renamingValue = context.get("renamingValue"); function getNodeState(props2) { const { node, indexPath } = props2; const value = collection2.getNodeValue(node); const firstNode = collection2.getFirstNode(); const firstNodeValue = firstNode ? collection2.getNodeValue(firstNode) : null; return { id: getNodeId(scope, value), value, indexPath, valuePath: collection2.getValuePath(indexPath), disabled: Boolean(node.disabled), focused: focusedValue == null ? firstNodeValue == value : focusedValue === value, selected: selectedValue.includes(value), expanded: expandedValue.includes(value), loading: loadingStatus[value] === "loading", depth: indexPath.length, isBranch: collection2.isBranchNode(node), renaming: renamingValue === value, get checked() { return getCheckedState(collection2, node, checkedValue); } }; } return { collection: collection2, expandedValue, selectedValue, checkedValue, toggleChecked(value, isBranch) { send({ type: "CHECKED.TOGGLE", value, isBranch }); }, setChecked(value) { send({ type: "CHECKED.SET", value }); }, clearChecked() { send({ type: "CHECKED.CLEAR" }); }, getCheckedMap() { return getCheckedValueMap(collection2, checkedValue); }, expand(value) { send({ type: value ? "BRANCH.EXPAND" : "EXPANDED.ALL", value }); }, collapse(value) { send({ type: value ? "BRANCH.COLLAPSE" : "EXPANDED.CLEAR", value }); }, deselect(value) { send({ type: value ? "NODE.DESELECT" : "SELECTED.CLEAR", value }); }, select(value) { send({ type: value ? "NODE.SELECT" : "SELECTED.ALL", value, isTrusted: false }); }, getVisibleNodes() { return computed("visibleNodes"); }, focus(value) { focusNode(scope, value); }, selectParent(value) { const parentNode = collection2.getParentNode(value); if (!parentNode) return; const _selectedValue = utils.add(selectedValue, collection2.getNodeValue(parentNode)); send({ type: "SELECTED.SET", value: _selectedValue, src: "select.parent" }); }, expandParent(value) { const parentNode = collection2.getParentNode(value); if (!parentNode) return; const _expandedValue = utils.add(expandedValue, collection2.getNodeValue(parentNode)); send({ type: "EXPANDED.SET", value: _expandedValue, src: "expand.parent" }); }, setExpandedValue(value) { const _expandedValue = utils.uniq(value); send({ type: "EXPANDED.SET", value: _expandedValue }); }, setSelectedValue(value) { const _selectedValue = utils.uniq(value); send({ type: "SELECTED.SET", value: _selectedValue }); }, startRenaming(value) { send({ type: "NODE.RENAME", value }); }, submitRenaming(value, label) { send({ type: "RENAME.SUBMIT", value, label }); }, cancelRenaming() { send({ type: "RENAME.CANCEL" }); }, getRootProps() { return normalize.element({ ...parts.root.attrs, id: getRootId(scope), dir: prop("dir") }); }, getLabelProps() { return normalize.element({ ...parts.label.attrs, id: getLabelId(scope), dir: prop("dir") }); }, getTreeProps() { return normalize.element({ ...parts.tree.attrs, id: getTreeId(scope), dir: prop("dir"), role: "tree", "aria-label": "Tree View", "aria-labelledby": getLabelId(scope), "aria-multiselectable": prop("selectionMode") === "multiple" || void 0, tabIndex: -1, onKeyDown(event) { if (event.defaultPrevented) return; if (domQuery.isComposingEvent(event)) return; const target = domQuery.getEventTarget(event); if (domQuery.isEditableElement(target)) return; const node = target?.closest("[data-part=branch-control], [data-part=item]"); if (!node) return; const nodeId = node.dataset.value; if (nodeId == null) { console.warn(`[zag-js/tree-view] Node id not found for node`, node); return; } const isBranchNode = node.matches("[data-part=branch-control]"); const keyMap = { ArrowDown(event2) { if (domQuery.isModifierKey(event2)) return; event2.preventDefault(); send({ type: "NODE.ARROW_DOWN", id: nodeId, shiftKey: event2.shiftKey }); }, ArrowUp(event2) { if (domQuery.isModifierKey(event2)) return; event2.preventDefault(); send({ type: "NODE.ARROW_UP", id: nodeId, shiftKey: event2.shiftKey }); }, ArrowLeft(event2) { if (domQuery.isModifierKey(event2) || node.dataset.disabled) return; event2.preventDefault(); send({ type: isBranchNode ? "BRANCH_NODE.ARROW_LEFT" : "NODE.ARROW_LEFT", id: nodeId }); }, ArrowRight(event2) { if (!isBranchNode || node.dataset.disabled) return; event2.preventDefault(); send({ type: "BRANCH_NODE.ARROW_RIGHT", id: nodeId }); }, Home(event2) { if (domQuery.isModifierKey(event2)) return; event2.preventDefault(); send({ type: "NODE.HOME", id: nodeId, shiftKey: event2.shiftKey }); }, End(event2) { if (domQuery.isModifierKey(event2)) return; event2.preventDefault(); send({ type: "NODE.END", id: nodeId, shiftKey: event2.shiftKey }); }, Space(event2) { if (node.dataset.disabled) return; if (isTypingAhead) { send({ type: "TREE.TYPEAHEAD", key: event2.key }); } else { keyMap.Enter?.(event2); } }, Enter(event2) { if (node.dataset.disabled) return; if (domQuery.isAnchorElement(target) && domQuery.isModifierKey(event2)) return; send({ type: isBranchNode ? "BRANCH_NODE.CLICK" : "NODE.CLICK", id: nodeId, src: "keyboard" }); if (!domQuery.isAnchorElement(target)) { event2.preventDefault(); } }, "*"(event2) { if (node.dataset.disabled) return; event2.preventDefault(); send({ type: "SIBLINGS.EXPAND", id: nodeId }); }, a(event2) { if (!event2.metaKey || node.dataset.disabled) return; event2.preventDefault(); send({ type: "SELECTED.ALL", moveFocus: true }); }, F2(event2) { if (node.dataset.disabled) return; const canRenameFn = prop("canRename"); if (!canRenameFn) return; const indexPath = collection2.getIndexPath(nodeId); if (indexPath) { const node2 = collection2.at(indexPath); if (node2 && !canRenameFn(node2, indexPath)) { return; } } event2.preventDefault(); send({ type: "NODE.RENAME", value: nodeId }); } }; const key = domQuery.getEventKey(event, { dir: prop("dir") }); const exec = keyMap[key]; if (exec) { exec(event); return; } if (!domQuery.getByTypeahead.isValidEvent(event)) return; send({ type: "TREE.TYPEAHEAD", key: event.key, id: nodeId }); event.preventDefault(); } }); }, getNodeState, getItemProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.item.attrs, id: nodeState.id, dir: prop("dir"), "data-ownedby": getTreeId(scope), "data-path": props2.indexPath.join("/"), "data-value": nodeState.value, tabIndex: nodeState.focused ? 0 : -1, "data-focus": domQuery.dataAttr(nodeState.focused), role: "treeitem", "aria-current": nodeState.selected ? "true" : void 0, "aria-selected": nodeState.disabled ? void 0 : nodeState.selected, "data-selected": domQuery.dataAttr(nodeState.selected), "aria-disabled": domQuery.ariaAttr(nodeState.disabled), "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-renaming": domQuery.dataAttr(nodeState.renaming), "aria-level": nodeState.depth, "data-depth": nodeState.depth, style: { "--depth": nodeState.depth }, onFocus(event) { event.stopPropagation(); send({ type: "NODE.FOCUS", id: nodeState.value }); }, onClick(event) { if (nodeState.disabled) return; if (!domQuery.isLeftClick(event)) return; if (domQuery.isAnchorElement(event.currentTarget) && domQuery.isModifierKey(event)) return; const isMetaKey = event.metaKey || event.ctrlKey; send({ type: "NODE.CLICK", id: nodeState.value, shiftKey: event.shiftKey, ctrlKey: isMetaKey }); event.stopPropagation(); if (!domQuery.isAnchorElement(event.currentTarget)) { event.preventDefault(); } } }); }, getItemTextProps(props2) { const itemState = getNodeState(props2); return normalize.element({ ...parts.itemText.attrs, "data-disabled": domQuery.dataAttr(itemState.disabled), "data-selected": domQuery.dataAttr(itemState.selected), "data-focus": domQuery.dataAttr(itemState.focused) }); }, getItemIndicatorProps(props2) { const itemState = getNodeState(props2); return normalize.element({ ...parts.itemIndicator.attrs, "aria-hidden": true, "data-disabled": domQuery.dataAttr(itemState.disabled), "data-selected": domQuery.dataAttr(itemState.selected), "data-focus": domQuery.dataAttr(itemState.focused), hidden: !itemState.selected }); }, getBranchProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branch.attrs, "data-depth": nodeState.depth, dir: prop("dir"), "data-branch": nodeState.value, role: "treeitem", "data-ownedby": getTreeId(scope), "data-value": nodeState.value, "aria-level": nodeState.depth, "aria-selected": nodeState.disabled ? void 0 : nodeState.selected, "data-path": props2.indexPath.join("/"), "data-selected": domQuery.dataAttr(nodeState.selected), "aria-expanded": nodeState.expanded, "data-state": nodeState.expanded ? "open" : "closed", "aria-disabled": domQuery.ariaAttr(nodeState.disabled), "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-loading": domQuery.dataAttr(nodeState.loading), "aria-busy": domQuery.ariaAttr(nodeState.loading), style: { "--depth": nodeState.depth } }); }, getBranchIndicatorProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchIndicator.attrs, "aria-hidden": true, "data-state": nodeState.expanded ? "open" : "closed", "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-selected": domQuery.dataAttr(nodeState.selected), "data-focus": domQuery.dataAttr(nodeState.focused), "data-loading": domQuery.dataAttr(nodeState.loading) }); }, getBranchTriggerProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchTrigger.attrs, role: "button", dir: prop("dir"), "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-state": nodeState.expanded ? "open" : "closed", "data-value": nodeState.value, "data-loading": domQuery.dataAttr(nodeState.loading), disabled: nodeState.loading, onClick(event) { if (nodeState.disabled || nodeState.loading) return; send({ type: "BRANCH_TOGGLE.CLICK", id: nodeState.value }); event.stopPropagation(); } }); }, getBranchControlProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchControl.attrs, role: "button", id: nodeState.id, dir: prop("dir"), tabIndex: nodeState.focused ? 0 : -1, "data-path": props2.indexPath.join("/"), "data-state": nodeState.expanded ? "open" : "closed", "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-selected": domQuery.dataAttr(nodeState.selected), "data-focus": domQuery.dataAttr(nodeState.focused), "data-renaming": domQuery.dataAttr(nodeState.renaming), "data-value": nodeState.value, "data-depth": nodeState.depth, "data-loading": domQuery.dataAttr(nodeState.loading), "aria-busy": domQuery.ariaAttr(nodeState.loading), onFocus(event) { send({ type: "NODE.FOCUS", id: nodeState.value }); event.stopPropagation(); }, onClick(event) { if (nodeState.disabled) return; if (nodeState.loading) return; if (!domQuery.isLeftClick(event)) return; if (domQuery.isAnchorElement(event.currentTarget) && domQuery.isModifierKey(event)) return; const isMetaKey = event.metaKey || event.ctrlKey; send({ type: "BRANCH_NODE.CLICK", id: nodeState.value, shiftKey: event.shiftKey, ctrlKey: isMetaKey }); event.stopPropagation(); } }); }, getBranchTextProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchText.attrs, dir: prop("dir"), "data-disabled": domQuery.dataAttr(nodeState.disabled), "data-state": nodeState.expanded ? "open" : "closed", "data-loading": domQuery.dataAttr(nodeState.loading) }); }, getBranchContentProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchContent.attrs, role: "group", dir: prop("dir"), "data-state": nodeState.expanded ? "open" : "closed", "data-depth": nodeState.depth, "data-path": props2.indexPath.join("/"), "data-value": nodeState.value, hidden: !nodeState.expanded }); }, getBranchIndentGuideProps(props2) { const nodeState = getNodeState(props2); return normalize.element({ ...parts.branchIndentGuide.attrs, "data-depth": nodeState.depth }); }, getNodeCheckboxProps(props2) { const nodeState = getNodeState(props2); const checkedState = nodeState.checked; return normalize.element({ ...parts.nodeCheckbox.attrs, tabIndex: -1, role: "checkbox", "data-state": checkedState === true ? "checked" : checkedState === false ? "unchecked" : "indeterminate", "aria-checked": checkedState === true ? "true" : checkedState === false ? "false" : "mixed", "data-disabled": domQuery.dataAttr(nodeState.disabled), onClick(event) { if (event.defaultPrevented) return; if (nodeState.disabled) return; if (!domQuery.isLeftClick(event)) return; send({ type: "CHECKED.TOGGLE", value: nodeState.value, isBranch: nodeState.isBranch }); event.stopPropagation(); const node = event.currentTarget.closest("[role=treeitem]"); node?.focus({ preventScroll: true }); } }); }, getNodeRenameInputProps(props2) { const nodeState = getNodeState(props2); return normalize.input({ ...parts.nodeRenameInput.attrs, id: getRenameInputId(scope, nodeState.value), type: "text", "aria-label": "Rename tree item", hidden: !nodeState.renaming, onKeyDown(event) { if (domQuery.isComposingEvent(event)) return; if (event.key === "Escape") { send({ type: "RENAME.CANCEL" }); event.preventDefault(); } if (event.key === "Enter") { send({ type: "RENAME.SUBMIT", label: event.currentTarget.value }); event.preventDefault(); } event.stopPropagation(); }, onBlur(event) { send({ type: "RENAME.SUBMIT", label: event.currentTarget.value }); } }); } }; } function expandBranches(params, values) { const { context, prop, refs } = params; if (!prop("loadChildren")) { context.set("expandedValue", (prev) => utils.uniq(utils.add(prev, ...values))); return; } const loadingStatus = context.get("loadingStatus"); const [loadedValues, loadingValues] = utils.partition(values, (value) => loadingStatus[value] === "loaded"); if (loadedValues.length > 0) { context.set("expandedValue", (prev) => utils.uniq(utils.add(prev, ...loadedValues))); } if (loadingValues.length === 0) return; const collection2 = prop("collection"); const [nodeWithChildren, nodeWithoutChildren] = utils.partition(loadingValues, (id) => { const node = collection2.findNode(id); return collection2.getNodeChildren(node).length > 0; }); if (nodeWithChildren.length > 0) { context.set("expandedValue", (prev) => utils.uniq(utils.add(prev, ...nodeWithChildren))); } if (nodeWithoutChildren.length === 0) return; context.set("loadingStatus", (prev) => ({ ...prev, ...nodeWithoutChildren.reduce((acc, id) => ({ ...acc, [id]: "loading" }), {}) })); const nodesToLoad = nodeWithoutChildren.map((id) => { const indexPath = collection2.getIndexPath(id); const valuePath = collection2.getValuePath(indexPath); const node = collection2.findNode(id); return { id, indexPath, valuePath, node }; }); const pendingAborts = refs.get("pendingAborts"); const loadChildren = prop("loadChildren"); utils.ensure(loadChildren, () => "[zag-js/tree-view] `loadChildren` is required for async expansion"); const proms = nodesToLoad.map(({ id, indexPath, valuePath, node }) => { const existingAbort = pendingAborts.get(id); if (existingAbort) { existingAbort.abort(); pendingAborts.delete(id); } const abortController = new AbortController(); pendingAborts.set(id, abortController); return loadChildren({ valuePath, indexPath, node, signal: abortController.signal }); }); Promise.allSettled(proms).then((results) => { const loadedValues2 = []; const nodeWithErrors = []; const nextLoadingStatus = context.get("loadingStatus"); let collection3 = prop("collection"); results.forEach((result, index) => { const { id, indexPath, node, valuePath } = nodesToLoad[index]; if (result.status === "fulfilled") { nextLoadingStatus[id] = "loaded"; loadedValues2.push(id); collection3 = collection3.replace(indexPath, { ...node, children: result.value }); } else { pendingAborts.delete(id); Reflect.deleteProperty(nextLoadingStatus, id); nodeWithErrors.push({ node, error: result.reason, indexPath, valuePath }); } }); context.set("loadingStatus", nextLoadingStatus); if (loadedValues2.length) { context.set("expandedValue", (prev) => utils.uniq(utils.add(prev, ...loadedValues2))); prop("onLoadChildrenComplete")?.({ collection: collection3 }); } if (nodeWithErrors.length) { prop("onLoadChildrenError")?.({ nodes: nodeWithErrors }); } }); } // src/utils/visit-skip.ts function skipFn(params) { const { prop, context } = params; return function skip({ indexPath }) { const paths = prop("collection").getValuePath(indexPath).slice(0, -1); return paths.some((value) => !context.get("expandedValue").includes(value)); }; } // src/tree-view.machine.ts var { and } = core.createGuards(); var machine = core.createMachine({ props({ props: props2 }) { return { selectionMode: "single", collection: collection.empty(), typeahead: true, expandOnClick: true, defaultExpandedValue: [], defaultSelectedValue: [], ...props2 }; }, initialState() { return "idle"; }, context({ prop, bindable, getContext }) { return { expandedValue: bindable(() => ({ defaultValue: prop("defaultExpandedValue"), value: prop("expandedValue"), isEqual: utils.isEqual, onChange(expandedValue) { const ctx = getContext(); const focusedValue = ctx.get("focusedValue"); prop("onExpandedChange")?.({ expandedValue, focusedValue, get expandedNodes() { return prop("collection").findNodes(expandedValue); } }); } })), selectedValue: bindable(() => ({ defaultValue: prop("defaultSelectedValue"), value: prop("selectedValue"), isEqual: utils.isEqual, onChange(selectedValue) { const ctx = getContext(); const focusedValue = ctx.get("focusedValue"); prop("onSelectionChange")?.({ selectedValue, focusedValue, get selectedNodes() { return prop("collection").findNodes(selectedValue); } }); } })), focusedValue: bindable(() => ({ defaultValue: prop("defaultFocusedValue") || null, value: prop("focusedValue"), onChange(focusedValue) { prop("onFocusChange")?.({ focusedValue, get focusedNode() { return focusedValue ? prop("collection").findNode(focusedValue) : null; } }); } })), loadingStatus: bindable(() => ({ defaultValue: {} })), checkedValue: bindable(() => ({ defaultValue: prop("defaultCheckedValue") || [], value: prop("checkedValue"), isEqual: utils.isEqual, onChange(value) { prop("onCheckedChange")?.({ checkedValue: value }); } })), renamingValue: bindable(() => ({ sync: true, defaultValue: null })) }; }, refs() { return { typeaheadState: { ...domQuery.getByTypeahead.defaultOptions }, pendingAborts: /* @__PURE__ */ new Map() }; }, computed: { isMultipleSelection: ({ prop }) => prop("selectionMode") === "multiple", isTypingAhead: ({ refs }) => refs.get("typeaheadState").keysSoFar.length > 0, visibleNodes: ({ prop, context }) => { const nodes = []; prop("collection").visit({ skip: skipFn({ prop, context }), onEnter: (node, indexPath) => { nodes.push({ node, indexPath }); } }); return nodes; } }, on: { "EXPANDED.SET": { actions: ["setExpanded"] }, "EXPANDED.CLEAR": { actions: ["clearExpanded"] }, "EXPANDED.ALL": { actions: ["expandAllBranches"] }, "BRANCH.EXPAND": { actions: ["expandBranches"] }, "BRANCH.COLLAPSE": { actions: ["collapseBranches"] }, "SELECTED.SET": { actions: ["setSelected"] }, "SELECTED.ALL": [ { guard: and("isMultipleSelection", "moveFocus"), actions: ["selectAllNodes", "focusTreeLastNode"] }, { guard: "isMultipleSelection", actions: ["selectAllNodes"] } ], "SELECTED.CLEAR": { actions: ["clearSelected"] }, "NODE.SELECT": { actions: ["selectNode"] }, "NODE.DESELECT": { actions: ["deselectNode"] }, "CHECKED.TOGGLE": { actions: ["toggleChecked"] }, "CHECKED.SET": { actions: ["setChecked"] }, "CHECKED.CLEAR": { actions: ["clearChecked"] }, "NODE.FOCUS": { actions: ["setFocusedNode"] }, "NODE.ARROW_DOWN": [ { guard: and("isShiftKey", "isMultipleSelection"), actions: ["focusTreeNextNode", "extendSelectionToNextNode"] }, { actions: ["focusTreeNextNode"] } ], "NODE.ARROW_UP": [ { guard: and("isShiftKey", "isMultipleSelection"), actions: ["focusTreePrevNode", "extendSelectionToPrevNode"] }, { actions: ["focusTreePrevNode"] } ], "NODE.ARROW_LEFT": { actions: ["focusBranchNode"] }, "BRANCH_NODE.ARROW_LEFT": [ { guard: "isBranchExpanded", actions: ["collapseBranch"] }, { actions: ["focusBranchNode"] } ], "BRANCH_NODE.ARROW_RIGHT": [ { guard: and("isBranchFocused", "isBranchExpanded"), actions: ["focusBranchFirstNode"] }, { actions: ["expandBranch"] } ], "SIBLINGS.EXPAND": { actions: ["expandSiblingBranches"] }, "NODE.HOME": [ { guard: and("isShiftKey", "isMultipleSelection"), actions: ["extendSelectionToFirstNode", "focusTreeFirstNode"] }, { actions: ["focusTreeFirstNode"] } ], "NODE.END": [ { guard: and("isShiftKey", "isMultipleSelection"), actions: ["extendSelectionToLastNode", "focusTreeLastNode"] }, { actions: ["focusTreeLastNode"] } ], "NODE.CLICK": [ { guard: and("isCtrlKey", "isMultipleSelection"), actions: ["toggleNodeSelection"] }, { guard: and("isShiftKey", "isMultipleSelection"), actions: ["extendSelectionToNode"] }, { actions: ["selectNode"] } ], "BRANCH_NODE.CLICK": [ { guard: and("isCtrlKey", "isMultipleSelection"), actions: ["toggleNodeSelection"] }, { guard: and("isShiftKey", "isMultipleSelection"), actions: ["extendSelectionToNode"] }, { guard: "expandOnClick", actions: ["selectNode", "toggleBranchNode"] }, { actions: ["selectNode"] } ], "BRANCH_TOGGLE.CLICK": { actions: ["toggleBranchNode"] }, "TREE.TYPEAHEAD": { actions: ["focusMatchedNode"] } }, exit: ["clearPendingAborts"], states: { idle: { on: { "NODE.RENAME": { target: "renaming", actions: ["setRenamingValue"] } } }, renaming: { entry: ["syncRenameInput", "focusRenameInput"], on: { "RENAME.SUBMIT": { guard: "isRenameLabelValid", target: "idle", actions: ["submitRenaming"] }, "RENAME.CANCEL": { target: "idle", actions: ["cancelRenaming"] } } } }, implementations: { guards: { isBranchFocused: ({ context, event }) => context.get("focusedValue") === event.id, isBranchExpanded: ({ context, event }) => context.get("expandedValue").includes(event.id), isShiftKey: ({ event }) => event.shiftKey, isCtrlKey: ({ event }) => event.ctrlKey, hasSelectedItems: ({ context }) => context.get("selectedValue").length > 0, isMultipleSelection: ({ prop }) => prop("selectionMode") === "multiple", moveFocus: ({ event }) => !!event.moveFocus, expandOnClick: ({ prop }) => !!prop("expandOnClick"), isRenameLabelValid: ({ event }) => event.label.trim() !== "" }, actions: { selectNode({ context, event }) { const value = event.id || event.value; context.set("selectedValue", (prev) => { if (value == null) return prev; if (!event.isTrusted && utils.isArray(value)) return prev.concat(...value); return [utils.isArray(value) ? utils.last(value) : value].filter(Boolean); }); }, deselectNode({ context, event }) { const value = utils.toArray(event.id || event.value); context.set("selectedValue", (prev) => utils.remove(prev, ...value)); }, setFocusedNode({ context, event }) { context.set("focusedValue", event.id); }, clearFocusedNode({ context }) { context.set("focusedValue", null); }, clearSelectedItem({ context }) { context.set("selectedValue", []); }, toggleBranchNode({ context, event, action }) { const isExpanded = context.get("expandedValue").includes(event.id); action(isExpanded ? ["collapseBranch"] : ["expandBranch"]); }, expandBranch(params) { const { event } = params; expandBranches(params, [event.id]); }, expandBranches(params) { const { context, event } = params; const valuesToExpand = utils.toArray(event.value); expandBranches(params, utils.diff(valuesToExpand, context.get("expandedValue"))); }, collapseBranch({ context, event }) { context.set("expandedValue", (prev) => utils.remove(prev, event.id)); }, collapseBranches(params) { const { context, event } = params; const value = utils.toArray(event.value); context.set("expandedValue", (prev) => utils.remove(prev, ...value)); }, setExpanded({ context, event }) { if (!utils.isArray(event.value)) return; context.set("expandedValue", event.value); }, clearExpanded({ context }) { context.set("expandedValue", []); }, setSelected({ context, event }) { if (!utils.isArray(event.value)) return; context.set("selectedValue", event.value); }, clearSelected({ context }) { context.set("selectedValue", []); }, focusTreeFirstNode(params) { const { prop, scope } = params; const collection2 = prop("collection"); const firstNode = collection2.getFirstNode(); const firstValue = collection2.getNodeValue(firstNode); const scrolled = scrollToNode(params, firstValue); if (scrolled) domQuery.raf(() => focusNode(scope, firstValue)); else focusNode(scope, firstValue); }, focusTreeLastNode(params) { const { prop, scope } = params; const collection2 = prop("collection"); const lastNode = collection2.getLastNode(void 0, { skip: skipFn(params) }); const lastValue = collection2.getNodeValue(lastNode); const scrolled = scrollToNode(params, lastValue); if (scrolled) domQuery.raf(() => focusNode(scope, lastValue)); else focusNode(scope, lastValue); }, focusBranchFirstNode(params) { const { event, prop, scope } = params; const collection2 = prop("collection"); const branchNode = collection2.findNode(event.id); const firstNode = collection2.getFirstNode(branchNode); const firstValue = collection2.getNodeValue(firstNode); const scrolled = scrollToNode(params, firstValue); if (scrolled) domQuery.raf(() => focusNode(scope, firstValue)); else focusNode(scope, firstValue); }, focusTreeNextNode(params) { const { event, prop, scope } = params; const collection2 = prop("collection"); const nextNode = collection2.getNextNode(event.id, { skip: skipFn(params) }); if (!nextNode) return; const nextValue = collection2.getNodeValue(nextNode); const scrolled = scrollToNode(params, nextValue); if (scrolled) domQuery.raf(() => focusNode(scope, nextValue)); else focusNode(scope, nextValue); }, focusTreePrevNode(params) { const { event, prop, scope } = params; const collection2 = prop("collection"); const prevNode = collection2.getPreviousNode(event.id, { skip: skipFn(params) }); if (!prevNode) return; const prevValue = collection2.getNodeValue(prevNode); const scrolled = scrollToNode(params, prevValue); if (scrolled) domQuery.raf(() => focusNode(scope, prevValue)); else focusNode(scope, prevValue); }, focusBranchNode(params) { const { event, prop, scope } = params; const collection2 = prop("collection"); const parentNode = collection2.getParentNode(event.id); const parentValue = parentNode ? collection2.getNodeValue(parentNode) : void 0; if (!parentValue) return; const scrolled = scrollToNode(params, parentValue); if (scrolled) domQuery.raf(() => focusNode(scope, parentValue)); else focusNode(scope, parentValue); }, selectAllNodes({ context, prop }) { context.set("selectedValue", prop("collection").getValues()); }, focusMatchedNode(params) { const { context, prop, refs, event, scope, computed } = params; const nodes = computed("visibleNodes"); const elements = nodes.map(({ node: node2 }) => ({ textContent: prop("collection").stringifyNode(node2), id: prop("collection").getNodeValue(node2) })); const node = domQuery.getByTypeahead(elements, { state: refs.get("typeaheadState"), activeId: context.get("focusedValue"), key: event.key }); if (!node?.id) return; const scrolled = scrollToNode(params, node.id); if (scrolled) domQuery.raf(() => focusNode(scope, node.id)); else focusNode(scope, node.id); }, toggleNodeSelection({ context, event }) { const selectedValue = utils.addOrRemove(context.get("selectedValue"), event.id); context.set("selectedValue", selectedValue); }, expandAllBranches(params) { const { context, prop } = params; const branchValues = prop("collection").getBranchValues(); const valuesToExpand = utils.diff(branchValues, context.get("expandedValue")); expandBranches(params, valuesToExpand); }, expandSiblingBranches(params) { const { context, event, prop } = params; const collection2 = prop("collection"); const indexPath = collection2.getIndexPath(event.id); if (!indexPath) return; const nodes = collection2.getSiblingNodes(indexPath); const values = nodes.map((node) => collection2.getNodeValue(node)); const valuesToExpand = utils.diff(values, context.get("expandedValue")); expandBranches(params, valuesToExpand); }, extendSelectionToNode(params) { const { context, event, prop, computed } = params; const collection2 = prop("collection"); const anchorValue = utils.first(context.get("selectedValue")) || collection2.getNodeValue(collection2.getFirstNode()); const targetValue = event.id; let values = [anchorValue, targetValue]; let hits = 0; const visibleNodes = computed("visibleNodes"); visibleNodes.forEach(({ node }) => { const nodeValue = collection2.getNodeValue(node); if (hits === 1) values.push(nodeValue); if (nodeValue === anchorValue || nodeValue === targetValue) hits++; }); context.set("selectedValue", utils.uniq(values)); }, extendSelectionToNextNode(params) { const { context, event, prop } = params; const collection2 = prop("collection"); const nextNode = collection2.getNextNode(event.id, { skip: skipFn(params) }); if (!nextNode) return; const values = new Set(context.get("selectedValue")); const nextValue = collection2.getNodeValue(nextNode); if (nextValue == null) return; if (values.has(event.id) && values.has(nextValue)) { values.delete(event.id); } else if (!values.has(nextValue)) { values.add(nextValue); } context.set("selectedValue", Array.from(values)); }, extendSelectionToPrevNode(params) { const { context, event, prop } = params; const collection2 = prop("collection"); const prevNode = collection2.getPreviousNode(event.id, { skip: skipFn(params) }); if (!prevNode) return; const values = new Set(context.get("selectedValue")); const prevValue = collection2.getNodeValue(prevNode); if (prevValue == null) return; if (values.has(event.id) && values.has(prevValue)) { values.delete(event.id); } else if (!values.has(prevValue)) { values.add(prevValue); } context.set("selectedValue", Array.from(values)); }, extendSelectionToFirstNode(params) { const { context, prop } = params; const collection2 = prop("collection"); const currentSelection = utils.first(context.get("selectedValue")); const values = []; collection2.visit({ skip: skipFn(params), onEnter: (node) => { const nodeValue = collection2.getNodeValue(node); values.push(nodeValue); if (nodeValue === currentSelection) { return "stop"; } } }); context.set("selectedValue", values); }, extendSelectionToLastNode(params) { const { context, prop } = params; const collection2 = prop("collection"); const currentSelection = utils.first(context.get("selectedValue")); const values = []; let current = false; collection2.visit({ skip: skipFn(params), onEnter: (node) => { const nodeValue = collection2.getNodeValue(node); if (nodeValue === currentSelection) current = true; if (current) values.push(nodeValue); } }); context.set("selectedValue", values); }, clearPendingAborts({ refs }) { const aborts = refs.get("pendingAborts"); aborts.forEach((abort) => abort.abort()); aborts.clear(); }, toggleChecked({ context, event, prop }) { const collection2 = prop("collection"); context.set( "checkedValue", (prev) => event.isBranch ? toggleBranchChecked(collection2, event.value, prev) : utils.addOrRemove(prev, event.value) ); }, setChecked({ context, event }) { context.set("checkedValue", event.value); }, clearChecked({ context }) { context.set("checkedValue", []); }, setRenamingValue({ context, event, prop }) { context.set("renamingValue", event.value); const onRenameStartFn = prop("onRenameStart"); if (onRenameStartFn) { const collection2 = prop("collection"); const indexPath = collection2.getIndexPath(event.value); if (indexPath) { const node = collection2.at(indexPath); if (node) { onRenameStartFn({ value: event.value, node, indexPath }); } } } }, submitRenaming({ context, event, prop, scope }) { const renamingValue = context.get("renamingValue"); if (!renamingValue) return; const collection2 = prop("collection"); const indexPath = collection2.getIndexPath(renamingValue); if (!indexPath) return; const trimmedLabel = event.label.trim(); const onBeforeRenameFn = prop("onBeforeRename"); if (onBeforeRenameFn) { const details = { value: renamingValue, label: trimmedLabel, indexPath }; const shouldRename = onBeforeRenameFn(details); if (!shouldRename) { context.set("renamingValue", null); focusNode(scope, renamingValue); return; } } prop("onRenameComplete")?.({ value: renamingValue, label: trimmedLabel, indexPath }); context.set("renamingValue", null); focusNode(scope, renamingValue); }, cancelRenaming({ context, scope }) { const renamingValue = context.get("renamingValue"); context.set("renamingValue", null); if (renamingValue) { focusNode(scope, renamingValue); } }, syncRenameInput({ context, scope, prop }) { const renamingValue = context.get("renamingValue"); if (!renamingValue) return; const collection2 = prop("collection"); const node = collection2.findNode(renamingValue); if (!node) return; const label = collection2.stringifyNode(node); const inputEl = getRenameInputEl(scope, renamingValue); domQuery.setElementValue(inputEl, label); }, focusRenameInput({ context, scope }) { const renamingValue = context.get("renamingValue"); if (!renamingValue) return; const inputEl = getRenameInputEl(scope, renamingValue); if (!inputEl) return; inputEl.focus(); inputEl.select(); } } } }); function scrollToNode(params, value) { const { prop, scope, computed } = params; const scrollToIndexFn = prop("scrollToIndexFn"); if (!scrollToIndexFn) return false; const collection2 = prop("collection"); const visibleNodes = computed("visibleNodes"); for (let i = 0; i < visibleNodes.length; i++) { const { node, indexPath } = visibleNodes[i]; if (collection2.getNodeValue(node) !== value) continue; scrollToIndexFn({ index: i, node, indexPath, getElement: () => scope.getById(getNodeId(scope, value)) }); return true; } return false; } var props = types.createProps()([ "ids", "collection", "dir", "expandedValue", "expandOnClick", "defaultFocusedValue", "focusedValue", "getRootNode", "id", "onExpandedChange", "onFocusChange", "onSelectionChange", "checkedValue", "selectedValue", "selectionMode", "typeahead", "defaultExpandedValue", "defaultSelectedValue", "defaultCheckedValue", "onCheckedChange", "onLoadChildrenComplete", "onLoadChildrenError", "loadChildren", "canRename", "onRenameStart", "onBeforeRename", "onRenameComplete", "scrollToIndexFn" ]); var splitProps = utils.createSplitProps(props); var itemProps = types.createProps()(["node", "indexPath"]); var splitItemProps = utils.createSplitProps(itemProps); exports.anatomy = anatomy; exports.collection = collection; exports.connect = connect; exports.filePathCollection = filePathCollection; exports.itemProps = itemProps; exports.machine = machine; exports.props = props; exports.splitItemProps = splitItemProps; exports.splitProps = splitProps;