@openremote/or-tree-menu 1.8.0-snapshot.20250725074716 → 1.8.0-snapshot.20250725120000
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/README.md +1 -1
- package/custom-elements.json +22 -22
- package/dist/umd/index.bundle.js +731 -731
- package/dist/umd/index.bundle.js.map +1 -1
- package/dist/umd/index.js +731 -731
- package/dist/umd/index.js.map +1 -1
- package/dist/umd/index.orbundle.js +789 -789
- package/dist/umd/index.orbundle.js.map +1 -1
- package/lib/index.js +713 -103
- package/lib/model.js +76 -1
- package/lib/or-tree-group.js +226 -49
- package/lib/or-tree-node.js +111 -40
- package/lib/util.js +39 -1
- package/package.json +5 -5
- package/rspack.config.js +13 -13
package/lib/index.js
CHANGED
|
@@ -1,103 +1,713 @@
|
|
|
1
|
-
var __decorate
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|