From 5a507cb1fa69f19d76434aa058cd25d11bc3309d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin?= <69278826+cetincakiroglu@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:34:13 +0300 Subject: [PATCH 1/3] Add checked property to treeNode --- src/app/components/api/treenode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/api/treenode.ts b/src/app/components/api/treenode.ts index d178ad837eb..1e64649c86c 100755 --- a/src/app/components/api/treenode.ts +++ b/src/app/components/api/treenode.ts @@ -3,6 +3,7 @@ * @group Interface */ export interface TreeNode { + checked?: boolean; /** * Label of the node. */ From 39465f9d25dd13749c14ac81a35836154a9dbd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin?= <69278826+cetincakiroglu@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:34:52 +0300 Subject: [PATCH 2/3] Fixed #15216 - TreeTable | Add selectionKeys support for checkbox selection. --- src/app/components/treetable/treetable.ts | 251 ++++++++++++++---- .../doc/treetable/selectioncheckboxdoc.ts | 37 ++- 2 files changed, 230 insertions(+), 58 deletions(-) diff --git a/src/app/components/treetable/treetable.ts b/src/app/components/treetable/treetable.ts index 187055e711b..d3cfb1ddd03 100755 --- a/src/app/components/treetable/treetable.ts +++ b/src/app/components/treetable/treetable.ts @@ -611,7 +611,17 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this._virtualRowHeight = val; console.warn('The virtualRowHeight property is deprecated, use virtualScrollItemSize property instead.'); } - _virtualRowHeight: number = 28; + /** + * A map of keys to control the selection state. + * @group Props + */ + @Input() get selectionKeys(): any { + return this._selectionKeys; + } + set selectionKeys(value: any) { + this._selectionKeys = value; + this.selectionKeysChange.emit(this._selectionKeys); + } /** * Callback to invoke on selected node change. * @param {TreeTableNode} object - Node instance. @@ -720,6 +730,12 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable * @group Emits */ @Output() onEditCancel: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when selectionKeys are changed. + * @param {Object} object - updated value of the selectionKeys. + * @group Emits + */ + @Output() selectionKeysChange: EventEmitter = new EventEmitter(); @ViewChild('container') containerViewChild: Nullable; @@ -739,6 +755,10 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable _value: TreeNode[] | undefined = []; + _virtualRowHeight: number = 28; + + _selectionKeys: any; + serializedValue: any[] | undefined | null; _totalRecords: number = 0; @@ -817,7 +837,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable _selection: any; - selectionKeys: any = {}; + selectedKeys: any = {}; rowTouched: Nullable; @@ -1001,7 +1021,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this._selection = simpleChange.selection.currentValue; if (!this.preventSelectionSetterPropagation) { - this.updateSelectionKeys(); + this.updateselectedKeys(); this.tableService.onSelectionChange(); } this.preventSelectionSetterPropagation = false; @@ -1056,15 +1076,15 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } } - updateSelectionKeys() { + updateselectedKeys() { if (this.dataKey && this._selection) { - this.selectionKeys = {}; + this.selectedKeys = {}; if (Array.isArray(this._selection)) { for (let node of this._selection) { - this.selectionKeys[String(ObjectUtils.resolveFieldData(node.data, this.dataKey))] = 1; + this.selectedKeys[String(ObjectUtils.resolveFieldData(node.data, this.dataKey))] = 1; } } else { - this.selectionKeys[String(ObjectUtils.resolveFieldData((this._selection).data, this.dataKey))] = 1; + this.selectedKeys[String(ObjectUtils.resolveFieldData((this._selection).data, this.dataKey))] = 1; } } } @@ -1587,14 +1607,14 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable if (selected && metaKey) { if (this.isSingleSelectionMode()) { this._selection = null; - this.selectionKeys = {}; + this.selectedKeys = {}; this.selectionChange.emit(null); } else { let selectionIndex = this.findIndexInSelection(rowNode.node); this._selection = this.selection.filter((val: TreeTableNode, i: number) => i != selectionIndex); this.selectionChange.emit(this.selection); if (dataKeyValue) { - delete this.selectionKeys[dataKeyValue]; + delete this.selectedKeys[dataKeyValue]; } } @@ -1604,21 +1624,21 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this._selection = rowNode.node; this.selectionChange.emit(rowNode.node); if (dataKeyValue) { - this.selectionKeys = {}; - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys = {}; + this.selectedKeys[dataKeyValue] = 1; } } else if (this.isMultipleSelectionMode()) { if (metaKey) { this._selection = this.selection || []; } else { this._selection = []; - this.selectionKeys = {}; + this.selectedKeys = {}; } this._selection = [...this.selection, rowNode.node]; this.selectionChange.emit(this.selection); if (dataKeyValue) { - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys[dataKeyValue] = 1; } } @@ -1628,7 +1648,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable if (this.selectionMode === 'single') { if (selected) { this._selection = null; - this.selectionKeys = {}; + this.selectedKeys = {}; this.selectionChange.emit(this.selection); this.onNodeUnselect.emit({ originalEvent: event.originalEvent, node: rowNode.node, type: 'row' }); } else { @@ -1636,8 +1656,8 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this.selectionChange.emit(this.selection); this.onNodeSelect.emit({ originalEvent: event.originalEvent, node: rowNode.node, type: 'row', index: event.rowIndex }); if (dataKeyValue) { - this.selectionKeys = {}; - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys = {}; + this.selectedKeys[dataKeyValue] = 1; } } } else if (this.selectionMode === 'multiple') { @@ -1647,14 +1667,14 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this.selectionChange.emit(this.selection); this.onNodeUnselect.emit({ originalEvent: event.originalEvent, node: rowNode.node, type: 'row' }); if (dataKeyValue) { - delete this.selectionKeys[dataKeyValue]; + delete this.selectedKeys[dataKeyValue]; } } else { this._selection = this.selection ? [...this.selection, rowNode.node] : [rowNode.node]; this.selectionChange.emit(this.selection); this.onNodeSelect.emit({ originalEvent: event.originalEvent, node: rowNode.node, type: 'row', index: event.rowIndex }); if (dataKeyValue) { - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys[dataKeyValue] = 1; } } } @@ -1695,7 +1715,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } if (dataKeyValue) { - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys[dataKeyValue] = 1; } } @@ -1706,6 +1726,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } toggleNodeWithCheckbox(event: any) { + // legacy selection support, will be removed in v18 this.selection = this.selection || []; this.preventSelectionSetterPropagation = true; let node = event.rowNode.node; @@ -1731,26 +1752,46 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } toggleNodesWithCheckbox(event: Event, check: boolean) { + // legacy selection support, will be removed in v18 let data = this.filteredNodes || this.value; this._selection = check && data ? data.slice() : []; - if (check) { - if (data && data.length) { - for (let node of data) { - this.propagateSelectionDown(node, true); - } - } - } else { + + this.toggleAll(check); + + if (!check) { this._selection = []; - this.selectionKeys = {}; + this.selectedKeys = {}; } this.preventSelectionSetterPropagation = true; this.selectionChange.emit(this._selection); this.tableService.onSelectionChange(); + this.onHeaderCheckboxToggle.emit({ originalEvent: event, checked: check }); } + toggleAll(checked: boolean) { + let data = this.filteredNodes || this.value; + + if (!this.selectionKeys) { + if (data && data.length) { + for (let node of data) { + this.propagateSelectionDown(node, checked); + } + } + } else { + // legacy selection support, will be removed in v18 + if (data && data.length) { + for (let node of data) { + this.propagateDown(node, checked); + } + this.selectionKeysChange.emit(this.selectionKeys); + } + } + } + propagateSelectionUp(node: TreeTableNode, select: boolean) { + // legacy selection support, will be removed in v18 if (node.children && node.children.length) { let selectedChildCount: number = 0; let childPartialSelected: boolean = false; @@ -1765,7 +1806,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this._selection = [...(this.selection || []), node]; node.partialSelected = false; if (dataKeyValue) { - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys[dataKeyValue] = 1; } } else { if (!select) { @@ -1774,7 +1815,7 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable this._selection = this.selection.filter((val: any, i: number) => i != index); if (dataKeyValue) { - delete this.selectionKeys[dataKeyValue]; + delete this.selectedKeys[dataKeyValue]; } } } @@ -1785,28 +1826,31 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } let parent = node.parent; + node.checked = select; if (parent) { this.propagateSelectionUp(parent, select); } } propagateSelectionDown(node: TreeTableNode, select: boolean) { + // legacy selection support, will be removed in v18 let index = this.findIndexInSelection(node); let dataKeyValue = this.dataKey ? String(ObjectUtils.resolveFieldData(node.data, this.dataKey)) : null; if (select && index == -1) { this._selection = [...(this.selection || []), node]; if (dataKeyValue) { - this.selectionKeys[dataKeyValue] = 1; + this.selectedKeys[dataKeyValue] = 1; } } else if (!select && index > -1) { this._selection = this.selection.filter((val: any, i: number) => i != index); if (dataKeyValue) { - delete this.selectionKeys[dataKeyValue]; + delete this.selectedKeys[dataKeyValue]; } } node.partialSelected = false; + node.checked = select; if (node.children && node.children.length) { for (let child of node.children) { @@ -1816,9 +1860,14 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable } isSelected(node: TreeTableNode) { + // legacy selection support, will be removed in v18 if (node && this.selection) { if (this.dataKey) { - return this.selectionKeys[ObjectUtils.resolveFieldData(node.data, this.dataKey)] !== undefined; + if (node.hasOwnProperty('checked')) { + return node['checked']; + } else { + return this.selectedKeys[ObjectUtils.resolveFieldData(node.data, this.dataKey)] !== undefined; + } } else { if (Array.isArray(this.selection)) return this.findIndexInSelection(node) > -1; else return this.equals(node, this.selection); @@ -1828,6 +1877,75 @@ export class TreeTable implements AfterContentInit, OnInit, OnDestroy, Blockable return false; } + isNodeSelected(node) { + return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.nodeKey(node)]?.checked === true : false; + } + + isNodePartialSelected(node) { + return this.selectionMode && this.selectionKeys ? this.selectionKeys[this.nodeKey(node)]?.partialChecked === true : false; + } + + nodeKey(node) { + return ObjectUtils.resolveFieldData(node, this.dataKey) || ObjectUtils.resolveFieldData(node?.data, this.dataKey); + } + + toggleCheckbox(event) { + let { rowNode, check, originalEvent } = event; + let node = rowNode.node; + if (this.selectionKeys) { + this.propagateDown(node, check); + if (node.parent) { + this.propagateUp(node.parent, check); + } + + this.selectionKeysChange.emit(this.selectionKeys); + } else { + this.toggleNodeWithCheckbox({ originalEvent, rowNode }); + } + + this.tableService.onSelectionChange(); + } + + propagateDown(node, check) { + if (check) { + this.selectionKeys[this.nodeKey(node)] = { checked: true, partialChecked: false }; + } else { + delete this.selectionKeys[this.nodeKey(node)]; + } + + if (node.children && node.children.length) { + for (let child of node.children) { + this.propagateDown(child, check); + } + } + } + + propagateUp(node, check) { + let checkedChildCount = 0; + let childPartialSelected = false; + + for (let child of node.children) { + if (this.selectionKeys[this.nodeKey(child)] && this.selectionKeys[this.nodeKey(child)].checked) checkedChildCount++; + else if (this.selectionKeys[this.nodeKey(child)] && this.selectionKeys[this.nodeKey(child)].partialChecked) childPartialSelected = true; + } + + if (check && checkedChildCount === node.children.length) { + this.selectionKeys[this.nodeKey(node)] = { checked: true, partialChecked: false }; + } else { + if (!check) { + delete this.selectionKeys[this.nodeKey(node)]; + } + + if (childPartialSelected || (checkedChildCount > 0 && checkedChildCount !== node.children.length)) this.selectionKeys[this.nodeKey(node)] = { checked: false, partialChecked: true }; + else this.selectionKeys[this.nodeKey(node)] = { checked: false, partialChecked: false }; + } + + let parent = node.parent; + if (parent) { + this.propagateUp(parent, check); + } + } + findIndexInSelection(node: any) { let index: number = -1; if (this.selection && this.selection.length) { @@ -2935,13 +3053,13 @@ export class TTContextMenuRow {
-
+
- + - +
@@ -2959,27 +3077,51 @@ export class TTCheckbox { checked: boolean | undefined; + partialChecked: boolean | undefined; + focused: boolean | undefined; subscription: Subscription | undefined; constructor(public tt: TreeTable, public tableService: TreeTableService, public cd: ChangeDetectorRef) { this.subscription = this.tt.tableService.selectionSource$.subscribe(() => { - this.checked = this.tt.isSelected(this.rowNode.node); + if (this.tt.selectionKeys) { + this.checked = this.tt.isNodeSelected(this.rowNode.node); + this.partialChecked = this.tt.isNodePartialSelected(this.rowNode.node); + } else { + this.checked = this.tt.isSelected(this.rowNode.node); + this.partialChecked = this.rowNode.node.partialSelected; + } this.cd.markForCheck(); }); } ngOnInit() { - this.checked = this.tt.isSelected(this.rowNode.node); + if (this.tt.selectionKeys) { + this.checked = this.tt.isNodeSelected(this.rowNode.node); + this.partialChecked = this.tt.isNodePartialSelected(this.rowNode.node); + } else { + // for backward compatibility + this.checked = this.tt.isSelected(this.rowNode.node); + this.partialChecked = this.rowNode.node.partialSelected; + } } onClick(event: Event) { if (!this.disabled) { - this.tt.toggleNodeWithCheckbox({ - originalEvent: event, - rowNode: this.rowNode - }); + if (this.tt.selectionKeys) { + const _check = !this.checked; + this.tt.toggleCheckbox({ + originalEvent: event, + check: _check, + rowNode: this.rowNode + }); + } else { + this.tt.toggleNodeWithCheckbox({ + originalEvent: event, + rowNode: this.rowNode + }); + } } DomHandler.clearSelection(); } @@ -3050,7 +3192,7 @@ export class TTHeaderCheckbox { } onClick(event: Event, checked: boolean) { - if (this.tt.value && this.tt.value.length > 0) { + if ((this.tt.value || this.tt.filteredNodes) && (this.tt.value.length > 0 || this.tt.filteredNodes.length > 0)) { this.tt.toggleNodesWithCheckbox(event, !checked); } @@ -3081,12 +3223,25 @@ export class TTHeaderCheckbox { const data = this.tt.filteredNodes || this.tt.value; if (data) { - for (let node of data) { - if (this.tt.isSelected(node)) { - checked = true; - } else { - checked = false; - break; + if (this.tt.selectionKeys) { + for (let node of data) { + if (this.tt.isNodeSelected(node)) { + checked = true; + } else { + checked = false; + break; + } + } + } + if (!this.tt.selectionKeys) { + // legacy selection support, will be removed in v18 + for (let node of data) { + if (this.tt.isSelected(node)) { + checked = true; + } else { + checked = false; + break; + } } } } else { diff --git a/src/app/showcase/doc/treetable/selectioncheckboxdoc.ts b/src/app/showcase/doc/treetable/selectioncheckboxdoc.ts index 979d64f2d40..5f5cf2f1899 100644 --- a/src/app/showcase/doc/treetable/selectioncheckboxdoc.ts +++ b/src/app/showcase/doc/treetable/selectioncheckboxdoc.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; import { TreeNode } from 'primeng/api'; import { Code } from '../../domain/code'; import { NodeService } from '../../service/nodeservice'; @@ -13,10 +13,15 @@ interface Column { template: `

Selection of multiple nodes via checkboxes is enabled by configuring selectionMode as checkbox.

+

+ In checkbox selection mode, value binding should be a key-value pair where key (or the dataKey) is the node key and value is an object that has checked and partialChecked properties to represent the checked state of a + node. +

-
+ +
- +
@@ -49,14 +54,17 @@ interface Column { export class SelectionCheckboxDoc { files!: TreeNode[]; - selectedNodes!: TreeNode[]; + selectionKeys = {}; cols!: Column[]; - constructor(private nodeService: NodeService) {} + constructor(private nodeService: NodeService, private cd: ChangeDetectorRef) {} loadDemoData() { - this.nodeService.getFilesystem().then((files) => (this.files = files)); + this.nodeService.getTreeTableNodes().then((files) => { + this.files = files; + this.cd.markForCheck(); + }); this.cols = [ { field: 'name', header: 'Name' }, @@ -66,7 +74,7 @@ export class SelectionCheckboxDoc { } code: Code = { - basic: ` + basic: `
@@ -93,7 +101,7 @@ export class SelectionCheckboxDoc { html: `
- +
@@ -136,14 +144,14 @@ interface Column { export class TreeTableSelectionCheckboxDemo implements OnInit { files!: TreeNode[]; - selectedNodes!: TreeNode[]; + selectionKeys = {}; cols!: Column[]; constructor(private nodeService: NodeService) {} ngOnInit() { - this.nodeService.getFilesystem().then((files) => (this.files = files)); + this.nodeService.getTreeTableNodes().then((files) => (this.files = files)); this.cols = [ { field: 'name', header: 'Name' }, @@ -155,4 +163,13 @@ export class TreeTableSelectionCheckboxDemo implements OnInit { service: ['NodeService'] }; + + code2: Code = { + typescript: `{ + '0-0': { + partialChecked: false, + checked: true + } +}` + }; } From 3811418febfb46bc656b103d6912356b35fd9da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin?= <69278826+cetincakiroglu@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:35:02 +0300 Subject: [PATCH 3/3] update dummy data --- src/app/showcase/service/nodeservice.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/showcase/service/nodeservice.ts b/src/app/showcase/service/nodeservice.ts index 35f390d08f2..9ef89620921 100755 --- a/src/app/showcase/service/nodeservice.ts +++ b/src/app/showcase/service/nodeservice.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { TreeNode } from "../../components/api/treenode"; +import { TreeNode } from '../../components/api/treenode'; @Injectable() export class NodeService { @@ -85,7 +85,7 @@ export class NodeService { { key: '0-0', data: { - name: 'React', + name: 'Angular', size: '25kb', type: 'Folder' }, @@ -93,7 +93,7 @@ export class NodeService { { key: '0-0-0', data: { - name: 'react.app', + name: 'angular.app', size: '10kb', type: 'Application' } @@ -774,7 +774,7 @@ export class NodeService { children.push({ key: `${parentIndex}-${childIndex}`, label: `Child ${parentIndex}-${childIndex}`, - selectable: true, + selectable: true }); } @@ -782,7 +782,7 @@ export class NodeService { key: parentIndex.toString(), label: `Parent ${parentIndex}`, selectable: true, - children: children, + children: children }); }