Refactor #3965 - For Tree

pull/4004/head
Tuğçe Küçükoğlu 2023-05-26 14:22:08 +03:00
parent ba0b555d27
commit 0554604c00
4 changed files with 271 additions and 223 deletions

View File

@ -0,0 +1,213 @@
<script>
import BaseComponent from 'primevue/basecomponent';
import { useStyle } from 'primevue/usestyle';
const styles = `
.p-tree-container {
margin: 0;
padding: 0;
list-style-type: none;
overflow: auto;
}
.p-treenode-children {
margin: 0;
padding: 0;
list-style-type: none;
}
.p-tree-wrapper {
overflow: auto;
}
.p-treenode-selectable {
cursor: pointer;
user-select: none;
}
.p-tree-toggler {
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
.p-treenode-leaf > .p-treenode-content .p-tree-toggler {
visibility: hidden;
}
.p-treenode-content {
display: flex;
align-items: center;
}
.p-tree-filter {
width: 100%;
}
.p-tree-filter-container {
position: relative;
display: block;
width: 100%;
}
.p-tree-filter-icon {
position: absolute;
top: 50%;
margin-top: -0.5rem;
}
.p-tree-loading {
position: relative;
min-height: 4rem;
}
.p-tree .p-tree-loading-overlay {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.p-tree-flex-scrollable {
display: flex;
flex: 1;
height: 100%;
flex-direction: column;
}
.p-tree-flex-scrollable .p-tree-wrapper {
flex: 1;
}
`;
const classes = {
root: ({ props }) => [
'p-tree p-component',
{
'p-tree-selectable': props.selectionMode != null,
'p-tree-loading': props.loading,
'p-tree-flex-scrollable': props.scrollHeight === 'flex'
}
],
loadingOverlay: 'p-tree-loading-overlay p-component-overlay',
loadingIcon: 'p-tree-loading-icon',
filterContainer: 'p-tree-filter-container',
input: 'p-tree-filter p-inputtext p-component',
searchIcon: 'p-tree-filter-icon',
wrapper: 'p-tree-wrapper',
container: 'p-tree-container',
node: ({ treenode }) => ['p-treenode', { 'p-treenode-leaf': treenode.leaf }],
content: ({ treenode }) => [
'p-treenode-content',
treenode.node.styleClass,
{
'p-treenode-selectable': treenode.selectable,
'p-highlight': treenode.checkboxMode ? treenode.checked : treenode.selected
}
],
toggler: 'p-tree-toggler p-link',
togglerIcon: 'p-tree-toggler-icon',
checkboxContainer: 'p-checkbox p-component',
checkbox: ({ treenode }) => [
'p-checkbox-box',
{
'p-highlight': treenode.checked,
'p-indeterminate': treenode.partialChecked
}
],
checkboxIcon: 'p-checkbox-icon',
nodeIcon: ({ treenode }) => ['p-treenode-icon', treenode.node.icon],
label: 'p-treenode-label',
subgroup: 'p-treenode-children'
};
const { load: loadStyle, unload: unloadStyle } = useStyle(styles, { id: 'primevue_tree_style', manual: true });
export default {
name: 'BaseTree',
extends: BaseComponent,
props: {
value: {
type: null,
default: null
},
expandedKeys: {
type: null,
default: null
},
selectionKeys: {
type: null,
default: null
},
selectionMode: {
type: String,
default: null
},
metaKeySelection: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: String,
default: undefined
},
filter: {
type: Boolean,
default: false
},
filterBy: {
type: String,
default: 'label'
},
filterMode: {
type: String,
default: 'lenient'
},
filterPlaceholder: {
type: String,
default: null
},
filterLocale: {
type: String,
default: undefined
},
scrollHeight: {
type: String,
default: null
},
level: {
type: Number,
default: 0
},
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
css: {
classes
},
watch: {
isUnstyled: {
immediate: true,
handler(newValue) {
!newValue && loadStyle();
}
}
}
};
</script>

View File

