primevue-mirror/components/lib/tree/TreeNode.vue

459 lines
17 KiB
Vue
Raw Normal View History

2022-09-06 12:03:37 +00:00
<template>
2022-12-08 11:04:25 +00:00
<li
ref="currentNode"
2023-05-31 13:11:41 +00:00
:class="cx('node')"
2022-12-08 11:04:25 +00:00
role="treeitem"
:aria-label="label(node)"
:aria-selected="ariaSelected"
:aria-expanded="expanded"
:aria-setsize="node.children ? node.children.length : 0"
:aria-posinset="index + 1"
:aria-level="level"
:aria-checked="ariaChecked"
:tabindex="index === 0 ? 0 : -1"
@keydown="onKeyDown"
2023-05-26 11:22:08 +00:00
v-bind="level === 1 ? getPTOptions('node') : ptm('subgroup')"
2022-12-08 11:04:25 +00:00
>
2023-05-31 13:11:41 +00:00
<div :class="cx('content')" @click="onClick" @touchend="onTouchEnd" :style="node.style" v-bind="getPTOptions('content')" :data-p-highlight="checkboxMode ? checked : selected" :data-p-selectable="selectable">
2023-05-26 11:22:08 +00:00
<button v-ripple type="button" :class="cx('toggler')" @click="toggle" tabindex="-1" aria-hidden="true" v-bind="getPTOptions('toggler')">
<template v-if="node.loading && loadingMode === 'icon'">
<component v-if="templates['nodetogglericon']" :is="templates['nodetogglericon']" :class="cx('nodetogglericon')" />
<SpinnerIcon v-else spin :class="cx('nodetogglericon')" v-bind="ptm('nodetogglericon')" />
</template>
<template v-else>
<component v-if="templates['togglericon']" :is="templates['togglericon']" :node="node" :expanded="expanded" :class="cx('togglerIcon')" />
<component v-else-if="expanded" :is="node.expandedIcon ? 'span' : 'ChevronDownIcon'" :class="cx('togglerIcon')" v-bind="getPTOptions('togglerIcon')" />
<component v-else :is="node.collapsedIcon ? 'span' : 'ChevronRightIcon'" :class="cx('togglerIcon')" v-bind="getPTOptions('togglerIcon')" />
</template>
2022-09-06 12:03:37 +00:00
</button>
2024-01-18 12:16:58 +00:00
<Checkbox v-if="checkboxMode" :modelValue="checked" :binary="true" :class="cx('nodeCheckbox')" :tabindex="-1" :unstyled="unstyled" :pt="getPTOptions('nodeCheckbox')" :data-p-checked="checked" :data-p-partialchecked="partialChecked">
2024-01-14 13:38:51 +00:00
<template #icon="slotProps">
<component v-if="templates['checkboxicon']" :is="templates['checkboxicon']" :checked="slotProps.checked" :partialChecked="partialChecked" :class="slotProps.class" />
<component v-else :is="checked ? 'CheckIcon' : partialChecked ? 'MinusIcon' : null" :class="slotProps.class" v-bind="getPTOptions('nodeCheckbox.icon')" />
</template>
</Checkbox>
<component v-if="templates['nodeicon']" :is="templates['nodeicon']" :node="node" :class="[cx('nodeIcon')]"></component>
2024-03-13 13:12:26 +00:00
<span v-else :class="[cx('nodeIcon'), node.icon]" v-bind="getPTOptions('nodeIcon')"></span>
<span :class="cx('label')" v-bind="getPTOptions('label')" @keydown.stop>
2022-09-14 11:26:01 +00:00
<component v-if="templates[node.type] || templates['default']" :is="templates[node.type] || templates['default']" :node="node" />
<template v-else>{{ label(node) }}</template>
2022-09-06 12:03:37 +00:00
</span>
</div>
2023-05-26 11:22:08 +00:00
<ul v-if="hasChildren && expanded" :class="cx('subgroup')" role="group" v-bind="ptm('subgroup')">
2022-09-14 11:26:01 +00:00
<TreeNode
v-for="childNode of node.children"
:key="childNode.key"
:node="childNode"
:templates="templates"
:level="level + 1"
:expandedKeys="expandedKeys"
@node-toggle="onChildNodeToggle"
@node-click="onChildNodeClick"
:selectionMode="selectionMode"
:selectionKeys="selectionKeys"
@checkbox-change="propagateUp"
2024-01-14 14:09:03 +00:00
:unstyled="unstyled"
2023-05-07 19:21:37 +00:00
:pt="pt"
2022-09-14 11:26:01 +00:00
/>
2022-09-06 12:03:37 +00:00
</ul>
</li>
</template>
<script>
import BaseComponent from 'primevue/basecomponent';
2024-01-14 13:38:51 +00:00
import Checkbox from 'primevue/checkbox';
import CheckIcon from 'primevue/icons/check';
import ChevronDownIcon from 'primevue/icons/chevrondown';
import ChevronRightIcon from 'primevue/icons/chevronright';
import MinusIcon from 'primevue/icons/minus';
import SpinnerIcon from 'primevue/icons/spinner';
2022-09-06 12:03:37 +00:00
import Ripple from 'primevue/ripple';
2022-12-08 11:04:25 +00:00
import { DomHandler } from 'primevue/utils';
2022-09-06 12:03:37 +00:00
export default {
name: 'TreeNode',
2023-07-04 06:29:36 +00:00
hostName: 'Tree',
extends: BaseComponent,
2022-09-06 12:03:37 +00:00
emits: ['node-toggle', 'node-click', 'checkbox-change'],
props: {
node: {
type: null,
default: null
},
expandedKeys: {
type: null,
default: null
},
loadingMode: {
type: String,
default: 'mask'
},
2022-09-06 12:03:37 +00:00
selectionKeys: {
type: null,
default: null
},
selectionMode: {
type: String,
default: null
},
templates: {
type: null,
default: null
},
level: {
type: Number,
default: null
},
index: null
2022-09-06 12:03:37 +00:00
},
nodeTouched: false,
toggleClicked: false,
2023-05-26 11:56:52 +00:00
mounted() {
this.setAllNodesTabIndexes();
},
2022-09-06 12:03:37 +00:00
methods: {
toggle() {
this.$emit('node-toggle', this.node);
this.toggleClicked = true;
2022-09-06 12:03:37 +00:00
},
label(node) {
2022-09-14 11:26:01 +00:00
return typeof node.label === 'function' ? node.label() : node.label;
2022-09-06 12:03:37 +00:00
},
onChildNodeToggle(node) {
this.$emit('node-toggle', node);
},
2023-05-07 19:21:37 +00:00
getPTOptions(key) {
return this.ptm(key, {
context: {
2023-07-19 12:57:15 +00:00
index: this.index,
2023-05-07 19:21:37 +00:00
expanded: this.expanded,
selected: this.selected,
2023-07-19 12:57:15 +00:00
checked: this.checked,
leaf: this.leaf
2023-05-07 19:21:37 +00:00
}
});
},
2022-09-06 12:03:37 +00:00
onClick(event) {
2023-05-26 11:22:08 +00:00
if (this.toggleClicked || DomHandler.getAttribute(event.target, '[data-pc-section="toggler"]') || DomHandler.getAttribute(event.target.parentElement, '[data-pc-section="toggler"]')) {
this.toggleClicked = false;
2022-09-06 12:03:37 +00:00
return;
}
if (this.isCheckboxSelectionMode()) {
this.toggleCheckbox();
2022-09-14 11:26:01 +00:00
} else {
2022-09-06 12:03:37 +00:00
this.$emit('node-click', {
originalEvent: event,
nodeTouched: this.nodeTouched,
node: this.node
});
}
this.nodeTouched = false;
},
onChildNodeClick(event) {
this.$emit('node-click', event);
},
onTouchEnd() {
this.nodeTouched = true;
},
onKeyDown(event) {
2022-12-08 11:04:25 +00:00
if (!this.isSameNode(event)) return;
2022-09-06 12:03:37 +00:00
switch (event.code) {
2022-12-08 11:04:25 +00:00
case 'Tab':
this.onTabKey(event);
break;
2022-09-06 12:03:37 +00:00
case 'ArrowDown':
2022-12-08 11:04:25 +00:00
this.onArrowDown(event);
2022-09-14 11:26:01 +00:00
break;
2022-09-06 12:03:37 +00:00
case 'ArrowUp':
2022-12-08 11:04:25 +00:00
this.onArrowUp(event);
2022-09-14 11:26:01 +00:00
break;
2022-09-06 12:03:37 +00:00
case 'ArrowRight':
2022-12-08 11:04:25 +00:00
this.onArrowRight(event);
break;
2022-09-06 12:03:37 +00:00
case 'ArrowLeft':
2022-12-08 11:04:25 +00:00
this.onArrowLeft(event);
2022-09-14 11:26:01 +00:00
break;
2022-09-06 12:03:37 +00:00
case 'Enter':
case 'NumpadEnter':
2022-09-06 12:03:37 +00:00
case 'Space':
2022-12-08 11:04:25 +00:00
this.onEnterKey(event);
2022-09-14 11:26:01 +00:00
break;
2022-09-06 12:03:37 +00:00
default:
2022-09-14 11:26:01 +00:00
break;
2022-09-06 12:03:37 +00:00
}
2022-12-08 11:04:25 +00:00
},
onArrowDown(event) {
2023-06-22 06:46:22 +00:00
const nodeElement = event.target.getAttribute('data-pc-section') === 'toggler' ? event.target.closest('[role="treeitem"]') : event.target;
2022-12-08 11:04:25 +00:00
const listElement = nodeElement.children[1];
if (listElement) {
this.focusRowChange(nodeElement, listElement.children[0]);
} else {
if (nodeElement.nextElementSibling) {
this.focusRowChange(nodeElement, nodeElement.nextElementSibling);
} else {
let nextSiblingAncestor = this.findNextSiblingOfAncestor(nodeElement);
if (nextSiblingAncestor) {
this.focusRowChange(nodeElement, nextSiblingAncestor);
}
}
}
2022-09-06 12:03:37 +00:00
event.preventDefault();
},
2022-12-08 11:04:25 +00:00
onArrowUp(event) {
const nodeElement = event.target;
if (nodeElement.previousElementSibling) {
this.focusRowChange(nodeElement, nodeElement.previousElementSibling, this.findLastVisibleDescendant(nodeElement.previousElementSibling));
} else {
let parentNodeElement = this.getParentNodeElement(nodeElement);
if (parentNodeElement) {
this.focusRowChange(nodeElement, parentNodeElement);
}
}
event.preventDefault();
},
onArrowRight(event) {
if (this.leaf || this.expanded) return;
event.currentTarget.tabIndex = -1;
this.$emit('node-toggle', this.node);
this.$nextTick(() => {
this.onArrowDown(event);
});
},
onArrowLeft(event) {
2023-05-26 11:22:08 +00:00
const togglerElement = DomHandler.findSingle(event.currentTarget, '[data-pc-section="toggler"]');
2022-12-08 11:04:25 +00:00
if (this.level === 0 && !this.expanded) {
return false;
}
if (this.expanded && !this.leaf) {
togglerElement.click();
return false;
}
const target = this.findBeforeClickableNode(event.currentTarget);
if (target) {
this.focusRowChange(event.currentTarget, target);
}
},
onEnterKey(event) {
this.setTabIndexForSelectionMode(event, this.nodeTouched);
this.onClick(event);
event.preventDefault();
},
onTabKey() {
this.setAllNodesTabIndexes();
},
setAllNodesTabIndexes() {
2023-06-20 10:49:32 +00:00
const nodes = DomHandler.find(this.$refs.currentNode.closest('[data-pc-section="container"]'), '[role="treeitem"]');
2022-12-08 11:04:25 +00:00
const hasSelectedNode = [...nodes].some((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true');
[...nodes].forEach((node) => {
node.tabIndex = -1;
});
if (hasSelectedNode) {
const selectedNodes = [...nodes].filter((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true');
selectedNodes[0].tabIndex = 0;
return;
}
[...nodes][0].tabIndex = 0;
},
setTabIndexForSelectionMode(event, nodeTouched) {
if (this.selectionMode !== null) {
2023-06-20 10:49:32 +00:00
const elements = [...DomHandler.find(this.$refs.currentNode.parentElement, '[role="treeitem"]')];
2022-12-08 11:04:25 +00:00
event.currentTarget.tabIndex = nodeTouched === false ? -1 : 0;
if (elements.every((element) => element.tabIndex === -1)) {
elements[0].tabIndex = 0;
}
}
},
focusRowChange(firstFocusableRow, currentFocusedRow, lastVisibleDescendant) {
firstFocusableRow.tabIndex = '-1';
currentFocusedRow.tabIndex = '0';
this.focusNode(lastVisibleDescendant || currentFocusedRow);
},
findBeforeClickableNode(node) {
const parentListElement = node.closest('ul').closest('li');
if (parentListElement) {
const prevNodeButton = DomHandler.findSingle(parentListElement, 'button');
if (prevNodeButton && prevNodeButton.style.visibility !== 'hidden') {
return parentListElement;
}
return this.findBeforeClickableNode(node.previousElementSibling);
}
return null;
},
2022-09-06 12:03:37 +00:00
toggleCheckbox() {
2022-09-14 11:26:01 +00:00
let _selectionKeys = this.selectionKeys ? { ...this.selectionKeys } : {};
2022-09-06 12:03:37 +00:00
const _check = !this.checked;
this.propagateDown(this.node, _check, _selectionKeys);
this.$emit('checkbox-change', {
node: this.node,
check: _check,
selectionKeys: _selectionKeys
});
},
propagateDown(node, check, selectionKeys) {
2022-09-14 11:26:01 +00:00
if (check) selectionKeys[node.key] = { checked: true, partialChecked: false };
else delete selectionKeys[node.key];
2022-09-06 12:03:37 +00:00
if (node.children && node.children.length) {
for (let child of node.children) {
this.propagateDown(child, check, selectionKeys);
}
}
},
propagateUp(event) {
let check = event.check;
2022-09-14 11:26:01 +00:00
let _selectionKeys = { ...event.selectionKeys };
2022-09-06 12:03:37 +00:00
let checkedChildCount = 0;
let childPartialSelected = false;
for (let child of this.node.children) {
2022-09-14 11:26:01 +00:00
if (_selectionKeys[child.key] && _selectionKeys[child.key].checked) checkedChildCount++;
else if (_selectionKeys[child.key] && _selectionKeys[child.key].partialChecked) childPartialSelected = true;
2022-09-06 12:03:37 +00:00
}
2022-09-14 11:26:01 +00:00
if (check && checkedChildCount === this.node.children.length) {
_selectionKeys[this.node.key] = { checked: true, partialChecked: false };
} else {
2022-09-06 12:03:37 +00:00
if (!check) {
delete _selectionKeys[this.node.key];
}
2022-09-14 11:26:01 +00:00
if (childPartialSelected || (checkedChildCount > 0 && checkedChildCount !== this.node.children.length)) _selectionKeys[this.node.key] = { checked: false, partialChecked: true };
else delete _selectionKeys[this.node.key];
2022-09-06 12:03:37 +00:00
}
this.$emit('checkbox-change', {
node: event.node,
check: event.check,
selectionKeys: _selectionKeys
});
},
onChildCheckboxChange(event) {
this.$emit('checkbox-change', event);
},
findNextSiblingOfAncestor(nodeElement) {
let parentNodeElement = this.getParentNodeElement(nodeElement);
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
if (parentNodeElement) {
2022-09-14 11:26:01 +00:00
if (parentNodeElement.nextElementSibling) return parentNodeElement.nextElementSibling;
else return this.findNextSiblingOfAncestor(parentNodeElement);
} else {
2022-09-06 12:03:37 +00:00
return null;
}
},
findLastVisibleDescendant(nodeElement) {
const childrenListElement = nodeElement.children[1];
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
if (childrenListElement) {
const lastChildElement = childrenListElement.children[childrenListElement.children.length - 1];
return this.findLastVisibleDescendant(lastChildElement);
2022-09-14 11:26:01 +00:00
} else {
2022-09-06 12:03:37 +00:00
return nodeElement;
}
},
getParentNodeElement(nodeElement) {
const parentNodeElement = nodeElement.parentElement.parentElement;
2023-06-20 10:49:32 +00:00
return DomHandler.getAttribute(parentNodeElement, 'role') === 'treeitem' ? parentNodeElement : null;
2022-09-06 12:03:37 +00:00
},
focusNode(element) {
2022-12-08 11:04:25 +00:00
element.focus();
2022-09-06 12:03:37 +00:00
},
isCheckboxSelectionMode() {
return this.selectionMode === 'checkbox';
2022-12-08 11:04:25 +00:00
},
isSameNode(event) {
2023-06-20 10:49:32 +00:00
return event.currentTarget && (event.currentTarget.isSameNode(event.target) || event.currentTarget.isSameNode(event.target.closest('[role="treeitem"]')));
2022-09-06 12:03:37 +00:00
}
},
computed: {
hasChildren() {
return this.node.children && this.node.children.length > 0;
},
expanded() {
return this.expandedKeys && this.expandedKeys[this.node.key] === true;
},
leaf() {
return this.node.leaf === false ? false : !(this.node.children && this.node.children.length);
},
selectable() {
return this.node.selectable === false ? false : this.selectionMode != null;
},
selected() {
2022-09-14 11:26:01 +00:00
return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.node.key] === true : false;
2022-09-06 12:03:37 +00:00
},
checkboxMode() {
return this.selectionMode === 'checkbox' && this.node.selectable !== false;
},
checked() {
2022-09-14 11:26:01 +00:00
return this.selectionKeys ? this.selectionKeys[this.node.key] && this.selectionKeys[this.node.key].checked : false;
2022-09-06 12:03:37 +00:00
},
partialChecked() {
2022-09-14 11:26:01 +00:00
return this.selectionKeys ? this.selectionKeys[this.node.key] && this.selectionKeys[this.node.key].partialChecked : false;
2022-12-08 11:04:25 +00:00
},
ariaChecked() {
return this.selectionMode === 'single' || this.selectionMode === 'multiple' ? this.selected : undefined;
},
ariaSelected() {
return this.checkboxMode ? this.checked : undefined;
2022-09-06 12:03:37 +00:00
}
},
components: {
2024-01-14 13:38:51 +00:00
Checkbox,
ChevronDownIcon,
ChevronRightIcon,
CheckIcon,
MinusIcon,
SpinnerIcon
},
2022-09-06 12:03:37 +00:00
directives: {
2022-09-14 11:26:01 +00:00
ripple: Ripple
2022-09-06 12:03:37 +00:00
}
2022-09-14 11:26:01 +00:00
};
2022-09-06 12:03:37 +00:00
</script>