UNPKG

primevue

Version:

PrimeVue is an open source UI library for Vue featuring a rich set of 80+ components, a theme designer, various theme alternatives such as Material, Bootstrap, Tailwind, premium templates and professional support. In addition, it integrates with PrimeBloc

1 lines 164 kB
{"version":3,"file":"index.mjs","sources":["../../src/tree/BaseTree.vue","../../src/tree/TreeDragDropService.js","../../src/tree/TreeNode.vue","../../src/tree/TreeNode.vue?vue&type=template&id=18d191c2&lang.js","../../src/tree/Tree.vue","../../src/tree/Tree.vue?vue&type=template&id=5875535c&lang.js"],"sourcesContent":["<script>\nimport BaseComponent from '@primevue/core/basecomponent';\nimport TreeStyle from 'primevue/tree/style';\n\nexport default {\n name: 'BaseTree',\n extends: BaseComponent,\n props: {\n value: {\n type: null,\n default: null\n },\n expandedKeys: {\n type: null,\n default: null\n },\n selectionKeys: {\n type: null,\n default: null\n },\n selectionMode: {\n type: String,\n default: null\n },\n metaKeySelection: {\n type: Boolean,\n default: false\n },\n loading: {\n type: Boolean,\n default: false\n },\n loadingIcon: {\n type: String,\n default: undefined\n },\n loadingMode: {\n type: String,\n default: 'mask'\n },\n filter: {\n type: Boolean,\n default: false\n },\n filterBy: {\n type: [String, Function],\n default: 'label'\n },\n filterMode: {\n type: String,\n default: 'lenient'\n },\n filterPlaceholder: {\n type: String,\n default: null\n },\n filterLocale: {\n type: String,\n default: undefined\n },\n highlightOnSelect: {\n type: Boolean,\n default: false\n },\n scrollHeight: {\n type: String,\n default: null\n },\n level: {\n type: Number,\n default: 0\n },\n draggableNodes: {\n type: Boolean,\n default: null\n },\n droppableNodes: {\n type: Boolean,\n default: null\n },\n draggableScope: {\n type: [String, Array],\n default: null\n },\n droppableScope: {\n type: [String, Array],\n default: null\n },\n validateDrop: {\n type: Boolean,\n default: false\n },\n ariaLabelledby: {\n type: String,\n default: null\n },\n ariaLabel: {\n type: String,\n default: null\n }\n },\n style: TreeStyle,\n provide() {\n return {\n $pcTree: this,\n $parentInstance: this\n };\n }\n};\n</script>\n","import { reactive } from 'vue';\n\nconst dragState = reactive({\n isDragging: false,\n dragNode: null,\n dragScope: null\n});\n\nconst dragStartHandlers = new Set();\nconst dragStopHandlers = new Set();\n\nexport function useTreeDragDropService() {\n const startDrag = (event) => {\n dragState.isDragging = true;\n dragState.dragNode = event.node;\n dragState.dragScope = event.scope;\n\n dragStartHandlers.forEach((handler) => handler(event));\n };\n\n const stopDrag = (event) => {\n dragState.isDragging = false;\n dragState.dragNode = null;\n dragState.dragScope = null;\n\n dragStopHandlers.forEach((handler) => handler(event));\n };\n\n const onDragStart = (handler) => {\n dragStartHandlers.add(handler);\n\n return () => dragStartHandlers.delete(handler);\n };\n\n const onDragStop = (handler) => {\n dragStopHandlers.add(handler);\n\n return () => dragStopHandlers.delete(handler);\n };\n\n return {\n dragState,\n startDrag,\n stopDrag,\n onDragStart,\n onDragStop\n };\n}\n","<template>\n <li\n ref=\"currentNode\"\n :class=\"cx('node')\"\n role=\"treeitem\"\n :aria-label=\"label(node)\"\n :aria-selected=\"ariaSelected\"\n :aria-expanded=\"expanded\"\n :aria-setsize=\"node.children ? node.children.length : 0\"\n :aria-posinset=\"index + 1\"\n :aria-level=\"level\"\n :aria-checked=\"ariaChecked\"\n :tabindex=\"index === 0 ? 0 : -1\"\n @keydown=\"onKeyDown\"\n v-bind=\"getPTOptions('node')\"\n >\n <div v-if=\"isPrevDropPointActive\" :class=\"cx('dropPoint')\" aria-hidden=\"true\" />\n <div\n :class=\"cx('nodeContent')\"\n :style=\"node.style\"\n :draggable=\"isDraggable\"\n @click=\"onClick\"\n @touchend=\"onTouchEnd\"\n @dragstart=\"onNodeDragStart\"\n @dragover=\"onNodeDragOver\"\n @dragenter=\"onNodeDragEnter\"\n @dragleave=\"onNodeDragLeave\"\n @dragend=\"onNodeDragEnd\"\n @drop=\"onNodeDrop\"\n v-bind=\"getPTOptions('nodeContent')\"\n :data-p-selected=\"checkboxMode ? checked : selected\"\n :data-p-selectable=\"selectable\"\n >\n <button v-ripple type=\"button\" :class=\"cx('nodeToggleButton')\" @click=\"toggle\" tabindex=\"-1\" :data-p-leaf=\"leaf\" v-bind=\"getPTOptions('nodeToggleButton')\">\n <template v-if=\"node.loading && loadingMode === 'icon'\">\n <!-- TODO: nodetogglericon deprecated since v4.0-->\n <component v-if=\"templates['nodetoggleicon'] || templates['nodetogglericon']\" :is=\"templates['nodetoggleicon'] || templates['nodetogglericon']\" :node=\"node\" :expanded=\"expanded\" :class=\"cx('nodeToggleIcon')\" />\n <SpinnerIcon v-else spin :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n </template>\n <template v-else>\n <!-- TODO: togglericon deprecated since v4.0-->\n <component v-if=\"templates['nodetoggleicon'] || templates['togglericon']\" :is=\"templates['nodetoggleicon'] || templates['togglericon']\" :node=\"node\" :expanded=\"expanded\" :class=\"cx('nodeToggleIcon')\" />\n <component v-else-if=\"expanded\" :is=\"node.expandedIcon ? 'span' : 'ChevronDownIcon'\" :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n <component v-else :is=\"node.collapsedIcon ? 'span' : 'ChevronRightIcon'\" :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n </template>\n </button>\n <Checkbox\n v-if=\"checkboxMode\"\n :defaultValue=\"checked\"\n :binary=\"true\"\n :indeterminate=\"partialChecked\"\n :class=\"cx('nodeCheckbox')\"\n :tabindex=\"-1\"\n :unstyled=\"unstyled\"\n :pt=\"getPTOptions('pcNodeCheckbox')\"\n :data-p-partialchecked=\"partialChecked\"\n >\n <template #icon=\"slotProps\">\n <component v-if=\"templates['checkboxicon']\" :is=\"templates['checkboxicon']\" :checked=\"slotProps.checked\" :partialChecked=\"partialChecked\" :class=\"slotProps.class\" />\n </template>\n </Checkbox>\n <component v-if=\"templates['nodeicon']\" :is=\"templates['nodeicon']\" :node=\"node\" :class=\"[cx('nodeIcon')]\" v-bind=\"getPTOptions('nodeIcon')\"></component>\n <span v-else :class=\"[cx('nodeIcon'), node.icon]\" v-bind=\"getPTOptions('nodeIcon')\"></span>\n <span :class=\"cx('nodeLabel')\" v-bind=\"getPTOptions('nodeLabel')\" @keydown.stop>\n <component v-if=\"templates[node.type] || templates['default']\" :is=\"templates[node.type] || templates['default']\" :node=\"node\" :expanded=\"expanded\" :selected=\"checkboxMode ? checked : selected\" />\n <template v-else>{{ label(node) }}</template>\n </span>\n </div>\n <div v-if=\"isNextDropPointActive\" :class=\"cx('dropPoint')\" aria-hidden=\"true\" />\n <ul v-if=\"hasChildren && expanded\" :class=\"cx('nodeChildren')\" role=\"group\" v-bind=\"ptm('nodeChildren')\">\n <TreeNode\n v-for=\"(childNode, index) of node.children\"\n :key=\"childNode.key\"\n :node=\"childNode\"\n :parentNode=\"node\"\n :rootNodes=\"rootNodes\"\n :templates=\"templates\"\n :level=\"level + 1\"\n :index=\"index\"\n :loadingMode=\"loadingMode\"\n :expandedKeys=\"expandedKeys\"\n @node-toggle=\"onChildNodeToggle\"\n @node-click=\"onChildNodeClick\"\n :selectionMode=\"selectionMode\"\n :selectionKeys=\"selectionKeys\"\n @checkbox-change=\"propagateUp\"\n :draggableScope=\"draggableScope\"\n :draggableNodes=\"draggableNodes\"\n :droppableNodes=\"droppableNodes\"\n :validateDrop=\"validateDrop\"\n @node-drop=\"$emit('node-drop', $event)\"\n @node-dragenter=\"$emit('node-dragenter', $event)\"\n @node-dragleave=\"$emit('node-dragleave', $event)\"\n @value-change=\"$emit('value-change', $event)\"\n :unstyled=\"unstyled\"\n :pt=\"pt\"\n />\n </ul>\n </li>\n</template>\n\n<script>\nimport { find, findSingle, getAttribute, getOuterHeight, getOuterWidth } from '@primeuix/utils';\nimport BaseComponent from '@primevue/core/basecomponent';\nimport CheckIcon from '@primevue/icons/check';\nimport ChevronDownIcon from '@primevue/icons/chevrondown';\nimport ChevronRightIcon from '@primevue/icons/chevronright';\nimport MinusIcon from '@primevue/icons/minus';\nimport SpinnerIcon from '@primevue/icons/spinner';\nimport Checkbox from 'primevue/checkbox';\nimport Ripple from 'primevue/ripple';\n\nexport default {\n name: 'TreeNode',\n hostName: 'Tree',\n extends: BaseComponent,\n emits: ['node-toggle', 'node-click', 'checkbox-change', 'node-drop', 'value-change', 'node-dragenter', 'node-dragleave'],\n props: {\n node: {\n type: null,\n default: null\n },\n parentNode: {\n type: null,\n default: null\n },\n rootNodes: {\n type: Array,\n default: null\n },\n expandedKeys: {\n type: null,\n default: null\n },\n loadingMode: {\n type: String,\n default: 'mask'\n },\n selectionKeys: {\n type: null,\n default: null\n },\n selectionMode: {\n type: String,\n default: null\n },\n templates: {\n type: null,\n default: null\n },\n level: {\n type: Number,\n default: null\n },\n draggableScope: {\n type: [String, Array],\n default: null\n },\n draggableNodes: {\n type: Boolean,\n default: null\n },\n droppableNodes: {\n type: Boolean,\n default: null\n },\n validateDrop: {\n type: Boolean,\n default: false\n },\n index: null\n },\n nodeTouched: false,\n toggleClicked: false,\n inject: {\n $pcTree: {\n default: undefined\n }\n },\n data() {\n return {\n isPrevDropPointHovered: false,\n isNextDropPointHovered: false,\n isNodeDropHovered: false\n };\n },\n mounted() {\n this.setAllNodesTabIndexes();\n },\n methods: {\n toggle() {\n this.$emit('node-toggle', this.node);\n this.toggleClicked = true;\n },\n label(node) {\n return typeof node.label === 'function' ? node.label() : node.label;\n },\n onChildNodeToggle(node) {\n this.$emit('node-toggle', node);\n },\n getPTOptions(key) {\n return this.ptm(key, {\n context: {\n node: this.node,\n index: this.index,\n expanded: this.expanded,\n selected: this.selected,\n checked: this.checked,\n partialChecked: this.partialChecked,\n leaf: this.leaf\n }\n });\n },\n onClick(event) {\n if (this.toggleClicked || getAttribute(event.target, '[data-pc-section=\"nodetogglebutton\"]') || getAttribute(event.target.parentElement, '[data-pc-section=\"nodetogglebutton\"]')) {\n this.toggleClicked = false;\n\n return;\n }\n\n if (this.isCheckboxSelectionMode()) {\n if (this.node.selectable != false) {\n this.toggleCheckbox();\n }\n } else {\n this.$emit('node-click', {\n originalEvent: event,\n nodeTouched: this.nodeTouched,\n node: this.node\n });\n }\n\n this.nodeTouched = false;\n },\n onChildNodeClick(event) {\n this.$emit('node-click', event);\n },\n onTouchEnd() {\n this.nodeTouched = true;\n },\n onKeyDown(event) {\n if (!this.isSameNode(event)) return;\n\n switch (event.code) {\n case 'Tab':\n this.onTabKey(event);\n\n break;\n\n case 'ArrowDown':\n this.onArrowDown(event);\n\n break;\n\n case 'ArrowUp':\n this.onArrowUp(event);\n\n break;\n\n case 'ArrowRight':\n this.onArrowRight(event);\n\n break;\n\n case 'ArrowLeft':\n this.onArrowLeft(event);\n\n break;\n\n case 'Enter':\n case 'NumpadEnter':\n case 'Space':\n this.onEnterKey(event);\n\n break;\n\n default:\n break;\n }\n },\n onArrowDown(event) {\n const nodeElement = event.target.getAttribute('data-pc-section') === 'nodetogglebutton' ? event.target.closest('[role=\"treeitem\"]') : event.target;\n const listElement = nodeElement.children[1];\n\n if (listElement) {\n this.focusRowChange(nodeElement, listElement.children[0]);\n } else {\n if (nodeElement.nextElementSibling) {\n this.focusRowChange(nodeElement, nodeElement.nextElementSibling);\n } else {\n let nextSiblingAncestor = this.findNextSiblingOfAncestor(nodeElement);\n\n if (nextSiblingAncestor) {\n this.focusRowChange(nodeElement, nextSiblingAncestor);\n }\n }\n }\n\n event.preventDefault();\n },\n onArrowUp(event) {\n const nodeElement = event.target;\n\n if (nodeElement.previousElementSibling) {\n this.focusRowChange(nodeElement, nodeElement.previousElementSibling, this.findLastVisibleDescendant(nodeElement.previousElementSibling));\n } else {\n let parentNodeElement = this.getParentNodeElement(nodeElement);\n\n if (parentNodeElement) {\n this.focusRowChange(nodeElement, parentNodeElement);\n }\n }\n\n event.preventDefault();\n },\n onArrowRight(event) {\n if (this.leaf || this.expanded) return;\n\n event.currentTarget.tabIndex = -1;\n\n this.$emit('node-toggle', this.node);\n this.$nextTick(() => {\n this.onArrowDown(event);\n });\n },\n onArrowLeft(event) {\n const togglerElement = findSingle(event.currentTarget, '[data-pc-section=\"nodetogglebutton\"]');\n\n if (this.level === 0 && !this.expanded) {\n return false;\n }\n\n if (this.expanded && !this.leaf) {\n togglerElement.click();\n\n return false;\n }\n\n const target = this.findBeforeClickableNode(event.currentTarget);\n\n if (target) {\n this.focusRowChange(event.currentTarget, target);\n }\n },\n onEnterKey(event) {\n this.setTabIndexForSelectionMode(event, this.nodeTouched);\n this.onClick(event);\n\n event.preventDefault();\n },\n onTabKey() {\n this.setAllNodesTabIndexes();\n },\n removeNodeFromTree(nodes, nodeToRemove) {\n return nodes.reduce((acc, node) => {\n if (node.key === nodeToRemove.key) {\n return acc;\n }\n if (node.children && node.children.length > 0) {\n const updatedChildren = this.removeNodeFromTree(node.children, nodeToRemove);\n acc.push({ ...node, children: updatedChildren });\n } else {\n acc.push(node);\n }\n\n return acc;\n }, []);\n },\n insertNodeInSiblings(nodes, targetKey, nodeToInsert, offset) {\n const targetIndex = nodes.findIndex((n) => n.key === targetKey);\n\n if (targetIndex !== -1) {\n return nodes.toSpliced(targetIndex + offset, 0, nodeToInsert);\n }\n\n return nodes.map((node) => {\n if (node.children && node.children.length > 0) {\n return { ...node, children: this.insertNodeInSiblings(node.children, targetKey, nodeToInsert, offset) };\n }\n\n return node;\n });\n },\n addNodeAsChild(nodes, parentKey, nodeToInsert) {\n return nodes.map((node) => {\n if (node.key === parentKey) {\n return { ...node, children: [...(node.children || []), nodeToInsert] };\n }\n\n if (node.children && node.children.length > 0) {\n return { ...node, children: this.addNodeAsChild(node.children, parentKey, nodeToInsert) };\n }\n\n return node;\n });\n },\n insertNodeOnDrop() {\n const { dragNode, dragNodeIndex, dragNodeSubNodes, dragDropService } = this.$pcTree;\n\n if (!this.node || dragNodeIndex == null || !dragNode || !dragNodeSubNodes) {\n return null;\n }\n\n const position = this.dropPosition;\n let updatedNodes = this.removeNodeFromTree(this.rootNodes, dragNode);\n\n if (position < 0) {\n // insert before a Node\n updatedNodes = this.insertNodeInSiblings(updatedNodes, this.node.key, dragNode, 0);\n } else if (position > 0) {\n // insert after a Node\n updatedNodes = this.insertNodeInSiblings(updatedNodes, this.node.key, dragNode, 1);\n } else {\n // insert as child of a Node\n updatedNodes = this.addNodeAsChild(updatedNodes, this.node.key, dragNode);\n }\n\n this.$emit('value-change', { nodes: updatedNodes });\n\n dragDropService.stopDrag({\n node: dragNode,\n subNodes: updatedNodes,\n index: dragNodeIndex\n });\n\n return updatedNodes;\n },\n onNodeDrop(event) {\n if (this.isDroppable) {\n event.preventDefault();\n event.stopPropagation();\n\n const { dragNode } = this.$pcTree;\n const position = this.dropPosition;\n const isValidDrop = position !== 0 || (position === 0 && this.isNodeDroppable);\n\n if (isValidDrop) {\n if (this.validateDrop) {\n this.$emit('node-drop', {\n originalEvent: event,\n value: this.rootNodes,\n dragNode: dragNode,\n dropNode: this.node,\n index: this.index,\n accept: () => {\n const updatedNodes = this.insertNodeOnDrop();\n\n this.$emit('node-drop', {\n originalEvent: event,\n value: updatedNodes,\n dragNode: dragNode,\n dropNode: this.node,\n index: this.index\n });\n }\n });\n } else {\n const updatedNodes = this.insertNodeOnDrop();\n\n this.$emit('node-drop', {\n originalEvent: event,\n value: updatedNodes,\n dragNode: dragNode,\n dropNode: this.node,\n index: this.index\n });\n }\n }\n\n this.isPrevDropPointHovered = false;\n this.isNextDropPointHovered = false;\n this.isNodeDropHovered = false;\n }\n },\n onNodeDragStart(event) {\n if (this.isNodeDraggable) {\n event.dataTransfer.effectAllowed = 'all';\n event.dataTransfer.setData('text', 'data');\n\n const target = event.currentTarget;\n const dragEl = target.cloneNode(true);\n const toggler = dragEl.querySelector('[data-pc-section=\"nodetogglebutton\"]');\n const checkbox = dragEl.querySelector('[data-pc-name=\"pcnodecheckbox\"]');\n\n target.setAttribute('data-p-dragging', 'true');\n dragEl.style.width = getOuterWidth(target) + 'px';\n dragEl.style.height = getOuterHeight(target) + 'px';\n dragEl.setAttribute('data-pc-section', 'drag-image');\n toggler.style.visibility = 'hidden';\n checkbox?.remove();\n document.body.appendChild(dragEl);\n event.dataTransfer.setDragImage(dragEl, 0, 0);\n\n setTimeout(() => document.body.removeChild(dragEl), 0);\n\n this.$pcTree.dragDropService.startDrag({\n node: this.node,\n subNodes: this.subNodes,\n index: this.index,\n scope: this.draggableScope\n });\n } else {\n event.preventDefault();\n }\n },\n onNodeDragOver(event) {\n if (this.isDroppable) {\n event.dataTransfer.dropEffect = 'copy';\n const nodeElement = event.currentTarget;\n const rect = nodeElement.getBoundingClientRect();\n const y = event.clientY - rect.top;\n\n this.isPrevDropPointHovered = false;\n this.isNextDropPointHovered = false;\n this.isNodeDropHovered = false;\n\n if (y < rect.height * 0.25) {\n this.isPrevDropPointHovered = true;\n } else if (y > rect.height * 0.75) {\n this.isNextDropPointHovered = true;\n } else if (this.isNodeDroppable) {\n this.isNodeDropHovered = true;\n }\n } else {\n event.dataTransfer.dropEffect = 'none';\n }\n\n if (this.droppableNodes) {\n event.preventDefault();\n event.stopPropagation();\n }\n },\n onNodeDragEnter() {\n this.$emit('node-dragenter', {\n node: this.node\n });\n },\n onNodeDragLeave() {\n this.$emit('node-dragleave', {\n node: this.node\n });\n\n this.isPrevDropPointHovered = false;\n this.isNextDropPointHovered = false;\n this.isNodeDropHovered = false;\n },\n onNodeDragEnd(event) {\n event.currentTarget?.removeAttribute('data-p-dragging');\n\n this.$pcTree.dragDropService.stopDrag({\n node: this.node,\n subNodes: this.subNodes,\n index: this.index\n });\n },\n setAllNodesTabIndexes() {\n const nodes = find(this.$refs.currentNode.closest('[data-pc-section=\"rootchildren\"]'), '[role=\"treeitem\"]');\n\n const hasSelectedNode = [...nodes].some((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true');\n\n [...nodes].forEach((node) => {\n node.tabIndex = -1;\n });\n\n if (hasSelectedNode) {\n const selectedNodes = [...nodes].filter((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true');\n\n selectedNodes[0].tabIndex = 0;\n\n return;\n }\n\n [...nodes][0].tabIndex = 0;\n },\n setTabIndexForSelectionMode(event, nodeTouched) {\n if (this.selectionMode !== null) {\n const elements = [...find(this.$refs.currentNode.parentElement, '[role=\"treeitem\"]')];\n\n event.currentTarget.tabIndex = nodeTouched === false ? -1 : 0;\n\n if (elements.every((element) => element.tabIndex === -1)) {\n elements[0].tabIndex = 0;\n }\n }\n },\n focusRowChange(firstFocusableRow, currentFocusedRow, lastVisibleDescendant) {\n firstFocusableRow.tabIndex = '-1';\n currentFocusedRow.tabIndex = '0';\n\n this.focusNode(lastVisibleDescendant || currentFocusedRow);\n },\n findBeforeClickableNode(node) {\n const parentListElement = node.closest('ul').closest('li');\n\n if (parentListElement) {\n const prevNodeButton = findSingle(parentListElement, 'button');\n\n if (prevNodeButton && prevNodeButton.style.visibility !== 'hidden') {\n return parentListElement;\n }\n\n return this.findBeforeClickableNode(node.previousElementSibling);\n }\n\n return null;\n },\n toggleCheckbox() {\n let _selectionKeys = this.selectionKeys ? { ...this.selectionKeys } : {};\n const _check = !this.checked;\n\n this.propagateDown(this.node, _check, _selectionKeys);\n\n this.$emit('checkbox-change', {\n node: this.node,\n check: _check,\n selectionKeys: _selectionKeys\n });\n },\n propagateDown(node, check, selectionKeys) {\n if (check && node.selectable != false) selectionKeys[node.key] = { checked: true, partialChecked: false };\n else delete selectionKeys[node.key];\n\n if (node.children && node.children.length) {\n for (let child of node.children) {\n this.propagateDown(child, check, selectionKeys);\n }\n }\n },\n propagateUp(event) {\n let check = event.check;\n let _selectionKeys = { ...event.selectionKeys };\n let checkedChildCount = 0;\n let childPartialSelected = false;\n\n for (let child of this.node.children) {\n if (_selectionKeys[child.key] && _selectionKeys[child.key].checked) checkedChildCount++;\n else if (_selectionKeys[child.key] && _selectionKeys[child.key].partialChecked) childPartialSelected = true;\n }\n\n if (check && checkedChildCount === this.node.children.length) {\n _selectionKeys[this.node.key] = { checked: true, partialChecked: false };\n } else {\n if (!check) {\n delete _selectionKeys[this.node.key];\n }\n\n if (childPartialSelected || (checkedChildCount > 0 && checkedChildCount !== this.node.children.length)) _selectionKeys[this.node.key] = { checked: false, partialChecked: true };\n else delete _selectionKeys[this.node.key];\n }\n\n this.$emit('checkbox-change', {\n node: event.node,\n check: event.check,\n selectionKeys: _selectionKeys\n });\n },\n onChildCheckboxChange(event) {\n this.$emit('checkbox-change', event);\n },\n findNextSiblingOfAncestor(nodeElement) {\n let parentNodeElement = this.getParentNodeElement(nodeElement);\n\n if (parentNodeElement) {\n if (parentNodeElement.nextElementSibling) return parentNodeElement.nextElementSibling;\n else return this.findNextSiblingOfAncestor(parentNodeElement);\n } else {\n return null;\n }\n },\n findLastVisibleDescendant(nodeElement) {\n const childrenListElement = nodeElement.children[1];\n\n if (childrenListElement) {\n const lastChildElement = childrenListElement.children[childrenListElement.children.length - 1];\n\n return this.findLastVisibleDescendant(lastChildElement);\n } else {\n return nodeElement;\n }\n },\n getParentNodeElement(nodeElement) {\n const parentNodeElement = nodeElement.parentElement.parentElement;\n\n return getAttribute(parentNodeElement, 'role') === 'treeitem' ? parentNodeElement : null;\n },\n focusNode(element) {\n element.focus();\n },\n isCheckboxSelectionMode() {\n return this.selectionMode === 'checkbox';\n },\n isSameNode(event) {\n return event.currentTarget && (event.currentTarget.isSameNode(event.target) || event.currentTarget.isSameNode(event.target.closest('[role=\"treeitem\"]')));\n }\n },\n computed: {\n hasChildren() {\n return this.node.children && this.node.children.length > 0;\n },\n expanded() {\n return this.expandedKeys && this.expandedKeys[this.node.key] === true;\n },\n leaf() {\n return this.node.leaf === false ? false : !(this.node.children && this.node.children.length);\n },\n selectable() {\n return this.node.selectable === false ? false : this.selectionMode != null;\n },\n selected() {\n return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.node.key] === true : false;\n },\n checkboxMode() {\n return this.selectionMode === 'checkbox' && this.node.selectable !== false;\n },\n checked() {\n return this.selectionKeys ? this.selectionKeys[this.node.key] && this.selectionKeys[this.node.key].checked : false;\n },\n partialChecked() {\n return this.selectionKeys ? this.selectionKeys[this.node.key] && this.selectionKeys[this.node.key].partialChecked : false;\n },\n ariaChecked() {\n return this.selectionMode === 'single' || this.selectionMode === 'multiple' ? this.selected : undefined;\n },\n ariaSelected() {\n return this.checkboxMode ? this.checked : undefined;\n },\n isPrevDropPointActive() {\n return this.isPrevDropPointHovered && this.isDroppable;\n },\n isNextDropPointActive() {\n return this.isNextDropPointHovered && this.isDroppable;\n },\n dropPosition() {\n return this.isPrevDropPointActive ? -1 : this.isNextDropPointActive ? 1 : 0;\n },\n subNodes() {\n return this.parentNode ? this.parentNode.children : this.rootNodes;\n },\n isDraggable() {\n return this.draggableNodes;\n },\n isDroppable() {\n return this.droppableNodes && this.$pcTree.allowNodeDrop(this.node);\n },\n isNodeDraggable() {\n return this.node?.draggable !== false && this.isDraggable;\n },\n isNodeDroppable() {\n return this.node?.droppable !== false && this.isDroppable;\n },\n isNodeDropActive() {\n return this.isNodeDropHovered && this.isNodeDroppable;\n }\n },\n components: {\n Checkbox,\n ChevronDownIcon,\n ChevronRightIcon,\n CheckIcon,\n MinusIcon,\n SpinnerIcon\n },\n directives: {\n ripple: Ripple\n }\n};\n</script>\n","<template>\n <li\n ref=\"currentNode\"\n :class=\"cx('node')\"\n role=\"treeitem\"\n :aria-label=\"label(node)\"\n :aria-selected=\"ariaSelected\"\n :aria-expanded=\"expanded\"\n :aria-setsize=\"node.children ? node.children.length : 0\"\n :aria-posinset=\"index + 1\"\n :aria-level=\"level\"\n :aria-checked=\"ariaChecked\"\n :tabindex=\"index === 0 ? 0 : -1\"\n @keydown=\"onKeyDown\"\n v-bind=\"getPTOptions('node')\"\n >\n <div v-if=\"isPrevDropPointActive\" :class=\"cx('dropPoint')\" aria-hidden=\"true\" />\n <div\n :class=\"cx('nodeContent')\"\n :style=\"node.style\"\n :draggable=\"isDraggable\"\n @click=\"onClick\"\n @touchend=\"onTouchEnd\"\n @dragstart=\"onNodeDragStart\"\n @dragover=\"onNodeDragOver\"\n @dragenter=\"onNodeDragEnter\"\n @dragleave=\"onNodeDragLeave\"\n @dragend=\"onNodeDragEnd\"\n @drop=\"onNodeDrop\"\n v-bind=\"getPTOptions('nodeContent')\"\n :data-p-selected=\"checkboxMode ? checked : selected\"\n :data-p-selectable=\"selectable\"\n >\n <button v-ripple type=\"button\" :class=\"cx('nodeToggleButton')\" @click=\"toggle\" tabindex=\"-1\" :data-p-leaf=\"leaf\" v-bind=\"getPTOptions('nodeToggleButton')\">\n <template v-if=\"node.loading && loadingMode === 'icon'\">\n <!-- TODO: nodetogglericon deprecated since v4.0-->\n <component v-if=\"templates['nodetoggleicon'] || templates['nodetogglericon']\" :is=\"templates['nodetoggleicon'] || templates['nodetogglericon']\" :node=\"node\" :expanded=\"expanded\" :class=\"cx('nodeToggleIcon')\" />\n <SpinnerIcon v-else spin :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n </template>\n <template v-else>\n <!-- TODO: togglericon deprecated since v4.0-->\n <component v-if=\"templates['nodetoggleicon'] || templates['togglericon']\" :is=\"templates['nodetoggleicon'] || templates['togglericon']\" :node=\"node\" :expanded=\"expanded\" :class=\"cx('nodeToggleIcon')\" />\n <component v-else-if=\"expanded\" :is=\"node.expandedIcon ? 'span' : 'ChevronDownIcon'\" :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n <component v-else :is=\"node.collapsedIcon ? 'span' : 'ChevronRightIcon'\" :class=\"cx('nodeToggleIcon')\" v-bind=\"getPTOptions('nodeToggleIcon')\" />\n </template>\n </button>\n <Checkbox\n v-if=\"checkboxMode\"\n :defaultValue=\"checked\"\n :binary=\"true\"\n :indeterminate=\"partialChecked\"\n :class=\"cx('nodeCheckbox')\"\n :tabindex=\"-1\"\n :unstyled=\"unstyled\"\n :pt=\"getPTOptions('pcNodeCheckbox')\"\n :data-p-partialchecked=\"partialChecked\"\n >\n <template #icon=\"slotProps\">\n <component v-if=\"templates['checkboxicon']\" :is=\"templates['checkboxicon']\" :checked=\"slotProps.checked\" :partialChecked=\"partialChecked\" :class=\"slotProps.class\" />\n </template>\n </Checkbox>\n <component v-if=\"templates['nodeicon']\" :is=\"templates['nodeicon']\" :node=\"node\" :class=\"[cx('nodeIcon')]\" v-bind=\"getPTOptions('nodeIcon')\"></component>\n <span v-else :class=\"[cx('nodeIcon'), node.icon]\" v-bind=\"getPTOptions('nodeIcon')\"></span>\n <span :class=\"cx('nodeLabel')\" v-bind=\"getPTOptions('nodeLabel')\" @keydown.stop>\n <component v-if=\"templates[node.type] || templates['default']\" :is=\"templates[node.type] || templates['default']\" :node=\"node\" :expanded=\"expanded\" :selected=\"checkboxMode ? checked : selected\" />\n <template v-else>{{ label(node) }}</template>\n </span>\n </div>\n <div v-if=\"isNextDropPointActive\" :class=\"cx('dropPoint')\" aria-hidden=\"true\" />\n <ul v-if=\"hasChildren && expanded\" :class=\"cx('nodeChildren')\" role=\"group\" v-bind=\"ptm('nodeChildren')\">\n <TreeNode\n v-for=\"(childNode, index) of node.children\"\n :key=\"childNode.key\"\n :node=\"childNode\"\n :parentNode=\"node\"\n :rootNodes=\"rootNodes\"\n :templates=\"templates\"\n :level=\"level + 1\"\n :index=\"index\"\n :loadingMode=\"loadingMode\"\n :expandedKeys=\"expandedKeys\"\n @node-toggle=\"onChildNodeToggle\"\n @node-click=\"onChildNodeClick\"\n :selectionMode=\"selectionMode\"\n :selectionKeys=\"selectionKeys\"\n @checkbox-change=\"propagateUp\"\n :draggableScope=\"draggableScope\"\n :draggableNodes=\"draggableNodes\"\n :droppableNodes=\"droppableNodes\"\n :validateDrop=\"validateDrop\"\n @node-drop=\"$emit('node-drop', $event)\"\n @node-dragenter=\"$emit('node-dragenter', $event)\"\n @node-dragleave=\"$emit('node-dragleave', $event)\"\n @value-change=\"$emit('value-change', $event)\"\n :unstyled=\"unstyled\"\n :pt=\"pt\"\n />\n </ul>\n </li>\n</template>\n\n<script>\nimport { find, findSingle, getAttribute, getOuterHeight, getOuterWidth } from '@primeuix/utils';\nimport BaseComponent from '@primevue/core/basecomponent';\nimport CheckIcon from '@primevue/icons/check';\nimport ChevronDownIcon from '@primevue/icons/chevrondown';\nimport ChevronRightIcon from '@primevue/icons/chevronright';\nimport MinusIcon from '@primevue/icons/minus';\nimport SpinnerIcon from '@primevue/icons/spinner';\nimport Checkbox from 'primevue/checkbox';\nimport Ripple from 'primevue/ripple';\n\nexport default {\n name: 'TreeNode',\n hostName: 'Tree',\n extends: BaseComponent,\n emits: ['node-toggle', 'node-click', 'checkbox-change', 'node-drop', 'value-change', 'node-dragenter', 'node-dragleave'],\n props: {\n node: {\n type: null,\n default: null\n },\n parentNode: {\n type: null,\n default: null\n },\n rootNodes: {\n type: Array,\n default: null\n },\n expandedKeys: {\n type: null,\n default: null\n },\n loadingMode: {\n type: String,\n default: 'mask'\n },\n selectionKeys: {\n type: null,\n default: null\n },\n selectionMode: {\n type: String,\n default: null\n },\n templates: {\n type: null,\n default: null\n },\n level: {\n type: Number,\n default: null\n },\n draggableScope: {\n type: [String, Array],\n default: null\n },\n draggableNodes: {\n type: Boolean,\n default: null\n },\n droppableNodes: {\n type: Boolean,\n default: null\n },\n validateDrop: {\n type: Boolean,\n default: false\n },\n index: null\n },\n nodeTouched: false,\n toggleClicked: false,\n inject: {\n $pcTree: {\n default: undefined\n }\n },\n data() {\n return {\n isPrevDropPointHovered: false,\n isNextDropPointHovered: false,\n isNodeDropHovered: false\n };\n },\n mounted() {\n this.setAllNodesTabIndexes();\n },\n methods: {\n toggle() {\n this.$emit('node-toggle', this.node);\n this.toggleClicked = true;\n },\n label(node) {\n return typeof node.label === 'function' ? node.label() : node.label;\n },\n onChildNodeToggle(node) {\n this.$emit('node-toggle', node);\n },\n getPTOptions(key) {\n return this.ptm(key, {\n context: {\n node: this.node,\n index: this.index,\n expanded: this.expanded,\n selected: this.selected,\n checked: this.checked,\n partialChecked: this.partialChecked,\n leaf: this.leaf\n }\n });\n },\n onClick(event) {\n if (this.toggleClicked || getAttribute(event.target, '[data-pc-section=\"nodetogglebutton\"]') || getAttribute(event.target.parentElement, '[data-pc-section=\"nodetogglebutton\"]')) {\n this.toggleClicked = false;\n\n return;\n }\n\n if (this.isCheckboxSelectionMode()) {\n if (this.node.selectable != false) {\n this.toggleCheckbox();\n }\n } else {\n this.$emit('node-click', {\n originalEvent: event,\n nodeTouched: this.nodeTouched,\n node: this.node\n });\n }\n\n this.nodeTouched = false;\n },\n onChildNodeClick(event) {\n this.$emit('node-click', event);\n },\n onTouchEnd() {\n this.nodeTouched = true;\n },\n onKeyDown(event) {\n if (!this.isSameNode(event)) return;\n\n switch (event.code) {\n case 'Tab':\n this.onTabKey(event);\n\n break;\n\n case 'ArrowDown':\n this.onArrowDown(event);\n\n break;\n\n case 'ArrowUp':\n this.onArrowUp(event);\n\n break;\n\n case 'ArrowRight':\n this.onArrowRight(event);\n\n break;\n\n case 'ArrowLeft':\n this.onArrowLeft(event);\n\n break;\n\n case 'Enter':\n case 'NumpadEnter':\n case 'Space':\n this.onEnterKey(event);\n\n break;\n\n default:\n break;\n }\n },\n onArrowDown(event) {\n const nodeElement = event.target.getAttribute('data-pc-section') === 'nodetogglebutton' ? event.target.closest('[role=\"treeitem\"]') : event.target;\n const listElement = nodeElement.children[1];\n\n if (listElement) {\n this.focusRowChange(nodeElement, listElement.children[0]);\n } else {\n if (nodeElement.nextElementSibling) {\n this.focusRowChange(nodeElement, nodeElement.nextElementSibling);\n } else {\n let nextSiblingAncestor = this.findNextSiblingOfAncestor(nodeElement);\n\n if (nextSiblingAncestor) {\n this.focusRowChange(nodeElement, nextSiblingAncestor);\n }\n }\n }\n\n event.preventDefault();\n },\n onArrowUp(event) {\n const nodeElement = event.target;\n\n if (nodeElement.previousElementSibling) {\n this.focusRowChange(nodeElement, nodeElement.previousElementSibling, this.findLastVisibleDescendant(nodeElement.previousElementSibling));\n } else {\n let parentNodeElement = this.getParentNodeElement(nodeElement);\n\n if (parentNodeElement) {\n this.focusRowChange(nodeElement, parentNodeElement);\n }\n }\n\n event.preventDefault();\n },\n onArrowRight(event) {\n if (this.leaf || this.expanded) return;\n\n event.currentTarget.tabIndex = -1;\n\n this.$emit('node-toggle', this.node);\n this.$nextTick(() => {\n this.onArrowDown(event);\n });\n },\n onArrowLeft(event) {\n const togglerElement = findSingle(event.currentTarget, '[data-pc-section=\"nodetogglebutton\"]');\n\n if (this.level === 0 && !this.expanded) {\n return false;\n }\n\n if (this.expanded && !this.leaf) {\n togglerElement.click();\n\n return false;\n }\n\n const target = this.findBeforeClickableNode(event.currentTarget);\n\n if (target) {\n this.focusRowChange(event.currentTarget, target);\n }\n },\n onEnterKey(event) {\n this.setTabIndexForSelectionMode(event, this.nodeTouched);\n this.onClick(event);\n\n event.preventDefault();\n },\n onTabKey() {\n this.setAllNodesTabIndexes();\n },\n removeNodeFromTree(nodes, nodeToRemove) {\n return nodes.reduce((acc, node) => {\n if (node.key === nodeToRemove.key) {\n return acc;\n }\n if (node.children && node.children.length > 0) {\n const updatedChildren = this.removeNodeFromTree(node.children, nodeToRemove);\n acc.push({ ...node, children: updatedChildren });\n } else {\n acc.push(node);\n }\n\n return acc;\n }, []);\n },\n insertNodeInSiblings(nodes, targetKey, nodeToInsert, offset) {\n const targetIndex = nodes.findIndex((n) => n.key === targetKey);\n\n if (targetIndex !== -1) {\n return nodes.toSpliced(targetIndex + offset, 0, nodeToInsert);\n }\n\n return nodes.map((node) => {\n if (node.children && node.children.length > 0) {\n return { ...node, children: this.insertNodeInSiblings(node.children, targetKey, nodeToInsert, offset) };\n }\n\n return node;\n });\n },\n addNodeAsChild(nodes, parentKey, nodeToInsert) {\n return nodes.map((node) => {\n if (node.key === parentKey) {\n return { ...node, children: [...(node.children || []), nodeToInsert] };\n }\n\n if (node.children && node.children.length > 0) {\n return { ...node, children: this.addNodeAsChild(node.children, parentKey, nodeToInsert) };\n }\n\n return node;\n });\n },\n insertNodeOnDrop() {\n const { dragNode, dragNodeIndex, dragNodeSubNodes, dragDropService } = this.$pcTree;\n\n if (!this.node || dragNodeIndex == null || !dragNode || !dragNodeSubNodes) {\n return null;\n }\n\n const position = this.dropPosition;\n let updatedNodes = this.removeNodeFromTree(this.rootNodes, dragNode);\n\n if (position < 0) {\n // insert before a Node\n updatedNodes = this.insertNodeInSiblings(updatedNodes, this.node.key, dragNode, 0);\n } else if (position > 0) {\n // insert after a Node\n updatedNodes = this.insertNodeInSiblings(updatedNodes, this.node.key, dragNode, 1);\n } else {\n // insert as child of a Node\n updatedNodes = this.addNodeAsChild(updatedNodes, this.node.key, dragNode);\n }\n\n this.$emit('value-change', { nodes: updatedNodes });\n\n dragDropService.stopDrag({\n node: dragNode,\n subNodes: updatedNodes,\n index: dragNodeIndex\n });\n\n return updatedNodes;\n },\n onNodeDrop(event) {\n if (t