@ -298,6 +298,11 @@ export interface TreeProps {
* @type {TreePassThroughOptions} * @type {TreePassThroughOptions}
*/ */
pt?: TreePassThroughOptions; pt?: TreePassThroughOptions;
/**
* When enabled, it removes component related styles in the core.
* @defaultValue false
*/
unstyled?: boolean;
} }
/** /**
@ -307,11 +312,21 @@ export interface TreeSlots {
/** /**
* Custom loading icon template. * Custom loading icon template.
*/ */
loadingicon(): VNode[]; loadingicon(scope: {
/**
* Style class of the icon.
*/
class: string;
}): VNode[];
/** /**
* Custom search icon template. * Custom search icon template.
*/ */
searchicon(): VNode[]; searchicon(scope: {
/**
* Style class of the icon.
*/
class: string;
}): VNode[];
/** /**
* Custom toggler icon template. * Custom toggler icon template.
*/ */

View File

@ -1,21 +1,21 @@
<template> <template>
<div :class="containerClass" v-bind="ptm('root')"> <div :class="cx('root')" v-bind="ptm('root')">
<template v-if="loading"> <template v-if="loading">
<div class="p-tree-loading-overlay p-component-overlay" v-bind="ptm('loadingOverlay')"> <div :class="cx('loadingOverlay')" v-bind="ptm('loadingOverlay')">
<slot name="loadingicon"> <slot name="loadingicon" :class="cx('loadingIcon')">
<i v-if="loadingIcon" :class="['p-tree-loading-icon pi-spin', loadingIcon]" v-bind="ptm('loadingIcon')" /> <i v-if="loadingIcon" :class="[cx('loadingIcon'), 'pi-spin', loadingIcon]" v-bind="ptm('loadingIcon')" />
<SpinnerIcon v-else spin class="p-tree-loading-icon" v-bind="ptm('loadingIcon')" /> <SpinnerIcon v-else spin :class="cx('loadingIcon')" v-bind="ptm('loadingIcon')" />
</slot> </slot>
</div> </div>
</template> </template>
<div v-if="filter" class="p-tree-filter-container" v-bind="ptm('filterContainer')"> <div v-if="filter" :class="cx('filterContainer')" v-bind="ptm('filterContainer')">
<input v-model="filterValue" type="text" autocomplete="off" class="p-tree-filter p-inputtext p-component" :placeholder="filterPlaceholder" @keydown="onFilterKeydown" v-bind="ptm('input')" /> <input v-model="filterValue" type="text" autocomplete="off" :class="cx('root')" :placeholder="filterPlaceholder" @keydown="onFilterKeydown" v-bind="ptm('input')" />
<slot name="searchicon"> <slot name="searchicon" :class="cx('searchIcon')">
<SearchIcon class="p-tree-filter-icon" v-bind="ptm('searchIcon')" /> <SearchIcon :class="cx('searchIcon')" v-bind="ptm('searchIcon')" />
</slot> </slot>
</div> </div>
<div class="p-tree-wrapper" :style="{ maxHeight: scrollHeight }" v-bind="ptm('wrapper')"> <div :class="cx('wrapper')" :style="{ maxHeight: scrollHeight }" v-bind="ptm('wrapper')">
<ul class="p-tree-container" role="tree" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel" v-bind="ptm('container')"> <ul :class="cx('container')" role="tree" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel" v-bind="ptm('container')">
<TreeNode <TreeNode
v-for="(node, index) of valueToRender" v-for="(node, index) of valueToRender"
:key="node.key" :key="node.key"
@ -37,82 +37,16 @@
</template> </template>
<script> <script>
import BaseComponent from 'primevue/basecomponent';
import SearchIcon from 'primevue/icons/search'; import SearchIcon from 'primevue/icons/search';
import SpinnerIcon from 'primevue/icons/spinner'; import SpinnerIcon from 'primevue/icons/spinner';
import { ObjectUtils } from 'primevue/utils'; import { ObjectUtils } from 'primevue/utils';
import BaseTree from './BaseTree.vue';
import TreeNode from './TreeNode.vue'; import TreeNode from './TreeNode.vue';
export default { export default {
name: 'Tree', name: 'Tree',
extends: BaseComponent, extends: BaseTree,
emits: ['node-expand', 'node-collapse', 'update:expandedKeys', 'update:selectionKeys', 'node-select', 'node-unselect'], emits: ['node-expand', 'node-collapse', 'update:expandedKeys', 'update:selectionKeys', 'node-select', 'node-unselect'],
props: {
value: {
type: null,
default: null
},
expandedKeys: {
type: null,
default: null
},
selectionKeys: {
type: null,
default: null
},
selectionMode: {
type: String,
default: null
},
metaKeySelection: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: String,
default: undefined
},
filter: {
type: Boolean,
default: false
},
filterBy: {
type: String,
default: 'label'
},
filterMode: {
type: String,
default: 'lenient'
},
filterPlaceholder: {
type: String,
default: null
},
filterLocale: {
type: String,
default: undefined
},
scrollHeight: {
type: String,
default: null
},
level: {
type: Number,
default: 0
},
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
data() { data() {
return { return {
d_expandedKeys: this.expandedKeys || {}, d_expandedKeys: this.expandedKeys || {},
@ -275,16 +209,6 @@ export default {
} }
}, },
computed: { computed: {
containerClass() {
return [
'p-tree p-component',
{
'p-tree-selectable': this.selectionMode != null,
'p-tree-loading': this.loading,
'p-tree-flex-scrollable': this.scrollHeight === 'flex'
}
];
},
filteredValue() { filteredValue() {
let filteredNodes = []; let filteredNodes = [];
const searchFields = this.filterBy.split(','); const searchFields = this.filterBy.split(',');
@ -317,87 +241,3 @@ export default {
} }
}; };
</script> </script>
<style>
.p-tree-container {
margin: 0;
padding: 0;
list-style-type: none;
overflow: auto;
}
.p-treenode-children {
margin: 0;
padding: 0;
list-style-type: none;
}
.p-tree-wrapper {
overflow: auto;
}
.p-treenode-selectable {
cursor: pointer;
user-select: none;
}
.p-tree-toggler {
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
.p-treenode-leaf > .p-treenode-content .p-tree-toggler {
visibility: hidden;
}
.p-treenode-content {
display: flex;
align-items: center;
}
.p-tree-filter {
width: 100%;
}
.p-tree-filter-container {
position: relative;
display: block;
width: 100%;
}
.p-tree-filter-icon {
position: absolute;
top: 50%;
margin-top: -0.5rem;
}
.p-tree-loading {
position: relative;
min-height: 4rem;
}
.p-tree .p-tree-loading-overlay {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.p-tree-flex-scrollable {
display: flex;
flex: 1;
height: 100%;
flex-direction: column;
}
.p-tree-flex-scrollable .p-tree-wrapper {
flex: 1;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<li <li
ref="currentNode" ref="currentNode"
:class="containerClass" :class="getCXOptions('node')"
role="treeitem" role="treeitem"
:aria-label="label(node)" :aria-label="label(node)"
:aria-selected="ariaSelected" :aria-selected="ariaSelected"
@ -12,27 +12,28 @@
:aria-checked="ariaChecked" :aria-checked="ariaChecked"
:tabindex="index === 0 ? 0 : -1" :tabindex="index === 0 ? 0 : -1"
@keydown="onKeyDown" @keydown="onKeyDown"
v-bind="getPTOptions('node')" v-bind="level === 1 ? getPTOptions('node') : ptm('subgroup')"
data-pc-section="treeitem"
> >
<div :class="contentClass" @click="onClick" @touchend="onTouchEnd" :style="node.style" v-bind="getPTOptions('content')"> <div :class="getCXOptions('content')" @click="onClick" @touchend="onTouchEnd" :style="node.style" v-bind="getPTOptions('content')">
<button v-ripple type="button" class="p-tree-toggler p-link" @click="toggle" tabindex="-1" aria-hidden="true" v-bind="getPTOptions('toggler')"> <button v-ripple type="button" :class="cx('toggler')" @click="toggle" tabindex="-1" aria-hidden="true" v-bind="getPTOptions('toggler')">
<component v-if="templates['togglericon']" :is="templates['togglericon']" :node="node" :expanded="expanded" class="p-tree-toggler-icon" /> <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="p-tree-toggler-icon" v-bind="getPTOptions('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="p-tree-toggler-icon" v-bind="getPTOptions('togglerIcon')" /> <component v-else :is="node.collapsedIcon ? 'span' : 'ChevronRightIcon'" :class="cx('togglerIcon')" v-bind="getPTOptions('togglerIcon')" />
</button> </button>
<div v-if="checkboxMode" class="p-checkbox p-component" aria-hidden="true" v-bind="getPTOptions('checkboxContainer')"> <div v-if="checkboxMode" :class="cx('checkboxContainer')" aria-hidden="true" v-bind="getPTOptions('checkboxContainer')">
<div :class="checkboxClass" role="checkbox" v-bind="getPTOptions('checkbox')"> <div :class="getCXOptions('checkbox')" role="checkbox" v-bind="getPTOptions('checkbox')">
<component v-if="templates['checkboxicon']" :is="templates['checkboxicon']" :checked="checked" :partialChecked="partialChecked" class="p-checkbox-icon" /> <component v-if="templates['checkboxicon']" :is="templates['checkboxicon']" :checked="checked" :partialChecked="partialChecked" :class="cx('checkboxIcon')" />
<component v-else :is="checked ? 'CheckIcon' : partialChecked ? 'MinusIcon' : null" class="p-checkbox-icon" v-bind="getPTOptions('checkboxIcon')" /> <component v-else :is="checked ? 'CheckIcon' : partialChecked ? 'MinusIcon' : null" :class="cx('checkboxIcon')" v-bind="getPTOptions('checkboxIcon')" />
</div> </div>
</div> </div>
<span :class="icon" v-bind="getPTOptions('nodeIcon')"></span> <span :class="getCXOptions('nodeIcon')" v-bind="getPTOptions('nodeIcon')"></span>
<span class="p-treenode-label" v-bind="getPTOptions('label')"> <span :class="cx('label')" v-bind="getPTOptions('label')">
<component v-if="templates[node.type] || templates['default']" :is="templates[node.type] || templates['default']" :node="node" /> <component v-if="templates[node.type] || templates['default']" :is="templates[node.type] || templates['default']" :node="node" />
<template v-else>{{ label(node) }}</template> <template v-else>{{ label(node) }}</template>
</span> </span>
</div> </div>
<ul v-if="hasChildren && expanded" class="p-treenode-children" role="group" v-bind="ptm('subgroup')"> <ul v-if="hasChildren && expanded" :class="cx('subgroup')" role="group" v-bind="ptm('subgroup')">
<TreeNode <TreeNode
v-for="childNode of node.children" v-for="childNode of node.children"
:key="childNode.key" :key="childNode.key"
@ -52,17 +53,17 @@
</template> </template>
<script> <script>
import BaseComponent from 'primevue/basecomponent';
import CheckIcon from 'primevue/icons/check'; import CheckIcon from 'primevue/icons/check';
import ChevronDownIcon from 'primevue/icons/chevrondown'; import ChevronDownIcon from 'primevue/icons/chevrondown';
import ChevronRightIcon from 'primevue/icons/chevronright'; import ChevronRightIcon from 'primevue/icons/chevronright';
import MinusIcon from 'primevue/icons/minus'; import MinusIcon from 'primevue/icons/minus';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
import { DomHandler } from 'primevue/utils'; import { DomHandler } from 'primevue/utils';
import BaseTree from './BaseTree.vue';
export default { export default {
name: 'TreeNode', name: 'TreeNode',
extends: BaseComponent, extends: BaseTree,
emits: ['node-toggle', 'node-click', 'checkbox-change'], emits: ['node-toggle', 'node-click', 'checkbox-change'],
props: { props: {
node: { node: {
@ -96,13 +97,6 @@ export default {
}, },
nodeTouched: false, nodeTouched: false,
toggleClicked: false, toggleClicked: false,
mounted() {
const hasTreeSelectParent = this.$refs.currentNode.closest('.p-treeselect-items-wrapper');
if (hasTreeSelectParent) {
this.setAllNodesTabIndexes();
}
},
methods: { methods: {
toggle() { toggle() {
this.$emit('node-toggle', this.node); this.$emit('node-toggle', this.node);
@ -123,8 +117,13 @@ export default {
} }
}); });
}, },
getCXOptions(key) {
return this.cx(key, {
treenode: this
});
},
onClick(event) { onClick(event) {
if (this.toggleClicked || DomHandler.hasClass(event.target, 'p-tree-toggler') || DomHandler.hasClass(event.target.parentElement, 'p-tree-toggler')) { if (this.toggleClicked || DomHandler.getAttribute(event.target, '[data-pc-section="toggler"]') || DomHandler.getAttribute(event.target.parentElement, '[data-pc-section="toggler"]')) {
this.toggleClicked = false; this.toggleClicked = false;
return; return;
@ -233,7 +232,7 @@ export default {
}); });
}, },
onArrowLeft(event) { onArrowLeft(event) {
const togglerElement = DomHandler.findSingle(event.currentTarget, '.p-tree-toggler'); const togglerElement = DomHandler.findSingle(event.currentTarget, '[data-pc-section="toggler"]');
if (this.level === 0 && !this.expanded) { if (this.level === 0 && !this.expanded) {
return false; return false;
@ -261,7 +260,7 @@ export default {
this.setAllNodesTabIndexes(); this.setAllNodesTabIndexes();
}, },
setAllNodesTabIndexes() { setAllNodesTabIndexes() {
const nodes = DomHandler.find(this.$refs.currentNode.closest('.p-tree-container'), '.p-treenode'); const nodes = DomHandler.find(this.$refs.currentNode.closest('[data-pc-section="container"]'), '[data-pc-section="treeitem"]');
const hasSelectedNode = [...nodes].some((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true'); const hasSelectedNode = [...nodes].some((node) => node.getAttribute('aria-selected') === 'true' || node.getAttribute('aria-checked') === 'true');
@ -281,7 +280,7 @@ export default {
}, },
setTabIndexForSelectionMode(event, nodeTouched) { setTabIndexForSelectionMode(event, nodeTouched) {
if (this.selectionMode !== null) { if (this.selectionMode !== null) {
const elements = [...DomHandler.find(this.$refs.currentNode.parentElement, '.p-treenode')]; const elements = [...DomHandler.find(this.$refs.currentNode.parentElement, '[data-pc-section="node"]')];
event.currentTarget.tabIndex = nodeTouched === false ? -1 : 0; event.currentTarget.tabIndex = nodeTouched === false ? -1 : 0;
@ -388,7 +387,7 @@ export default {
getParentNodeElement(nodeElement) { getParentNodeElement(nodeElement) {
const parentNodeElement = nodeElement.parentElement.parentElement; const parentNodeElement = nodeElement.parentElement.parentElement;
return DomHandler.hasClass(parentNodeElement, 'p-treenode') ? parentNodeElement : null; return DomHandler.getAttribute(parentNodeElement, '[data-pc-section="node"]') ? parentNodeElement : null;
}, },
focusNode(element) { focusNode(element) {
element.focus(); element.focus();
@ -397,7 +396,7 @@ export default {
return this.selectionMode === 'checkbox'; return this.selectionMode === 'checkbox';
}, },
isSameNode(event) { isSameNode(event) {
return event.currentTarget && (event.currentTarget.isSameNode(event.target) || event.currentTarget.isSameNode(event.target.closest('.p-treenode'))); return event.currentTarget && (event.currentTarget.isSameNode(event.target) || event.currentTarget.isSameNode(event.target.closest('[data-pc-section="node"]')));
} }
}, },
computed: { computed: {
@ -416,25 +415,6 @@ export default {
selected() { selected() {
return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.node.key] === true : false; return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.node.key] === true : false;
}, },
containerClass() {
return ['p-treenode', { 'p-treenode-leaf': this.leaf }];
},
contentClass() {
return [
'p-treenode-content',
this.node.styleClass,
{
'p-treenode-selectable': this.selectable,
'p-highlight': this.checkboxMode ? this.checked : this.selected
}
];
},
icon() {
return ['p-treenode-icon', this.node.icon];
},
checkboxClass() {
return ['p-checkbox-box', { 'p-highlight': this.checked, 'p-indeterminate': this.partialChecked }];
},
checkboxMode() { checkboxMode() {
return this.selectionMode === 'checkbox' && this.node.selectable !== false; return this.selectionMode === 'checkbox' && this.node.selectable !== false;
}, },