@openremote/or-tree-menu 1.8.0-snapshot.20250725074716 → 1.8.0-snapshot.20250725120001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,103 +1,713 @@
1
- var __decorate=this&&this.__decorate||function(e,t,r,o){var i,n=arguments.length,s=n<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,r):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,r,o);else for(var l=e.length-1;l>=0;l--)(i=e[l])&&(s=(n<3?i(s):n>3?i(t,r,s):i(t,r))||s);return n>3&&s&&Object.defineProperty(t,r,s),s},__awaiter=this&&this.__awaiter||function(e,t,r,o){return new(r||(r=Promise))(function(i,n){function s(e){try{d(o.next(e))}catch(e){n(e)}}function l(e){try{d(o.throw(e))}catch(e){n(e)}}function d(e){var t;e.done?i(e.value):((t=e.value)instanceof r?t:new r(function(e){e(t)})).then(s,l)}d((o=o.apply(e,t||[])).next())})};import{css as e,html as t,LitElement as r}from"lit";import{customElement as o,property as i,queryAll as n}from"lit/decorators.js";import{map as s}from"lit/directives/map.js";import{when as l}from"lit/directives/when.js";import{InputType as d}from"@openremote/or-mwc-components/or-mwc-input";import{getContentWithMenuTemplate as a}from"@openremote/or-mwc-components/or-mwc-menu";import{Util as h}from"@openremote/core";import{moveNodesToGroupNode as p}from"./util";import{OrTreeDragEvent as c,OrTreeSelectEvent as u,TreeMenuSelection as g,TreeMenuSorting as _}from"./model";import{i18next as f}from"@openremote/or-translate";import"./or-tree-group";import"./or-tree-node";export*from"./or-tree-group";export*from"./or-tree-node";export*from"./model";let styles=e`
2
- * {
3
- box-sizing: border-box;
4
- }
5
-
6
- #tree-container {
7
- display: flex;
8
- flex-direction: column;
9
- height: 100%;
10
- }
11
-
12
- #tree-list {
13
- flex: 1;
14
- overflow: hidden auto;
15
- list-style: none;
16
- padding: 0;
17
- margin: 0;
18
- }
19
-
20
- or-tree-node > or-tree-node > * {
21
- pointer-events: none;
22
- }
23
-
24
- or-tree-node {
25
- transition: background-color 80ms;
26
- }
27
-
28
- or-tree-node[drophover] {
29
- background: #e9ecef;
30
- }
31
-
32
- #tree-header {
33
- display: flex;
34
- justify-content: space-between;
35
- align-items: center;
36
- padding: 0 15px;
37
- min-height: 48px;
38
- background: var(--or-app-color4, #4d9d2a);
39
- color: var(--or-app-color7, white);
40
- --or-icon-fill: var(--or-app-color7, white);
41
- }
42
-
43
- #tree-header-title {
44
- margin: 0;
45
- font-weight: 500;
46
- font-size: 16px;
47
- }
48
-
49
- #tree-header-actions {
50
- display: flex;
51
- align-items: center;
52
- }
53
- `,OrTreeMenu=class extends r{constructor(){super(...arguments),this.nodes=[],this.selection=g.LEAF,this.draggable=!1,this.noHeader=!1,this.menuTitle="Tree Menu",this.sortBy=_.A_TO_Z,this.groupFirst=!1,this._treeNodeCache=new Map}static get styles(){return[styles]}willUpdate(e){return(e.has("nodes")||e.has("sortBy")||e.has("groupFirst"))&&this.nodes&&(this.nodes=this._sortNodes(this.nodes,this.sortBy,this.groupFirst)),e.has("selection")&&e.get("selection")&&this.selection&&this.deselectAllNodes(),super.willUpdate(e)}render(){return t`
54
- <div id="tree-container">
55
- ${l(!this.noHeader,()=>this._getHeaderTemplate())}
56
- ${this._getTreeTemplate(this.nodes)}
57
- ${this._getErrorTemplate()}
58
- </div>
59
- `}moveNodesToGroup(e,t){this.nodes=p(e,t,this.nodes)}_getTreeTemplate(e){return this._treeNodeCache=new Map,t`
60
- <ol id="tree-list" @dragover=${this._onDragOverList} @drop=${this._onDragDropList}>
61
- ${s(e,e=>this._getNodeTemplate(e))}
62
- </ol>
63
- `}_getNodeTemplate(e,t){return e.children?this._getGroupNodeTemplate(e,t):this._getSingleNodeTemplate(e,t)}_getSingleNodeTemplate(e,r){let o=this._setTreeNodeId(e);return t`
64
- <li draggable=${this.draggable}
65
- @dragstart=${t=>this._onDragStart(t,e)}
66
- @dragover=${t=>this._onDragOverSingleNode(t,e,r)}
67
- @dragleave=${t=>this._onDragLeaveSingleNode(t,e,r)}
68
- @drop=${t=>this._onDragDropSingleNode(t,e,r)}>
69
- <or-tree-node id=${o} ?selected=${e.selected} ?readonly=${e.readonly} @click="${this._onTreeNodeClick}">
70
- ${this._getSingleNodeSlotTemplate(e)}
71
- </or-tree-node>
72
- </li>
73
- `}_getSingleNodeSlotTemplate(e){return t`
74
- <or-icon slot="prefix" icon="flag"></or-icon>
75
- <span>${e.label}</span>
76
- <span slot="suffix"></span>
77
- `}_getGroupNodeTemplate(e,r){let o=this.selection===g.LEAF,i=this._setTreeNodeId(e);return t`
78
- <li>
79
- <or-tree-group ?leaf=${o} ?expanded=${e.expanded}>
80
- <or-tree-node slot="parent" id=${i} ?readonly=${o}
81
- @click=${this._onTreeGroupClick}
82
- @dragover=${t=>this._onDragOverGroup(t,e)}
83
- @dragleave=${t=>this._onDragLeaveGroup(t,e)}
84
- @drop=${t=>this._onDragDropGroup(t,e)}>
85
- ${this._getGroupNodeSlotTemplate(e)}
86
- </or-tree-node>
87
- ${s(e.children,t=>this._getNodeTemplate(t,e))}
88
- </or-tree-group>
89
- </li>
90
- `}_getGroupNodeSlotTemplate(e){return t`
91
- <or-icon slot="prefix" icon="folder"></or-icon>
92
- <span>${e.label}</span>
93
- <span slot="suffix"></span>
94
- `}_getHeaderTemplate(){return t`
95
- <div id="tree-header">
96
- <h3 id="tree-header-title">
97
- <or-translate value=${this.menuTitle}></or-translate>
98
- </h3>
99
- <div id="tree-header-actions">
100
- ${this._getSortActionTemplate(this.sortBy,this.sortOptions)}
101
- </div>
102
- </div>
103
- `}_getSortActionTemplate(e,r){return a(t`<or-mwc-input type=${d.BUTTON} icon="sort-variant" title="${f.t("sort")}"></or-mwc-input>`,(r||[]).map(e=>({value:e,text:e})),e,e=>this._onSortClick(String(e)))}_getErrorTemplate(){return t``}_onSortClick(e){this.sortBy=e}_onTreeGroupClick(e){let t=e.currentTarget,r=t.parentElement,o=(e,t)=>{e.select(),this._lastSelectedNode=t,this._notifyNodesSelect()};switch(this.selection){case g.LEAF:return;case g.MULTI:if(e.shiftKey&&this._lastSelectedNode){let e=Array.from(this._uiNodes||[]),t=r.getGroupNode();if(t){let r=e.indexOf(t),o=e.indexOf(this._lastSelectedNode);this._selectNodesBetween(e,r,o);return}}else if(e.ctrlKey)return void o(r,t);this.deselectAllNodes(),o(r,t);return;case g.SINGLE:this.deselectAllNodes(),o(r,t);return}}_onTreeNodeClick(e){let t=e.currentTarget;if(t)if(this.selection===g.MULTI){if(e.shiftKey&&this._lastSelectedNode){let e=Array.from(this._uiNodes||[]),r=e.indexOf(this._lastSelectedNode),o=e.indexOf(t);if(r>-1&&o>-1)return void this._selectNodesBetween(e,r,o)}else if(e.ctrlKey)return void this._selectNode(t);this.deselectAllNodes(),this._selectNode(t);return}else{this.deselectAllNodes(),this._selectNode(t);return}}_onDragStart(e,t){var r;e.target?null==(r=e.dataTransfer)||r.setData("treeNode",JSON.stringify(t)):e.preventDefault()}_onDragOverList(e){this.draggable&&e.preventDefault()}_onDragOverSingleNode(e,t,r){if(this.draggable&&(e.preventDefault(),r)){let e=this._getUiNodeFromTree(r);null==e||e.setAttribute("drophover","true")}}_onDragOverGroup(e,t){this.draggable&&(e.preventDefault(),e.currentTarget.setAttribute("drophover","true"))}_onDragLeaveSingleNode(e,t,r){if(r&&this.draggable){e.preventDefault();let t=this._getUiNodeFromTree(r);null==t||t.removeAttribute("drophover")}}_onDragLeaveGroup(e,t){this.draggable&&e.currentTarget.removeAttribute("drophover")}_onDragDropList(e){this._onDragDropGroup(e)}_onDragDropSingleNode(e,t,r){this._onDragDropGroup(e,r)}_onDragDropGroup(e,t){var r,o;if(this.draggable){e.preventDefault(),e.stopPropagation(),t&&(null==(r=this._getUiNodeFromTree(t))||r.removeAttribute("drophover"));let i=[],n=null==(o=e.dataTransfer)?void 0:o.getData("treeNode");if(n){let e=JSON.parse(n);e&&i.push(e)}if(this.selection===g.MULTI){let e=this._findSelectedTreeNodes();(e=e.filter(e=>!e.children)).length>0&&(this.deselectAllNodes(),i.push(...e.filter(e=>!i.find(t=>JSON.stringify(t)===JSON.stringify(e)))))}(null==t?void 0:t.children)&&(i=i.filter(e=>{var r;return!(null==(r=t.children)?void 0:r.find(t=>t.id===e.id))})),i.length>0&&this._dispatchCancellableDragEvent(i,t,this.nodes).then(()=>{this.nodes=p(i,t,this.nodes)}).catch(e=>{})}}_dispatchCancellableDragEvent(e,t,r=[]){return new Promise((o,i)=>{this.dispatchEvent(new c(e,t,r))?o():i()})}_selectNode(e,t=!0){if(e){if(t){let t=[...this._findSelectedTreeNodes()],r=this._getTreeNodeFromTree(e);if(r&&t.push(r),!this._dispatchSelectEvent(t))return}e.selected=!0,this._lastSelectedNode=e}}_selectNodesBetween(e,t,r,o=!0){let i=[];if(t<r)for(let o=t;o<=r;o++)i.push(e[o]);else if(t>r)for(let o=r;o<=t;o++)i.push(e[o]);if(o){let e=i.map(e=>this._getTreeNodeFromTree(e)).filter(e=>e);if(!this._notifyNodesSelect(e))return}i.forEach(e=>this._selectNode(e))}_notifyNodesSelect(e){return __awaiter(this,void 0,void 0,function*(){return yield this.getUpdateComplete(),e||(e=this._findSelectedTreeNodes()),this._dispatchSelectEvent(e)})}_findSelectedTreeNodes(e=Array.from(this._uiNodes||[]),t=this._treeNodeCache){let r=e.filter(e=>e.selected),o=Array.from(t.entries());return r.map(e=>o.find(t=>t[1]===e.id)).map(e=>null==e?void 0:e[0]).filter(e=>void 0!==e)}_getTreeNodeFromTree(e,t=this._treeNodeCache){var r;return null==(r=Array.from(t.entries()).find(t=>t[1]===e.id))?void 0:r[0]}_getUiNodeFromTree(e,t=this._treeNodeCache){var r;let o=Array.from(t.entries()),i=JSON.stringify(e),n=null==(r=o.find(e=>JSON.stringify(e[0])===i))?void 0:r[1];return n?this.shadowRoot.getElementById(n):void 0}_dispatchSelectEvent(e){return this.dispatchEvent(new u(e||[]))}deselectAllNodes(){(this._uiGroups||[]).forEach(e=>e.deselect()),(this._uiNodes||[]).forEach(e=>e.selected=!1)}_sortNodes(e,t,r=!1){console.debug(`Sorting nodes in the tree menu using '${t}'`);let o=e.filter(e=>void 0!==e.children);if(o.forEach(e=>{var r;return null==(r=e.children)?void 0:r.sort(this._getSortFunction(t))}),r){let r=e.filter(e=>void 0===e.children);return o.sort(this._getSortFunction(t)),r.sort(this._getSortFunction(t)),[...o,...r]}return e.sort(this._getSortFunction(t))}_setTreeNodeId(e,t=Math.random().toString(36).substring(2,11)){return this._treeNodeCache.get(e)?this._treeNodeCache.get(e):(this._treeNodeCache.set(e,t),t)}_getSortFunction(e){return e===_.Z_TO_A?(e,t)=>t.label.localeCompare(e.label):h.sortByString(e=>e.label)}expandGroup(e){this.updateComplete.then(()=>{let t=this.nodes.find(t=>t.id===e&&t.children);t&&(t.expanded=!0,this.requestUpdate())})}};__decorate([i({type:Array})],OrTreeMenu.prototype,"nodes",void 0),__decorate([i({type:String})],OrTreeMenu.prototype,"selection",void 0),__decorate([i({type:Boolean})],OrTreeMenu.prototype,"draggable",void 0),__decorate([i({type:Boolean,attribute:"no-header"})],OrTreeMenu.prototype,"noHeader",void 0),__decorate([i({type:String,attribute:"menu-title"})],OrTreeMenu.prototype,"menuTitle",void 0),__decorate([i({type:Array,attribute:"sort-options"})],OrTreeMenu.prototype,"sortOptions",void 0),__decorate([i({type:String,attribute:"sort-by",reflect:!0})],OrTreeMenu.prototype,"sortBy",void 0),__decorate([i({type:Boolean,attribute:"group-first"})],OrTreeMenu.prototype,"groupFirst",void 0),__decorate([n("or-tree-node")],OrTreeMenu.prototype,"_uiNodes",void 0),__decorate([n("or-tree-group")],OrTreeMenu.prototype,"_uiGroups",void 0),OrTreeMenu=__decorate([o("or-tree-menu")],OrTreeMenu);export{OrTreeMenu};
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
8
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
9
+ return new (P || (P = Promise))(function (resolve, reject) {
10
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
11
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
12
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
13
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
14
+ });
15
+ };
16
+ /*
17
+ * Copyright 2025, OpenRemote Inc.
18
+ *
19
+ * See the CONTRIBUTORS.txt file in the distribution for a
20
+ * full listing of individual contributors.
21
+ *
22
+ * This program is free software: you can redistribute it and/or modify
23
+ * it under the terms of the GNU Affero General Public License as
24
+ * published by the Free Software Foundation, either version 3 of the
25
+ * License, or (at your option) any later version.
26
+ *
27
+ * This program is distributed in the hope that it will be useful,
28
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
+ * GNU Affero General Public License for more details.
31
+ *
32
+ * You should have received a copy of the GNU Affero General Public License
33
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
34
+ */
35
+ import { css, html, LitElement } from "lit";
36
+ import { customElement, property, queryAll } from "lit/decorators.js";
37
+ import { map } from "lit/directives/map.js";
38
+ import { when } from "lit/directives/when.js";
39
+ import { InputType } from "@openremote/or-mwc-components/or-mwc-input";
40
+ import { getContentWithMenuTemplate } from "@openremote/or-mwc-components/or-mwc-menu";
41
+ import { Util } from "@openremote/core";
42
+ import { moveNodesToGroupNode } from "./util";
43
+ import { OrTreeDragEvent, OrTreeSelectEvent, TreeMenuSelection, TreeMenuSorting } from "./model";
44
+ import { i18next } from "@openremote/or-translate";
45
+ import "./or-tree-group";
46
+ import "./or-tree-node";
47
+ export * from "./or-tree-group";
48
+ export * from "./or-tree-node";
49
+ export * from "./model";
50
+ const styles = css `
51
+ * {
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ #tree-container {
56
+ display: flex;
57
+ flex-direction: column;
58
+ height: 100%;
59
+ }
60
+
61
+ #tree-list {
62
+ flex: 1;
63
+ overflow: hidden auto;
64
+ list-style: none;
65
+ padding: 0;
66
+ margin: 0;
67
+ }
68
+
69
+ or-tree-node > or-tree-node > * {
70
+ pointer-events: none;
71
+ }
72
+
73
+ or-tree-node {
74
+ transition: background-color 80ms;
75
+ }
76
+
77
+ or-tree-node[drophover] {
78
+ background: #e9ecef;
79
+ }
80
+
81
+ #tree-header {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ padding: 0 15px;
86
+ min-height: 48px;
87
+ background: var(--or-app-color4, #4d9d2a);
88
+ color: var(--or-app-color7, white);
89
+ --or-icon-fill: var(--or-app-color7, white);
90
+ }
91
+
92
+ #tree-header-title {
93
+ margin: 0;
94
+ font-weight: 500;
95
+ font-size: 16px;
96
+ }
97
+
98
+ #tree-header-actions {
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+ `;
103
+ /**
104
+ * @event {OrTreeSelectEvent} or-tree-select - Triggers upon selecting a node, and dispatches a list of the nodes selected.
105
+ * @event {OrTreeDragEvent} or-tree-drag - Triggers upon dragging a node to a new group, and dispatches a list of dragged nodes, the group node, and the updated list of all nodes.
106
+ */
107
+ let OrTreeMenu = class OrTreeMenu extends LitElement {
108
+ constructor() {
109
+ super(...arguments);
110
+ /**
111
+ * List of node items in the menu.
112
+ * Uses the TreeNode format for rendering the OrTreeNode elements.
113
+ */
114
+ this.nodes = [];
115
+ /**
116
+ * Changes the allowed selection method within the tree.
117
+ * Common options are `LEAF`, `SINGLE` and `MULTI`.
118
+ */
119
+ this.selection = TreeMenuSelection.LEAF;
120
+ /**
121
+ * Disables and enables dragging of nodes into groups.
122
+ */
123
+ this.draggable = false;
124
+ /**
125
+ * Removes the header from the tree menu.
126
+ */
127
+ this.noHeader = false;
128
+ /**
129
+ * Adjusts the title in the menu header.
130
+ */
131
+ this.menuTitle = "Tree Menu";
132
+ /**
133
+ * Represents the selected sorting option
134
+ */
135
+ this.sortBy = TreeMenuSorting.A_TO_Z;
136
+ /**
137
+ * Automatically prioritizes the groups, and positions these on top.
138
+ */
139
+ this.groupFirst = false;
140
+ // A Map<TreeNode, unique generated ID> to easily identify which TreeNode has been used to render a TreeNodeComponent.
141
+ this._treeNodeCache = new Map();
142
+ }
143
+ static get styles() {
144
+ return [styles];
145
+ }
146
+ willUpdate(changedProps) {
147
+ if ((changedProps.has("nodes") || changedProps.has("sortBy") || changedProps.has("groupFirst")) && this.nodes) {
148
+ this.nodes = this._sortNodes(this.nodes, this.sortBy, this.groupFirst);
149
+ }
150
+ // If the selection method has been changed, for example from LEAF to SINGLE, we deselect everything
151
+ if (changedProps.has("selection") && changedProps.get("selection") && this.selection) {
152
+ this.deselectAllNodes();
153
+ }
154
+ return super.willUpdate(changedProps);
155
+ }
156
+ render() {
157
+ return html `
158
+ <div id="tree-container">
159
+ ${when(!this.noHeader, () => this._getHeaderTemplate())}
160
+ ${this._getTreeTemplate(this.nodes)}
161
+ ${this._getErrorTemplate()}
162
+ </div>
163
+ `;
164
+ }
165
+ /* ------------------------------------------------------------- */
166
+ //region Public functions for or-tree-menu
167
+ /* ------------------------------------------------------------- */
168
+ /**
169
+ * Function that moves an array of {@link TreeNode} into another {@link TreeNode}, by adding them to their children.
170
+ * The function takes care of removing the nodes from the former group, and makes sure no duplicates end up in the list.
171
+ *
172
+ * @param nodesToMove - The array of nodes that are moved into a group.
173
+ * @param groupNode - The group node to insert nodesToMove in.
174
+ */
175
+ moveNodesToGroup(nodesToMove, groupNode) {
176
+ this.nodes = moveNodesToGroupNode(nodesToMove, groupNode, this.nodes);
177
+ }
178
+ //endregion
179
+ /* ------------------------------------------------------------- */
180
+ //region HTML Templates functions
181
+ /* ------------------------------------------------------------- */
182
+ /**
183
+ * Returns a HTML template that displays the tree menu.
184
+ * @param nodes - List of nodes to be rendered
185
+ */
186
+ _getTreeTemplate(nodes) {
187
+ this._treeNodeCache = new Map(); // clear cache on every new render of the tree
188
+ return html `
189
+ <ol id="tree-list" @dragover=${this._onDragOverList} @drop=${this._onDragDropList}>
190
+ ${map(nodes, node => this._getNodeTemplate(node))}
191
+ </ol>
192
+ `;
193
+ }
194
+ /**
195
+ * Returns an HTML template for displaying a single node within a tree menu. This can both be a group or a solo node.
196
+ * @param node - Node to be rendered
197
+ * @param parent - Optional parent (group node) if the node is placed inside a group
198
+ */
199
+ _getNodeTemplate(node, parent) {
200
+ const isGroup = node.children;
201
+ if (isGroup) {
202
+ return this._getGroupNodeTemplate(node, parent);
203
+ }
204
+ else {
205
+ return this._getSingleNodeTemplate(node, parent);
206
+ }
207
+ }
208
+ /**
209
+ * Returns an HTML template for displaying a single node
210
+ * @param node - Node to be rendered
211
+ * @param parent - Optional parent (group node) if the node is placed inside a group
212
+ */
213
+ _getSingleNodeTemplate(node, parent) {
214
+ const randomId = this._setTreeNodeId(node);
215
+ return html `
216
+ <li draggable=${this.draggable}
217
+ @dragstart=${(ev) => this._onDragStart(ev, node)}
218
+ @dragover=${(ev) => this._onDragOverSingleNode(ev, node, parent)}
219
+ @dragleave=${(ev) => this._onDragLeaveSingleNode(ev, node, parent)}
220
+ @drop=${(ev) => this._onDragDropSingleNode(ev, node, parent)}>
221
+ <or-tree-node id=${randomId} ?selected=${node.selected} ?readonly=${node.readonly} @click="${this._onTreeNodeClick}">
222
+ ${this._getSingleNodeSlotTemplate(node)}
223
+ </or-tree-node>
224
+ </li>
225
+ `;
226
+ }
227
+ /**
228
+ * Returns an HTML template for the slot element inside <or-tree-node>.
229
+ * It allows customization such as a prefix icon, adjusting the label, etc.
230
+ * @param node - Node to be rendered
231
+ */
232
+ _getSingleNodeSlotTemplate(node) {
233
+ return html `
234
+ <or-icon slot="prefix" icon="flag"></or-icon>
235
+ <span>${node.label}</span>
236
+ <span slot="suffix"></span>
237
+ `;
238
+ }
239
+ /**
240
+ * Returns a HTML template for rendering a group node (aka a node with children)
241
+ * @param node - Node to be rendered
242
+ * @param _parent - Optional parent (group node) if the node is placed inside a group
243
+ */
244
+ _getGroupNodeTemplate(node, _parent) {
245
+ const leaf = this.selection === TreeMenuSelection.LEAF;
246
+ const randomId = this._setTreeNodeId(node);
247
+ return html `
248
+ <li>
249
+ <or-tree-group ?leaf=${leaf} ?expanded=${node.expanded}>
250
+ <or-tree-node slot="parent" id=${randomId} ?readonly=${leaf}
251
+ @click=${this._onTreeGroupClick}
252
+ @dragover=${(ev) => this._onDragOverGroup(ev, node)}
253
+ @dragleave=${(ev) => this._onDragLeaveGroup(ev, node)}
254
+ @drop=${(ev) => this._onDragDropGroup(ev, node)}>
255
+ ${this._getGroupNodeSlotTemplate(node)}
256
+ </or-tree-node>
257
+ ${map(node.children, n => this._getNodeTemplate(n, node))}
258
+ </or-tree-group>
259
+ </li>
260
+ `;
261
+ }
262
+ /**
263
+ * Returns an HTML template for the parent slot element inside <or-tree-group>.
264
+ * It allows customization such as a prefix icon, adjusting the label, etc.
265
+ * @param node - Node to be rendered
266
+ */
267
+ _getGroupNodeSlotTemplate(node) {
268
+ return html `
269
+ <or-icon slot="prefix" icon="folder"></or-icon>
270
+ <span>${node.label}</span>
271
+ <span slot="suffix"></span>
272
+ `;
273
+ }
274
+ /**
275
+ * Returns a HTML template for the header
276
+ */
277
+ _getHeaderTemplate() {
278
+ return html `
279
+ <div id="tree-header">
280
+ <h3 id="tree-header-title">
281
+ <or-translate value=${this.menuTitle}></or-translate>
282
+ </h3>
283
+ <div id="tree-header-actions">
284
+ ${this._getSortActionTemplate(this.sortBy, this.sortOptions)}
285
+ </div>
286
+ </div>
287
+ `;
288
+ }
289
+ /**
290
+ * Returns a HTML template for the sorting options menu.
291
+ * @param value - The selected sorting option
292
+ * @param options - The available sorting options
293
+ */
294
+ _getSortActionTemplate(value, options) {
295
+ return getContentWithMenuTemplate(html `<or-mwc-input type=${InputType.BUTTON} icon="sort-variant" title="${i18next.t("sort")}"></or-mwc-input>`, (options || []).map(sort => ({ value: sort, text: sort })), value, value => this._onSortClick(String(value)));
296
+ }
297
+ /**
298
+ * Returns a HTML template for displaying errors
299
+ */
300
+ _getErrorTemplate() {
301
+ return html ``;
302
+ }
303
+ //endregion
304
+ /* ------------------------------------------------------------- */
305
+ //region Event callback functions
306
+ /* ------------------------------------------------------------- */
307
+ /**
308
+ * HTML callback event for selecting a sort option in the dropdown menu,.
309
+ * @param value - The new selected sort option
310
+ */
311
+ _onSortClick(value) {
312
+ this.sortBy = value;
313
+ }
314
+ /**
315
+ * HTML callback event for clicking on a group node. (aka a node with children)
316
+ * Based on the configured TreeMenuSelection, it single- or multi selects the nodes.
317
+ */
318
+ _onTreeGroupClick(ev) {
319
+ const elem = ev.currentTarget;
320
+ const group = elem.parentElement;
321
+ const select = (treeGroup, nodeElem) => {
322
+ treeGroup.select();
323
+ this._lastSelectedNode = nodeElem;
324
+ this._notifyNodesSelect();
325
+ };
326
+ switch (this.selection) {
327
+ case TreeMenuSelection.LEAF: {
328
+ return; // Group node cannot be selected when in leaf
329
+ }
330
+ case TreeMenuSelection.MULTI: {
331
+ // Shift selects all nodes between the previous selected, and this one.
332
+ if (ev.shiftKey && this._lastSelectedNode) {
333
+ const nodes = Array.from(this._uiNodes || []);
334
+ const parentNode = group.getGroupNode();
335
+ if (parentNode) {
336
+ const indexOfClickedNode = nodes.indexOf(parentNode);
337
+ const indexOfPreviousNode = nodes.indexOf(this._lastSelectedNode);
338
+ this._selectNodesBetween(nodes, indexOfClickedNode, indexOfPreviousNode);
339
+ return;
340
+ }
341
+ // Ctrl multi selects without deselecting the previous one.
342
+ }
343
+ else if (ev.ctrlKey) {
344
+ select(group, elem);
345
+ return;
346
+ }
347
+ // Otherwise, select node like normal
348
+ this.deselectAllNodes();
349
+ select(group, elem);
350
+ return;
351
+ }
352
+ case TreeMenuSelection.SINGLE: {
353
+ this.deselectAllNodes();
354
+ select(group, elem);
355
+ return;
356
+ }
357
+ }
358
+ }
359
+ /** HTML callback event for when a child node of the tree gets clicked on. */
360
+ _onTreeNodeClick(ev) {
361
+ const node = ev.currentTarget;
362
+ if (node) {
363
+ switch (this.selection) {
364
+ case TreeMenuSelection.MULTI: {
365
+ // Shift selects all nodes between the previous selected, and this one.
366
+ if (ev.shiftKey && this._lastSelectedNode) {
367
+ const nodes = Array.from(this._uiNodes || []);
368
+ const prevIndex = nodes.indexOf(this._lastSelectedNode);
369
+ const clickedIndex = nodes.indexOf(node);
370
+ if (prevIndex > -1 && clickedIndex > -1) {
371
+ this._selectNodesBetween(nodes, prevIndex, clickedIndex);
372
+ return;
373
+ }
374
+ // Ctrl multi selects without deselecting the previous one.
375
+ }
376
+ else if (ev.ctrlKey) {
377
+ this._selectNode(node);
378
+ return;
379
+ }
380
+ // Otherwise select the node like normal
381
+ this.deselectAllNodes();
382
+ this._selectNode(node);
383
+ return;
384
+ }
385
+ default: {
386
+ this.deselectAllNodes();
387
+ this._selectNode(node);
388
+ return;
389
+ }
390
+ }
391
+ }
392
+ }
393
+ //endregion
394
+ /* ------------------------------------------------------------- */
395
+ //region Drag-and-drop callback functions
396
+ /* ------------------------------------------------------------- */
397
+ /** HTML callback event for 'dragstart' (the moment when a drag gesture is started) */
398
+ _onDragStart(ev, node) {
399
+ var _a;
400
+ if (ev.target) {
401
+ (_a = ev.dataTransfer) === null || _a === void 0 ? void 0 : _a.setData("treeNode", JSON.stringify(node));
402
+ }
403
+ else {
404
+ ev.preventDefault();
405
+ }
406
+ }
407
+ /** HTML callback event for 'dragover' on the list element */
408
+ _onDragOverList(ev) {
409
+ if (this.draggable)
410
+ ev.preventDefault();
411
+ }
412
+ /** HTML callback event for 'dragover', so while a node is dragged over a single node */
413
+ _onDragOverSingleNode(ev, node, parent) {
414
+ if (this.draggable) {
415
+ ev.preventDefault(); // allows dropping the node on this node
416
+ if (parent) {
417
+ const elem = this._getUiNodeFromTree(parent);
418
+ elem === null || elem === void 0 ? void 0 : elem.setAttribute("drophover", "true");
419
+ }
420
+ }
421
+ }
422
+ /** HTML callback event for 'dragover', so while a node is dragged over a group node */
423
+ _onDragOverGroup(ev, groupNode) {
424
+ if (this.draggable) {
425
+ ev.preventDefault(); // allows dropping the node on the group
426
+ ev.currentTarget.setAttribute("drophover", "true");
427
+ }
428
+ }
429
+ /** HTML callback event for 'dragover', so while a node is dragged over a single node */
430
+ _onDragLeaveSingleNode(ev, node, parent) {
431
+ if (parent && this.draggable) {
432
+ ev.preventDefault(); // allows dropping the node on this node
433
+ const elem = this._getUiNodeFromTree(parent);
434
+ elem === null || elem === void 0 ? void 0 : elem.removeAttribute("drophover");
435
+ }
436
+ }
437
+ /** HTML callback event for 'dragleave', so after a node has been dragged over a group node */
438
+ _onDragLeaveGroup(ev, groupNode) {
439
+ if (this.draggable)
440
+ ev.currentTarget.removeAttribute("drophover");
441
+ }
442
+ /** HTML callback event for when a node is dropped on the list level. */
443
+ _onDragDropList(ev) {
444
+ this._onDragDropGroup(ev);
445
+ }
446
+ /** HTML callback event for when a node is dropped onto a single node, after dragging it over. */
447
+ _onDragDropSingleNode(ev, groupNode, parent) {
448
+ this._onDragDropGroup(ev, parent);
449
+ }
450
+ /** HTML callback event for when a node is dropped onto a group node, after dragging it over. */
451
+ _onDragDropGroup(ev, groupNode) {
452
+ var _a, _b;
453
+ if (this.draggable) {
454
+ ev.preventDefault(); // allows dropping the node on this node
455
+ ev.stopPropagation();
456
+ // Remove "hover background" from the group node in the UI
457
+ if (groupNode)
458
+ (_a = this._getUiNodeFromTree(groupNode)) === null || _a === void 0 ? void 0 : _a.removeAttribute("drophover");
459
+ let nodesToMove = [];
460
+ // Get the dragged element from the event payload data
461
+ const data = (_b = ev.dataTransfer) === null || _b === void 0 ? void 0 : _b.getData("treeNode");
462
+ if (data) {
463
+ const node = JSON.parse(data);
464
+ if (node)
465
+ nodesToMove.push(node);
466
+ }
467
+ // If selection is multi, also add all selected ones
468
+ if (this.selection === TreeMenuSelection.MULTI) {
469
+ let selected = this._findSelectedTreeNodes();
470
+ // Filter out groups
471
+ selected = selected.filter(n => !n.children);
472
+ if (selected.length > 0) {
473
+ this.deselectAllNodes();
474
+ nodesToMove.push(...selected.filter(node => !nodesToMove.find(n => JSON.stringify(n) === JSON.stringify(node))));
475
+ }
476
+ }
477
+ // Filter out "nodes to move" that are already in that group
478
+ if (groupNode === null || groupNode === void 0 ? void 0 : groupNode.children) {
479
+ nodesToMove = nodesToMove.filter(node => { var _a; return !((_a = groupNode.children) === null || _a === void 0 ? void 0 : _a.find(child => child.id === node.id)); });
480
+ }
481
+ // Finally, move the nodes
482
+ if (nodesToMove.length > 0) {
483
+ this._dispatchCancellableDragEvent(nodesToMove, groupNode, this.nodes).then(() => {
484
+ this.nodes = moveNodesToGroupNode(nodesToMove, groupNode, this.nodes);
485
+ }).catch(ignored => { });
486
+ }
487
+ }
488
+ }
489
+ /**
490
+ * Dispatches a cancellable tree drag event.
491
+ */
492
+ _dispatchCancellableDragEvent(nodes, groupNode, allNodes = []) {
493
+ return new Promise((resolve, reject) => {
494
+ const success = this.dispatchEvent(new OrTreeDragEvent(nodes, groupNode, allNodes));
495
+ success ? resolve() : reject();
496
+ });
497
+ }
498
+ //endregion
499
+ /* ------------------------------------------------------------- */
500
+ /**
501
+ * Selects the node using the HTML attribute 'selected' of OrTreeNode
502
+ * @param node - Node to be selected
503
+ * @param notify - Boolean whether to notify the HTML parents of an or-tree-select.
504
+ */
505
+ _selectNode(node, notify = true) {
506
+ if (node) {
507
+ if (notify) {
508
+ const selected = [...this._findSelectedTreeNodes()];
509
+ const treeNode = this._getTreeNodeFromTree(node);
510
+ if (treeNode)
511
+ selected.push(treeNode);
512
+ const success = this._dispatchSelectEvent(selected); // Dispatch a "select" event, and return when cancelled by a consumer
513
+ if (!success)
514
+ return;
515
+ }
516
+ node.selected = true;
517
+ this._lastSelectedNode = node;
518
+ }
519
+ }
520
+ /**
521
+ * Multi-selects the nodes between two indexes in a list of OrTreeNode.
522
+ * @param nodes - List of nodes in the tree menu
523
+ * @param index1 - Start index of the nodes to select
524
+ * @param index2 - End index of the nodes to select
525
+ * @param notify - Boolean whether to notify the HTML parents of an or-tree-select.
526
+ */
527
+ _selectNodesBetween(nodes, index1, index2, notify = true) {
528
+ const selectedNodes = [];
529
+ if (index1 < index2) {
530
+ for (let x = index1; x <= index2; x++) {
531
+ selectedNodes.push(nodes[x]);
532
+ }
533
+ }
534
+ else if (index1 > index2) {
535
+ for (let x = index2; x <= index1; x++) {
536
+ selectedNodes.push(nodes[x]);
537
+ }
538
+ }
539
+ // Dispatch a "select" event with the new nodes. If cancelled, we will not select the nodes.
540
+ if (notify) {
541
+ const nodes = selectedNodes.map(n => this._getTreeNodeFromTree(n)).filter(n => n);
542
+ const success = this._notifyNodesSelect(nodes);
543
+ if (!success)
544
+ return;
545
+ }
546
+ // Select the nodes
547
+ selectedNodes.forEach((node) => this._selectNode(node));
548
+ }
549
+ /**
550
+ * Function that notifies parent HTMLElements that a tree node got selected.
551
+ * It dispatches the OrTreeSelectEvent, which includes a list of the selected nodes.
552
+ * @returns a 'success' boolean whether the event was cancelled or not
553
+ */
554
+ _notifyNodesSelect(selectedNodes) {
555
+ return __awaiter(this, void 0, void 0, function* () {
556
+ yield this.getUpdateComplete(); // await render to include the latest changes.
557
+ if (!selectedNodes) {
558
+ selectedNodes = this._findSelectedTreeNodes();
559
+ }
560
+ return this._dispatchSelectEvent(selectedNodes);
561
+ });
562
+ }
563
+ /**
564
+ * Utility function to detect the selected tree nodes.
565
+ * @param uiNodes - List of <or-tree-node> UI elements
566
+ * @param cache - Optionally, supply the cache for retrieving a {@link TreeNode} object from a <or-tree-node> element.
567
+ */
568
+ _findSelectedTreeNodes(uiNodes = Array.from(this._uiNodes || []), cache = this._treeNodeCache) {
569
+ // Get the list of selected TreeNodeComponent elements in the UI.
570
+ const selectedUiNodes = uiNodes.filter(n => n.selected);
571
+ // Get the list of cached generated IDs (for tracking which TreeNode belongs to which TreeNodeComponent)
572
+ const treeNodeEntries = Array.from(cache.entries());
573
+ // Find the generated IDs in the list of TreeNodeComponents, and compare the element ID
574
+ return selectedUiNodes.map(component => treeNodeEntries
575
+ .find(v => v[1] === component.id))
576
+ .map(x => x === null || x === void 0 ? void 0 : x[0])
577
+ .filter(n => n !== undefined);
578
+ }
579
+ /**
580
+ * Utility function that gets a {@link TreeNode} based on an {@link OrTreeNode} HTML element in the menu.
581
+ * @param uiNode - The tree node HTML element
582
+ * @param cache - Optional cache to get the TreeNode from
583
+ */
584
+ _getTreeNodeFromTree(uiNode, cache = this._treeNodeCache) {
585
+ var _a;
586
+ const treeNodeEntries = Array.from(cache.entries()); // Get the list of cached generated IDs (for tracking which TreeNode belongs to which TreeNodeComponent)
587
+ return (_a = treeNodeEntries.find(entry => entry[1] === uiNode.id)) === null || _a === void 0 ? void 0 : _a[0];
588
+ }
589
+ /**
590
+ * Utility function that gets an {@link OrTreeNode} HTML element based on the {@link TreeNode} input object.
591
+ * @param treeNode - The tree node object
592
+ * @param cache - Optional cache to get the HTML element from
593
+ */
594
+ _getUiNodeFromTree(treeNode, cache = this._treeNodeCache) {
595
+ var _a;
596
+ const treeNodeEntries = Array.from(cache.entries()); // Get the list of cached generated IDs (for tracking which TreeNode belongs to which TreeNodeComponent)
597
+ const treeNodeJSON = JSON.stringify(treeNode);
598
+ const elementId = (_a = treeNodeEntries.find(entry => JSON.stringify(entry[0]) === treeNodeJSON)) === null || _a === void 0 ? void 0 : _a[1];
599
+ return elementId ? this.shadowRoot.getElementById(elementId) : undefined;
600
+ }
601
+ /**
602
+ * Utility function for sending a "select" event, so consumers of this component are aware a new node has been selected.
603
+ * @param selectedNodes - List of selected nodes to include in the event payload.
604
+ * @returns a 'success' boolean whether the event was cancelled or not.
605
+ */
606
+ _dispatchSelectEvent(selectedNodes) {
607
+ return this.dispatchEvent(new OrTreeSelectEvent(selectedNodes || []));
608
+ }
609
+ /**
610
+ * Public function that deselects all tree nodes.
611
+ */
612
+ deselectAllNodes() {
613
+ (this._uiGroups || []).forEach(uiGroup => uiGroup.deselect());
614
+ (this._uiNodes || []).forEach(uiNode => uiNode.selected = false);
615
+ }
616
+ /**
617
+ * Utility function that sorts the list of {@link nodes} based on the given {@link sortBy} method.
618
+ * @param nodes - List of nodes to be sorted
619
+ * @param sortBy - Sorting option
620
+ * @param groupFirst - Whether to prioritize group nodes, and place them on the top of the list.
621
+ */
622
+ _sortNodes(nodes, sortBy, groupFirst = false) {
623
+ console.debug(`Sorting nodes in the tree menu using '${sortBy}'`);
624
+ const grouped = nodes.filter(node => node.children !== undefined);
625
+ // TODO: Apply recursive sorting if nested deeper inside the tree
626
+ grouped.forEach(g => { var _a; return (_a = g.children) === null || _a === void 0 ? void 0 : _a.sort(this._getSortFunction(sortBy)); });
627
+ // Optionally, prioritize group nodes, and place all groups on top of the menu
628
+ if (groupFirst) {
629
+ const ungrouped = nodes.filter(node => node.children === undefined);
630
+ grouped.sort(this._getSortFunction(sortBy));
631
+ ungrouped.sort(this._getSortFunction(sortBy));
632
+ return [...grouped, ...ungrouped];
633
+ }
634
+ return nodes.sort(this._getSortFunction(sortBy));
635
+ }
636
+ /**
637
+ * Function that caches a random ID into a key-value storage, linking the TreeNode with a generated ID.
638
+ * This generated ID can be used somewhere else, for example in an HTMLElement ID as a unique identifier.
639
+ * @param node - The TreeNode object to cache
640
+ * @param randomId - Optionally you can supply an ID to use for caching
641
+ */
642
+ _setTreeNodeId(node, randomId = Math.random().toString(36).substring(2, 11)) {
643
+ if (this._treeNodeCache.get(node)) {
644
+ return this._treeNodeCache.get(node);
645
+ }
646
+ this._treeNodeCache.set(node, randomId);
647
+ return randomId;
648
+ }
649
+ /**
650
+ * Function for retrieving the sorting for TreeNodes based on a sortBy parameter.
651
+ * The sortBy parameter represents a key in the TreeNode object like 'label'.
652
+ * @param sortBy - Sorting option to use, such as "a_to_z"
653
+ */
654
+ _getSortFunction(sortBy) {
655
+ switch (sortBy) {
656
+ case TreeMenuSorting.Z_TO_A:
657
+ return (a, b) => b.label.localeCompare(a.label);
658
+ default:
659
+ return Util.sortByString(node => node.label);
660
+ }
661
+ }
662
+ /**
663
+ * Programmatically finds a group by its ID and sets it to be expanded.
664
+ * This is called by the parent component after a new node is added to a tree group.
665
+ * @param {string} groupId The ID of the group to expand.
666
+ */
667
+ expandGroup(groupId) {
668
+ // Wait for any pending Lit updates to complete to ensure the node list is stable.
669
+ this.updateComplete.then(() => {
670
+ const groupNode = this.nodes.find(node => node.id === groupId && node.children);
671
+ if (groupNode) {
672
+ groupNode.expanded = true;
673
+ // re-render with the change.
674
+ this.requestUpdate();
675
+ }
676
+ });
677
+ }
678
+ };
679
+ __decorate([
680
+ property({ type: Array })
681
+ ], OrTreeMenu.prototype, "nodes", void 0);
682
+ __decorate([
683
+ property({ type: String })
684
+ ], OrTreeMenu.prototype, "selection", void 0);
685
+ __decorate([
686
+ property({ type: Boolean })
687
+ ], OrTreeMenu.prototype, "draggable", void 0);
688
+ __decorate([
689
+ property({ type: Boolean, attribute: "no-header" })
690
+ ], OrTreeMenu.prototype, "noHeader", void 0);
691
+ __decorate([
692
+ property({ type: String, attribute: "menu-title" })
693
+ ], OrTreeMenu.prototype, "menuTitle", void 0);
694
+ __decorate([
695
+ property({ type: Array, attribute: "sort-options" })
696
+ ], OrTreeMenu.prototype, "sortOptions", void 0);
697
+ __decorate([
698
+ property({ type: String, attribute: "sort-by", reflect: true })
699
+ ], OrTreeMenu.prototype, "sortBy", void 0);
700
+ __decorate([
701
+ property({ type: Boolean, attribute: "group-first" })
702
+ ], OrTreeMenu.prototype, "groupFirst", void 0);
703
+ __decorate([
704
+ queryAll("or-tree-node")
705
+ ], OrTreeMenu.prototype, "_uiNodes", void 0);
706
+ __decorate([
707
+ queryAll("or-tree-group")
708
+ ], OrTreeMenu.prototype, "_uiGroups", void 0);
709
+ OrTreeMenu = __decorate([
710
+ customElement("or-tree-menu")
711
+ ], OrTreeMenu);
712
+ export { OrTreeMenu };
713
+ //# sourceMappingURL=index.js.map