@schukai/monster 4.62.0 → 4.63.0
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.63.0] - 2025-12-30
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Add new HTML Tree View for Issue [#350](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/350)
|
|
10
|
+
### Changes
|
|
11
|
+
|
|
12
|
+
- update doc
|
|
13
|
+
- Update issue 349 documentation and dropzone configuration
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
5
17
|
## [4.62.0] - 2025-12-30
|
|
6
18
|
|
|
7
19
|
### Add Features
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.63.0"}
|
|
@@ -107,6 +107,10 @@ function getLocalizedLabel() {
|
|
|
107
107
|
*
|
|
108
108
|
* Displays a monster-select with all available languages except the current document language.
|
|
109
109
|
*
|
|
110
|
+
* @fragments /fragments/components/accessibility/locale-select/
|
|
111
|
+
*
|
|
112
|
+
* @example /examples/components/accessibility/locale-select-simple Simple language switcher
|
|
113
|
+
*
|
|
110
114
|
* @since 3.97.0
|
|
111
115
|
* @copyright Volker Schukai
|
|
112
116
|
* @summary A simple language switcher as a select.
|
|
@@ -92,7 +92,8 @@ const fileTimeoutMapSymbol = Symbol("fileTimeoutMap");
|
|
|
92
92
|
*
|
|
93
93
|
* @fragments /fragments/components/form/dropzone/
|
|
94
94
|
*
|
|
95
|
-
* @example /examples/components/form/dropzone-simple
|
|
95
|
+
* @example /examples/components/form/dropzone-simple Simple dropzone
|
|
96
|
+
* @example /examples/components/form/dropzone-avatar Profile image upload
|
|
96
97
|
*
|
|
97
98
|
* @since 4.40.0
|
|
98
99
|
* @copyright Volker Schukai
|
|
@@ -55,7 +55,7 @@ const timerCallbackSymbol = Symbol("timerCallback");
|
|
|
55
55
|
*
|
|
56
56
|
* @fragments /fragments/components/layout/iframe/
|
|
57
57
|
*
|
|
58
|
-
* @example /examples/components/layout/iframe-simple
|
|
58
|
+
* @example /examples/components/layout/iframe-simple Simple iframe
|
|
59
59
|
*
|
|
60
60
|
* @since 3.76.0
|
|
61
61
|
* @copyright Volker Schukai
|
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
7
|
+
*
|
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
|
10
|
+
* For more information about purchasing a commercial license, please contact Volker Schukai.
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { instanceSymbol } from "../../constants.mjs";
|
|
16
|
+
import { addAttributeToken } from "../../dom/attributes.mjs";
|
|
17
|
+
import {
|
|
18
|
+
ATTRIBUTE_ERRORMESSAGE,
|
|
19
|
+
ATTRIBUTE_ROLE,
|
|
20
|
+
ATTRIBUTE_UPDATER_INSERT_REFERENCE,
|
|
21
|
+
} from "../../dom/constants.mjs";
|
|
22
|
+
import {
|
|
23
|
+
assembleMethodSymbol,
|
|
24
|
+
CustomElement,
|
|
25
|
+
registerCustomElement,
|
|
26
|
+
} from "../../dom/customelement.mjs";
|
|
27
|
+
import { findTargetElementFromEvent } from "../../dom/events.mjs";
|
|
28
|
+
import { fireCustomEvent } from "../../dom/events.mjs";
|
|
29
|
+
import { isFunction, isString } from "../../types/is.mjs";
|
|
30
|
+
import { ID } from "../../types/id.mjs";
|
|
31
|
+
import { CommonStyleSheet } from "../stylesheet/common.mjs";
|
|
32
|
+
import { TreeMenuStyleSheet } from "./stylesheet/tree-menu.mjs";
|
|
33
|
+
import { ATTRIBUTE_INTEND } from "./../constants.mjs";
|
|
34
|
+
|
|
35
|
+
export { HtmlTreeMenu };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @private
|
|
39
|
+
* @type {symbol}
|
|
40
|
+
*/
|
|
41
|
+
const entryIndexSymbol = Symbol("entryIndex");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @private
|
|
45
|
+
* @type {symbol}
|
|
46
|
+
*/
|
|
47
|
+
const openEntryEventHandlerSymbol = Symbol("openEntryEventHandler");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* HtmlTreeMenu
|
|
51
|
+
*
|
|
52
|
+
* @since 4.62.0
|
|
53
|
+
* @summary A TreeMenu control that builds its entries from nested HTML lists.
|
|
54
|
+
* @fires entries-imported
|
|
55
|
+
*/
|
|
56
|
+
class HtmlTreeMenu extends CustomElement {
|
|
57
|
+
/**
|
|
58
|
+
* This method is called by the `instanceof` operator.
|
|
59
|
+
* @return {symbol}
|
|
60
|
+
*/
|
|
61
|
+
static get [instanceSymbol]() {
|
|
62
|
+
return Symbol.for("@schukai/monster/components/tree-menu/html@@instance");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @property {Object} templates Template definitions
|
|
67
|
+
* @property {string} templates.main Main template
|
|
68
|
+
* @property {Object} classes
|
|
69
|
+
* @property {String} classes.control the class for the control element
|
|
70
|
+
* @property {String} classes.label the class for the label element
|
|
71
|
+
* @property {Object} lazy
|
|
72
|
+
* @property {boolean} lazy.enabled enables lazy loading by endpoint
|
|
73
|
+
* @property {string} lazy.attribute="data-monster-endpoint" attribute for the endpoint
|
|
74
|
+
* @property {Object} lazy.fetchOptions fetch options for lazy requests
|
|
75
|
+
* @property {Object} features
|
|
76
|
+
* @property {boolean} features.selectParents=false allow selecting entries with children
|
|
77
|
+
* @property {Object} actions
|
|
78
|
+
* @property {Function} actions.open the action to open an entry (entry, index, event)
|
|
79
|
+
* @property {Function} actions.close the action to close an entry (entry, index, event)
|
|
80
|
+
* @property {Function} actions.select the action to select an entry (entry, index, event)
|
|
81
|
+
* @property {Function} actions.onexpand the action to expand an entry (entry, index, event)
|
|
82
|
+
* @property {Function} actions.oncollapse the action to collapse an entry (entry, index, event)
|
|
83
|
+
* @property {Function} actions.onselect the action to select an entry (entry, index, event)
|
|
84
|
+
* @property {Function} actions.onnavigate the action to navigate (entry, index, event)
|
|
85
|
+
* @property {Function} actions.onlazyload the action before lazy load (entry, index, event)
|
|
86
|
+
* @property {Function} actions.onlazyloaded the action after lazy load (entry, index, event)
|
|
87
|
+
* @property {Function} actions.onlazyerror the action on lazy error (entry, index, event)
|
|
88
|
+
*/
|
|
89
|
+
get defaults() {
|
|
90
|
+
return Object.assign({}, super.defaults, {
|
|
91
|
+
classes: {
|
|
92
|
+
control: "monster-theme-primary-1",
|
|
93
|
+
label: "monster-theme-primary-1",
|
|
94
|
+
},
|
|
95
|
+
lazy: {
|
|
96
|
+
enabled: true,
|
|
97
|
+
attribute: "data-monster-endpoint",
|
|
98
|
+
fetchOptions: {
|
|
99
|
+
method: "GET",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
features: {
|
|
103
|
+
selectParents: false,
|
|
104
|
+
},
|
|
105
|
+
templates: {
|
|
106
|
+
main: getTemplate(),
|
|
107
|
+
},
|
|
108
|
+
actions: {
|
|
109
|
+
open: null,
|
|
110
|
+
close: null,
|
|
111
|
+
select: (entry) => {
|
|
112
|
+
console.warn("select action is not defined", entry);
|
|
113
|
+
},
|
|
114
|
+
onexpand: null,
|
|
115
|
+
oncollapse: null,
|
|
116
|
+
onselect: null,
|
|
117
|
+
onnavigate: null,
|
|
118
|
+
onlazyload: null,
|
|
119
|
+
onlazyloaded: null,
|
|
120
|
+
onlazyerror: null,
|
|
121
|
+
},
|
|
122
|
+
entries: [],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @return {void}
|
|
128
|
+
*/
|
|
129
|
+
[assembleMethodSymbol]() {
|
|
130
|
+
super[assembleMethodSymbol]();
|
|
131
|
+
this[entryIndexSymbol] = new Map();
|
|
132
|
+
initEventHandler.call(this);
|
|
133
|
+
importEntries.call(this);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @return {CSSStyleSheet[]}
|
|
138
|
+
*/
|
|
139
|
+
static getCSSStyleSheet() {
|
|
140
|
+
return [CommonStyleSheet, TreeMenuStyleSheet];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @return {string}
|
|
145
|
+
*/
|
|
146
|
+
static getTag() {
|
|
147
|
+
return "monster-html-tree-menu";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Select an entry by value.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} value
|
|
154
|
+
* @return {void}
|
|
155
|
+
*/
|
|
156
|
+
selectEntry(value) {
|
|
157
|
+
this.shadowRoot
|
|
158
|
+
.querySelectorAll("[data-monster-role=entry]")
|
|
159
|
+
.forEach((entry) => {
|
|
160
|
+
entry.classList.remove("selected");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
value = String(value);
|
|
164
|
+
const index = findEntryIndex.call(this, value);
|
|
165
|
+
if (index === -1) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const currentNode = this.shadowRoot.querySelector(
|
|
170
|
+
"[data-monster-insert-reference=entries-" + index + "]",
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (!currentNode) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const allowParentSelect = this.getOption("features.selectParents") === true;
|
|
178
|
+
if (currentEntry?.["has-children"] === true && allowParentSelect) {
|
|
179
|
+
applySelection.call(this, currentEntry, Number(index), currentNode);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
currentNode.click();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find an entry by value.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} value
|
|
190
|
+
* @return {Object|null}
|
|
191
|
+
*/
|
|
192
|
+
findEntry(value) {
|
|
193
|
+
const index = findEntryIndex.call(this, String(value));
|
|
194
|
+
if (index === -1) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
entry: this.getOption("entries." + index),
|
|
199
|
+
index,
|
|
200
|
+
node: getEntryNode.call(this, index),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Open a node by value.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} value
|
|
208
|
+
* @return {void}
|
|
209
|
+
*/
|
|
210
|
+
openEntry(value) {
|
|
211
|
+
toggleEntryState.call(this, String(value), "open");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Expand a node and all its descendants by value.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} value
|
|
218
|
+
* @return {void}
|
|
219
|
+
*/
|
|
220
|
+
expandEntry(value) {
|
|
221
|
+
expandEntry.call(this, String(value));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Collapse a node and all its descendants by value.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} value
|
|
228
|
+
* @return {void}
|
|
229
|
+
*/
|
|
230
|
+
collapseEntry(value) {
|
|
231
|
+
collapseEntry.call(this, String(value));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Close a node by value.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} value
|
|
238
|
+
* @return {void}
|
|
239
|
+
*/
|
|
240
|
+
closeEntry(value) {
|
|
241
|
+
toggleEntryState.call(this, String(value), "close");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Show a node by value.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} value
|
|
248
|
+
* @return {void}
|
|
249
|
+
*/
|
|
250
|
+
showEntry(value) {
|
|
251
|
+
setEntryVisibility.call(this, String(value), "visible");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Hide a node by value.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} value
|
|
258
|
+
* @return {void}
|
|
259
|
+
*/
|
|
260
|
+
hideEntry(value) {
|
|
261
|
+
setEntryVisibility.call(this, String(value), "hidden");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove a node by value.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} value
|
|
268
|
+
* @return {void}
|
|
269
|
+
*/
|
|
270
|
+
removeEntry(value) {
|
|
271
|
+
removeEntry.call(this, String(value));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Insert a node.
|
|
276
|
+
*
|
|
277
|
+
* @param {Object} entry
|
|
278
|
+
* @param {string|null} parentValue
|
|
279
|
+
* @return {void}
|
|
280
|
+
*/
|
|
281
|
+
insertEntry(entry, parentValue = null) {
|
|
282
|
+
insertEntry.call(this, entry, parentValue);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Insert a node before a reference entry.
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} entry
|
|
289
|
+
* @param {string} referenceValue
|
|
290
|
+
* @return {void}
|
|
291
|
+
*/
|
|
292
|
+
insertEntryBefore(entry, referenceValue) {
|
|
293
|
+
insertEntryAt.call(this, entry, String(referenceValue), "before");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Insert a node after a reference entry.
|
|
298
|
+
*
|
|
299
|
+
* @param {Object} entry
|
|
300
|
+
* @param {string} referenceValue
|
|
301
|
+
* @return {void}
|
|
302
|
+
*/
|
|
303
|
+
insertEntryAfter(entry, referenceValue) {
|
|
304
|
+
insertEntryAt.call(this, entry, String(referenceValue), "after");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
function initEventHandler() {
|
|
312
|
+
this[openEntryEventHandlerSymbol] = (event) => {
|
|
313
|
+
const container = findTargetElementFromEvent(
|
|
314
|
+
event,
|
|
315
|
+
ATTRIBUTE_ROLE,
|
|
316
|
+
"entry",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!(container instanceof HTMLElement)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const index = container
|
|
324
|
+
.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE)
|
|
325
|
+
.split("-")
|
|
326
|
+
.pop();
|
|
327
|
+
|
|
328
|
+
const currentEntry = this.getOption("entries." + index);
|
|
329
|
+
|
|
330
|
+
const allowParentSelect = this.getOption("features.selectParents") === true;
|
|
331
|
+
if (currentEntry["has-children"] === false) {
|
|
332
|
+
const href = currentEntry.href;
|
|
333
|
+
const isNavigation = isString(href) && href !== "";
|
|
334
|
+
const doNavigate = getAction.call(this, ["onnavigate", "navigate"]);
|
|
335
|
+
|
|
336
|
+
if (isNavigation) {
|
|
337
|
+
let allowNavigation = true;
|
|
338
|
+
if (isFunction(doNavigate)) {
|
|
339
|
+
const result = doNavigate.call(this, currentEntry, index, event);
|
|
340
|
+
if (result === false) {
|
|
341
|
+
allowNavigation = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const navEvent = dispatchEntryEvent.call(
|
|
346
|
+
this,
|
|
347
|
+
"monster-html-tree-menu-navigate",
|
|
348
|
+
{
|
|
349
|
+
entry: currentEntry,
|
|
350
|
+
index,
|
|
351
|
+
event,
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
if (navEvent.defaultPrevented) {
|
|
355
|
+
allowNavigation = false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!allowNavigation) {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (isAnchorEvent(event) === false) {
|
|
364
|
+
window.location.assign(href);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
applySelection.call(this, currentEntry, Number(index), container, event);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const currentState = this.getOption("entries." + index + ".state");
|
|
374
|
+
const newState = currentState === "close" ? "open" : "close";
|
|
375
|
+
if (newState === "open") {
|
|
376
|
+
const entry = this.getOption("entries." + index);
|
|
377
|
+
if (shouldLazyLoad.call(this, entry)) {
|
|
378
|
+
void ensureEntryLoaded
|
|
379
|
+
.call(this, Number(index), event)
|
|
380
|
+
.then((loaded) => {
|
|
381
|
+
if (loaded) {
|
|
382
|
+
applyEntryState.call(this, Number(index), newState, event);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
applyEntryState.call(this, Number(index), newState, event);
|
|
390
|
+
|
|
391
|
+
if (allowParentSelect) {
|
|
392
|
+
applySelection.call(this, currentEntry, Number(index), container, event);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const types = this.getOption("toggleEventType", ["click"]);
|
|
397
|
+
for (const [, type] of Object.entries(types)) {
|
|
398
|
+
this.shadowRoot.addEventListener(type, this[openEntryEventHandlerSymbol]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Import menu entries from HTML list.
|
|
404
|
+
*
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
function importEntries() {
|
|
408
|
+
const rootList = this.querySelector("ul,ol");
|
|
409
|
+
if (!(rootList instanceof HTMLElement)) {
|
|
410
|
+
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "no list found");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
rootList.setAttribute("hidden", "");
|
|
415
|
+
rootList.classList.add("hidden");
|
|
416
|
+
|
|
417
|
+
const entries = [];
|
|
418
|
+
buildEntriesFromList.call(this, rootList, 0, false, entries);
|
|
419
|
+
this.setOption("entries", entries);
|
|
420
|
+
rebuildEntryIndex.call(this);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @private
|
|
425
|
+
* @param {HTMLElement} list
|
|
426
|
+
* @param {number} level
|
|
427
|
+
* @param {boolean} ancestorHidden
|
|
428
|
+
* @param {Array} entries
|
|
429
|
+
*/
|
|
430
|
+
function buildEntriesFromList(list, level, ancestorHidden, entries) {
|
|
431
|
+
const lazyConfig = this.getOption("lazy", {});
|
|
432
|
+
const lazyEnabled = lazyConfig?.enabled !== false;
|
|
433
|
+
const lazyAttribute = lazyConfig?.attribute || "";
|
|
434
|
+
const items = Array.from(list.children).filter((node) =>
|
|
435
|
+
node.matches("li"),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
for (const li of items) {
|
|
439
|
+
const childList = li.querySelector(":scope > ul,:scope > ol");
|
|
440
|
+
const hasChildList =
|
|
441
|
+
childList instanceof HTMLElement &&
|
|
442
|
+
Array.from(childList.children).some((node) => node.matches("li"));
|
|
443
|
+
const endpoint =
|
|
444
|
+
lazyEnabled && isString(lazyAttribute) && lazyAttribute !== ""
|
|
445
|
+
? li.getAttribute(lazyAttribute)
|
|
446
|
+
: null;
|
|
447
|
+
const hasLazyEndpoint = isString(endpoint) && endpoint !== "";
|
|
448
|
+
const hasChildren = hasChildList || hasLazyEndpoint;
|
|
449
|
+
|
|
450
|
+
const label = getLabelHtml(li, childList);
|
|
451
|
+
const value = getEntryValue(li);
|
|
452
|
+
const href = getEntryHref(li, childList);
|
|
453
|
+
const liHidden = isHiddenElement(li);
|
|
454
|
+
const childHidden = childList ? isHiddenElement(childList) : false;
|
|
455
|
+
let state = "close";
|
|
456
|
+
if (hasChildren && !(childHidden || hasLazyEndpoint)) {
|
|
457
|
+
state = "open";
|
|
458
|
+
}
|
|
459
|
+
const visibility = ancestorHidden || liHidden ? "hidden" : "visible";
|
|
460
|
+
|
|
461
|
+
entries.push({
|
|
462
|
+
value,
|
|
463
|
+
label,
|
|
464
|
+
icon: "",
|
|
465
|
+
intend: level,
|
|
466
|
+
state,
|
|
467
|
+
visibility,
|
|
468
|
+
["has-children"]: hasChildren,
|
|
469
|
+
href,
|
|
470
|
+
["lazy-endpoint"]: hasLazyEndpoint ? endpoint : null,
|
|
471
|
+
["lazy-loaded"]: hasLazyEndpoint ? hasChildList : true,
|
|
472
|
+
["lazy-loading"]: false,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (hasChildList) {
|
|
476
|
+
buildEntriesFromList.call(
|
|
477
|
+
this,
|
|
478
|
+
childList,
|
|
479
|
+
level + 1,
|
|
480
|
+
ancestorHidden || liHidden || state === "close",
|
|
481
|
+
entries,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @private
|
|
489
|
+
* @param {HTMLElement} element
|
|
490
|
+
* @return {boolean}
|
|
491
|
+
*/
|
|
492
|
+
function isHiddenElement(element) {
|
|
493
|
+
return (
|
|
494
|
+
element.hasAttribute("hidden") ||
|
|
495
|
+
element.classList.contains("hidden") ||
|
|
496
|
+
element.classList.contains("hide") ||
|
|
497
|
+
element.classList.contains("none")
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @private
|
|
503
|
+
* @param {HTMLElement} li
|
|
504
|
+
* @param {HTMLElement|null} childList
|
|
505
|
+
* @return {string}
|
|
506
|
+
*/
|
|
507
|
+
function getLabelHtml(li, childList) {
|
|
508
|
+
const clone = li.cloneNode(true);
|
|
509
|
+
if (childList) {
|
|
510
|
+
const nested = clone.querySelector("ul,ol");
|
|
511
|
+
if (nested) {
|
|
512
|
+
nested.remove();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const html = clone.innerHTML.trim();
|
|
516
|
+
if (html !== "") {
|
|
517
|
+
return html;
|
|
518
|
+
}
|
|
519
|
+
return li.textContent.trim();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @private
|
|
524
|
+
* @param {HTMLElement} li
|
|
525
|
+
* @param {HTMLElement|null} childList
|
|
526
|
+
* @return {string|null}
|
|
527
|
+
*/
|
|
528
|
+
function getEntryHref(li, childList) {
|
|
529
|
+
const clone = li.cloneNode(true);
|
|
530
|
+
if (childList) {
|
|
531
|
+
const nested = clone.querySelector("ul,ol");
|
|
532
|
+
if (nested) {
|
|
533
|
+
nested.remove();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const anchor = clone.querySelector("a[href]");
|
|
537
|
+
const href = anchor?.getAttribute("href") || li.getAttribute("href");
|
|
538
|
+
if (isString(href) && href !== "") {
|
|
539
|
+
return href;
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @private
|
|
546
|
+
* @param {HTMLElement} li
|
|
547
|
+
* @return {string}
|
|
548
|
+
*/
|
|
549
|
+
function getEntryValue(li) {
|
|
550
|
+
const value =
|
|
551
|
+
li.getAttribute("data-monster-value") ||
|
|
552
|
+
li.getAttribute("data-value") ||
|
|
553
|
+
li.getAttribute("id");
|
|
554
|
+
if (isString(value) && value !== "") {
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
return new ID().toString();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @private
|
|
562
|
+
* @param {string} value
|
|
563
|
+
* @return {number}
|
|
564
|
+
*/
|
|
565
|
+
function findEntryIndex(value) {
|
|
566
|
+
if (this[entryIndexSymbol].has(value)) {
|
|
567
|
+
return this[entryIndexSymbol].get(value);
|
|
568
|
+
}
|
|
569
|
+
const entries = this.getOption("entries", []);
|
|
570
|
+
return entries.findIndex((entry) => String(entry.value) === value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
function rebuildEntryIndex() {
|
|
577
|
+
this[entryIndexSymbol].clear();
|
|
578
|
+
const entries = this.getOption("entries", []);
|
|
579
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
580
|
+
this[entryIndexSymbol].set(String(entries[i].value), i);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @private
|
|
586
|
+
* @param {number} index
|
|
587
|
+
* @return {HTMLElement|null}
|
|
588
|
+
*/
|
|
589
|
+
function getEntryNode(index) {
|
|
590
|
+
return this.shadowRoot.querySelector(
|
|
591
|
+
`[data-monster-insert-reference=entries-${index}]`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* @private
|
|
597
|
+
* @param {Object} entry
|
|
598
|
+
* @return {Object}
|
|
599
|
+
*/
|
|
600
|
+
function normalizeEntry(entry) {
|
|
601
|
+
const rawEndpoint =
|
|
602
|
+
entry?.endpoint || entry?.["lazy-endpoint"] || entry?.lazyEndpoint;
|
|
603
|
+
const endpoint =
|
|
604
|
+
isString(rawEndpoint) && rawEndpoint !== "" ? rawEndpoint : null;
|
|
605
|
+
const hasChildren = entry?.["has-children"] === true || endpoint !== null;
|
|
606
|
+
return {
|
|
607
|
+
value: String(entry?.value || new ID().toString()),
|
|
608
|
+
label: entry?.label || "",
|
|
609
|
+
icon: entry?.icon || "",
|
|
610
|
+
href: entry?.href || null,
|
|
611
|
+
intend: 0,
|
|
612
|
+
state: "close",
|
|
613
|
+
visibility: "visible",
|
|
614
|
+
["has-children"]: hasChildren,
|
|
615
|
+
["lazy-endpoint"]: endpoint,
|
|
616
|
+
["lazy-loaded"]: endpoint === null,
|
|
617
|
+
["lazy-loading"]: false,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @private
|
|
623
|
+
* @param {Array} entries
|
|
624
|
+
* @param {number} index
|
|
625
|
+
* @return {number}
|
|
626
|
+
*/
|
|
627
|
+
function getSubtreeEndIndex(entries, index) {
|
|
628
|
+
const targetIntend = entries[index].intend;
|
|
629
|
+
let end = index + 1;
|
|
630
|
+
while (end < entries.length && entries[end].intend > targetIntend) {
|
|
631
|
+
end += 1;
|
|
632
|
+
}
|
|
633
|
+
return end;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @private
|
|
638
|
+
* @param {string} value
|
|
639
|
+
* @param {string} state
|
|
640
|
+
*/
|
|
641
|
+
function toggleEntryState(value, state) {
|
|
642
|
+
const index = findEntryIndex.call(this, value);
|
|
643
|
+
if (index === -1) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (state === "open") {
|
|
647
|
+
const entry = this.getOption("entries." + index);
|
|
648
|
+
if (shouldLazyLoad.call(this, entry)) {
|
|
649
|
+
void ensureEntryLoaded.call(this, index).then((loaded) => {
|
|
650
|
+
if (loaded) {
|
|
651
|
+
applyEntryState.call(this, index, state);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
applyEntryState.call(this, index, state);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* @private
|
|
662
|
+
* @param {number} index
|
|
663
|
+
* @param {string} state
|
|
664
|
+
* @param {Event|undefined} event
|
|
665
|
+
*/
|
|
666
|
+
function applyEntryState(index, state, event) {
|
|
667
|
+
const entry = this.getOption("entries." + index);
|
|
668
|
+
if (!entry || entry["has-children"] === false) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const actionName = state === "open" ? "onexpand" : "oncollapse";
|
|
673
|
+
const doAction = getAction.call(this, [actionName, state]);
|
|
674
|
+
if (isFunction(doAction)) {
|
|
675
|
+
doAction.call(this, entry, index, event);
|
|
676
|
+
}
|
|
677
|
+
const eventName =
|
|
678
|
+
state === "open"
|
|
679
|
+
? "monster-html-tree-menu-expand"
|
|
680
|
+
: "monster-html-tree-menu-collapse";
|
|
681
|
+
fireCustomEvent(this, eventName, {
|
|
682
|
+
entry,
|
|
683
|
+
index,
|
|
684
|
+
event,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
this.setOption("entries." + index + ".state", state);
|
|
688
|
+
const newVisibility = state === "open" ? "visible" : "hidden";
|
|
689
|
+
|
|
690
|
+
const entryNode = this.shadowRoot.querySelector(
|
|
691
|
+
"[data-monster-insert-reference=entries-" + index + "]",
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
if (entryNode?.hasAttribute(ATTRIBUTE_INTEND)) {
|
|
695
|
+
const intend = entryNode.getAttribute(ATTRIBUTE_INTEND);
|
|
696
|
+
let ref = entryNode.nextElementSibling;
|
|
697
|
+
const childIntend = parseInt(intend) + 1;
|
|
698
|
+
|
|
699
|
+
const cmp = (a, b) => {
|
|
700
|
+
if (state === "open") {
|
|
701
|
+
return a === b;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return a >= b;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
while (ref?.hasAttribute(ATTRIBUTE_INTEND)) {
|
|
708
|
+
const refIntend = ref.getAttribute(ATTRIBUTE_INTEND);
|
|
709
|
+
|
|
710
|
+
if (!cmp(Number.parseInt(refIntend), childIntend)) {
|
|
711
|
+
if (refIntend === intend) {
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
ref = ref.nextElementSibling;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const refIndex = ref
|
|
719
|
+
.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE)
|
|
720
|
+
.split("-")
|
|
721
|
+
.pop();
|
|
722
|
+
|
|
723
|
+
this.setOption("entries." + refIndex + ".visibility", newVisibility);
|
|
724
|
+
|
|
725
|
+
if (state === "close") {
|
|
726
|
+
this.setOption("entries." + refIndex + ".state", "close");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
ref = ref.nextElementSibling;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* @private
|
|
736
|
+
* @param {string} value
|
|
737
|
+
* @param {string} visibility
|
|
738
|
+
*/
|
|
739
|
+
function setEntryVisibility(value, visibility) {
|
|
740
|
+
const index = findEntryIndex.call(this, value);
|
|
741
|
+
if (index === -1) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const entries = this.getOption("entries", []);
|
|
746
|
+
const target = entries[index];
|
|
747
|
+
if (!target) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
this.setOption("entries." + index + ".visibility", visibility);
|
|
752
|
+
const intend = target.intend;
|
|
753
|
+
for (let i = index + 1; i < entries.length; i += 1) {
|
|
754
|
+
if (entries[i].intend <= intend) {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
if (visibility === "hidden") {
|
|
758
|
+
this.setOption("entries." + i + ".visibility", "hidden");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* @private
|
|
765
|
+
* @param {string} value
|
|
766
|
+
*/
|
|
767
|
+
function expandEntry(value) {
|
|
768
|
+
const index = findEntryIndex.call(this, value);
|
|
769
|
+
if (index === -1) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const entry = this.getOption("entries." + index);
|
|
774
|
+
if (!entry || entry["has-children"] === false) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (shouldLazyLoad.call(this, entry)) {
|
|
779
|
+
void ensureEntryLoaded.call(this, index).then((loaded) => {
|
|
780
|
+
if (loaded) {
|
|
781
|
+
expandEntryByIndex.call(this, index);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
expandEntryByIndex.call(this, index);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* @private
|
|
792
|
+
* @param {string} value
|
|
793
|
+
*/
|
|
794
|
+
function collapseEntry(value) {
|
|
795
|
+
const index = findEntryIndex.call(this, value);
|
|
796
|
+
if (index === -1) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const entry = this.getOption("entries." + index);
|
|
801
|
+
if (!entry || entry["has-children"] === false) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const doAction = getAction.call(this, ["oncollapse", "close"]);
|
|
806
|
+
if (isFunction(doAction)) {
|
|
807
|
+
doAction.call(this, entry, index);
|
|
808
|
+
}
|
|
809
|
+
fireCustomEvent(this, "monster-html-tree-menu-collapse", {
|
|
810
|
+
entry,
|
|
811
|
+
index,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
this.setOption("entries." + index + ".state", "close");
|
|
815
|
+
|
|
816
|
+
const entries = this.getOption("entries", []);
|
|
817
|
+
const end = getSubtreeEndIndex(entries, index);
|
|
818
|
+
for (let i = index + 1; i < end; i += 1) {
|
|
819
|
+
this.setOption("entries." + i + ".visibility", "hidden");
|
|
820
|
+
if (entries[i]["has-children"]) {
|
|
821
|
+
this.setOption("entries." + i + ".state", "close");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* @private
|
|
828
|
+
* @param {string} value
|
|
829
|
+
*/
|
|
830
|
+
function removeEntry(value) {
|
|
831
|
+
const entries = this.getOption("entries", []);
|
|
832
|
+
const index = findEntryIndex.call(this, value);
|
|
833
|
+
if (index === -1) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const targetIntend = entries[index].intend;
|
|
838
|
+
let parentIndex = -1;
|
|
839
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
840
|
+
if (entries[i].intend < targetIntend) {
|
|
841
|
+
parentIndex = i;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const newEntries = [];
|
|
846
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
847
|
+
if (i === index) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (i > index && entries[i].intend > targetIntend) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
newEntries.push(entries[i]);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (parentIndex !== -1) {
|
|
857
|
+
const parentValue = String(entries[parentIndex].value);
|
|
858
|
+
const parentEntryIndex = newEntries.findIndex(
|
|
859
|
+
(entry) => String(entry.value) === parentValue,
|
|
860
|
+
);
|
|
861
|
+
if (parentEntryIndex !== -1) {
|
|
862
|
+
const parentIntend = newEntries[parentEntryIndex].intend;
|
|
863
|
+
const hasChildren = newEntries.some(
|
|
864
|
+
(entry, idx) =>
|
|
865
|
+
idx > parentEntryIndex && entry.intend > parentIntend,
|
|
866
|
+
);
|
|
867
|
+
if (!hasChildren) {
|
|
868
|
+
newEntries[parentEntryIndex] = Object.assign(
|
|
869
|
+
{},
|
|
870
|
+
newEntries[parentEntryIndex],
|
|
871
|
+
{
|
|
872
|
+
["has-children"]: false,
|
|
873
|
+
state: "close",
|
|
874
|
+
},
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
this.setOption("entries", newEntries);
|
|
881
|
+
rebuildEntryIndex.call(this);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* @private
|
|
886
|
+
* @param {Object} entry
|
|
887
|
+
* @param {string|null} parentValue
|
|
888
|
+
*/
|
|
889
|
+
function insertEntry(entry, parentValue) {
|
|
890
|
+
const entries = this.getOption("entries", []);
|
|
891
|
+
const newEntry = normalizeEntry(entry);
|
|
892
|
+
|
|
893
|
+
let insertIndex = entries.length;
|
|
894
|
+
if (isString(parentValue) && parentValue !== "") {
|
|
895
|
+
const parentIndex = findEntryIndex.call(this, parentValue);
|
|
896
|
+
if (parentIndex !== -1) {
|
|
897
|
+
const parent = entries[parentIndex];
|
|
898
|
+
newEntry.intend = parent.intend + 1;
|
|
899
|
+
newEntry.visibility = parent.state === "open" ? "visible" : "hidden";
|
|
900
|
+
entries[parentIndex] = Object.assign({}, parent, {
|
|
901
|
+
["has-children"]: true,
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
insertIndex = parentIndex + 1;
|
|
905
|
+
while (
|
|
906
|
+
insertIndex < entries.length &&
|
|
907
|
+
entries[insertIndex].intend > parent.intend
|
|
908
|
+
) {
|
|
909
|
+
insertIndex += 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const nextEntries = [
|
|
915
|
+
...entries.slice(0, insertIndex),
|
|
916
|
+
newEntry,
|
|
917
|
+
...entries.slice(insertIndex),
|
|
918
|
+
];
|
|
919
|
+
this.setOption("entries", nextEntries);
|
|
920
|
+
rebuildEntryIndex.call(this);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* @private
|
|
925
|
+
* @param {Object} entry
|
|
926
|
+
* @param {string} referenceValue
|
|
927
|
+
* @param {string} position
|
|
928
|
+
*/
|
|
929
|
+
function insertEntryAt(entry, referenceValue, position) {
|
|
930
|
+
const entries = this.getOption("entries", []);
|
|
931
|
+
const referenceIndex = findEntryIndex.call(this, referenceValue);
|
|
932
|
+
|
|
933
|
+
if (referenceIndex === -1) {
|
|
934
|
+
insertEntry.call(this, entry, null);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const referenceEntry = entries[referenceIndex];
|
|
939
|
+
const newEntry = normalizeEntry(entry);
|
|
940
|
+
newEntry.intend = referenceEntry.intend;
|
|
941
|
+
newEntry.visibility = referenceEntry.visibility;
|
|
942
|
+
|
|
943
|
+
let parentIndex = -1;
|
|
944
|
+
for (let i = referenceIndex - 1; i >= 0; i -= 1) {
|
|
945
|
+
if (entries[i].intend < referenceEntry.intend) {
|
|
946
|
+
parentIndex = i;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (parentIndex !== -1) {
|
|
952
|
+
entries[parentIndex] = Object.assign({}, entries[parentIndex], {
|
|
953
|
+
["has-children"]: true,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
let insertIndex = referenceIndex;
|
|
958
|
+
if (position === "after") {
|
|
959
|
+
insertIndex = getSubtreeEndIndex(entries, referenceIndex);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const nextEntries = [
|
|
963
|
+
...entries.slice(0, insertIndex),
|
|
964
|
+
newEntry,
|
|
965
|
+
...entries.slice(insertIndex),
|
|
966
|
+
];
|
|
967
|
+
|
|
968
|
+
this.setOption("entries", nextEntries);
|
|
969
|
+
rebuildEntryIndex.call(this);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* @private
|
|
974
|
+
* @param {number} index
|
|
975
|
+
*/
|
|
976
|
+
function expandEntryByIndex(index) {
|
|
977
|
+
const entry = this.getOption(`entries.${index}`);
|
|
978
|
+
if (!entry || entry["has-children"] === false) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const doAction = getAction.call(this, ["onexpand", "open"]);
|
|
983
|
+
if (isFunction(doAction)) {
|
|
984
|
+
doAction.call(this, entry, index);
|
|
985
|
+
}
|
|
986
|
+
fireCustomEvent(this, "monster-html-tree-menu-expand", {
|
|
987
|
+
entry,
|
|
988
|
+
index,
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
this.setOption("entries." + index + ".state", "open");
|
|
992
|
+
this.setOption("entries." + index + ".visibility", "visible");
|
|
993
|
+
|
|
994
|
+
const entries = this.getOption("entries", []);
|
|
995
|
+
const end = getSubtreeEndIndex(entries, index);
|
|
996
|
+
for (let i = index + 1; i < end; i += 1) {
|
|
997
|
+
this.setOption("entries." + i + ".visibility", "visible");
|
|
998
|
+
if (entries[i]["has-children"]) {
|
|
999
|
+
this.setOption("entries." + i + ".state", "open");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* @private
|
|
1006
|
+
* @param {Object} entry
|
|
1007
|
+
* @return {boolean}
|
|
1008
|
+
*/
|
|
1009
|
+
function shouldLazyLoad(entry) {
|
|
1010
|
+
if (!entry) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
const lazyConfig = this.getOption("lazy", {});
|
|
1014
|
+
if (lazyConfig?.enabled === false) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
const endpoint = entry["lazy-endpoint"];
|
|
1018
|
+
return (
|
|
1019
|
+
isString(endpoint) &&
|
|
1020
|
+
endpoint !== "" &&
|
|
1021
|
+
entry["lazy-loaded"] !== true &&
|
|
1022
|
+
entry["lazy-loading"] !== true
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* @private
|
|
1028
|
+
* @param {number} index
|
|
1029
|
+
* @param {Event|undefined} event
|
|
1030
|
+
* @return {Promise<boolean>}
|
|
1031
|
+
*/
|
|
1032
|
+
async function ensureEntryLoaded(index, event) {
|
|
1033
|
+
const entry = this.getOption(`entries.${index}`);
|
|
1034
|
+
if (!shouldLazyLoad.call(this, entry)) {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const beforeLoadAction = getAction.call(this, ["onlazyload"]);
|
|
1039
|
+
if (isFunction(beforeLoadAction)) {
|
|
1040
|
+
beforeLoadAction.call(this, entry, index, event);
|
|
1041
|
+
}
|
|
1042
|
+
fireCustomEvent(this, "monster-html-tree-menu-lazy-load", {
|
|
1043
|
+
entry,
|
|
1044
|
+
index,
|
|
1045
|
+
event,
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
this.setOption(`entries.${index}.lazy-loading`, true);
|
|
1049
|
+
|
|
1050
|
+
const endpoint = entry["lazy-endpoint"];
|
|
1051
|
+
const lazyConfig = this.getOption("lazy", {});
|
|
1052
|
+
const fetchOptions = Object.assign(
|
|
1053
|
+
{
|
|
1054
|
+
method: "GET",
|
|
1055
|
+
},
|
|
1056
|
+
lazyConfig?.fetchOptions || {},
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
let response = null;
|
|
1060
|
+
try {
|
|
1061
|
+
response = await fetch(endpoint, fetchOptions);
|
|
1062
|
+
if (!response.ok) {
|
|
1063
|
+
throw new Error("failed to load lazy entry");
|
|
1064
|
+
}
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
this.setOption(`entries.${index}.lazy-loading`, false);
|
|
1067
|
+
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
|
|
1068
|
+
const errorAction = getAction.call(this, ["onlazyerror"]);
|
|
1069
|
+
if (isFunction(errorAction)) {
|
|
1070
|
+
errorAction.call(this, entry, index, event);
|
|
1071
|
+
}
|
|
1072
|
+
fireCustomEvent(this, "monster-html-tree-menu-lazy-error", {
|
|
1073
|
+
entry,
|
|
1074
|
+
index,
|
|
1075
|
+
event,
|
|
1076
|
+
});
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
let html = "";
|
|
1081
|
+
try {
|
|
1082
|
+
html = await response.text();
|
|
1083
|
+
} catch (e) {
|
|
1084
|
+
this.setOption(`entries.${index}.lazy-loading`, false);
|
|
1085
|
+
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
|
|
1086
|
+
const errorAction = getAction.call(this, ["onlazyerror"]);
|
|
1087
|
+
if (isFunction(errorAction)) {
|
|
1088
|
+
errorAction.call(this, entry, index, event);
|
|
1089
|
+
}
|
|
1090
|
+
fireCustomEvent(this, "monster-html-tree-menu-lazy-error", {
|
|
1091
|
+
entry,
|
|
1092
|
+
index,
|
|
1093
|
+
event,
|
|
1094
|
+
});
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const list = parseLazyList(html);
|
|
1099
|
+
if (!list) {
|
|
1100
|
+
this.setOption(`entries.${index}.lazy-loading`, false);
|
|
1101
|
+
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "lazy entry has no list");
|
|
1102
|
+
const errorAction = getAction.call(this, ["onlazyerror"]);
|
|
1103
|
+
if (isFunction(errorAction)) {
|
|
1104
|
+
errorAction.call(this, entry, index, event);
|
|
1105
|
+
}
|
|
1106
|
+
fireCustomEvent(this, "monster-html-tree-menu-lazy-error", {
|
|
1107
|
+
entry,
|
|
1108
|
+
index,
|
|
1109
|
+
event,
|
|
1110
|
+
});
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const childEntries = [];
|
|
1115
|
+
buildEntriesFromList.call(this, list, entry.intend + 1, false, childEntries);
|
|
1116
|
+
|
|
1117
|
+
const entries = this.getOption("entries", []);
|
|
1118
|
+
const insertIndex = getSubtreeEndIndex(entries, index);
|
|
1119
|
+
const updatedEntry = Object.assign({}, entries[index], {
|
|
1120
|
+
["lazy-loaded"]: true,
|
|
1121
|
+
["lazy-loading"]: false,
|
|
1122
|
+
["has-children"]: childEntries.length > 0,
|
|
1123
|
+
state: childEntries.length > 0 ? "open" : "close",
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
const nextEntries = [
|
|
1127
|
+
...entries.slice(0, insertIndex),
|
|
1128
|
+
...childEntries,
|
|
1129
|
+
...entries.slice(insertIndex),
|
|
1130
|
+
];
|
|
1131
|
+
nextEntries[index] = updatedEntry;
|
|
1132
|
+
|
|
1133
|
+
this.setOption("entries", nextEntries);
|
|
1134
|
+
rebuildEntryIndex.call(this);
|
|
1135
|
+
|
|
1136
|
+
const loadedEntry = this.getOption(`entries.${index}`);
|
|
1137
|
+
const loadedAction = getAction.call(this, ["onlazyloaded"]);
|
|
1138
|
+
if (isFunction(loadedAction)) {
|
|
1139
|
+
loadedAction.call(this, loadedEntry, index, event);
|
|
1140
|
+
}
|
|
1141
|
+
fireCustomEvent(this, "monster-html-tree-menu-lazy-loaded", {
|
|
1142
|
+
entry: loadedEntry,
|
|
1143
|
+
index,
|
|
1144
|
+
event,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
return childEntries.length > 0;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* @private
|
|
1152
|
+
* @param {string} html
|
|
1153
|
+
* @return {HTMLElement|null}
|
|
1154
|
+
*/
|
|
1155
|
+
function parseLazyList(html) {
|
|
1156
|
+
if (!isString(html) || html.trim() === "") {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const parser = new DOMParser();
|
|
1161
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
1162
|
+
const list = doc.querySelector("ul,ol");
|
|
1163
|
+
if (list) {
|
|
1164
|
+
return list;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const items = Array.from(doc.body.children).filter((node) =>
|
|
1168
|
+
node.matches("li"),
|
|
1169
|
+
);
|
|
1170
|
+
if (items.length === 0) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const fallback = document.createElement("ul");
|
|
1175
|
+
for (const item of items) {
|
|
1176
|
+
fallback.appendChild(document.importNode(item, true));
|
|
1177
|
+
}
|
|
1178
|
+
return fallback;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* @private
|
|
1183
|
+
* @param {string[]} names
|
|
1184
|
+
* @return {Function|null}
|
|
1185
|
+
*/
|
|
1186
|
+
function getAction(names) {
|
|
1187
|
+
for (const name of names) {
|
|
1188
|
+
const action = this.getOption(`actions.${name}`);
|
|
1189
|
+
if (isFunction(action)) {
|
|
1190
|
+
return action;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* @private
|
|
1198
|
+
* @param {Event} event
|
|
1199
|
+
* @return {boolean}
|
|
1200
|
+
*/
|
|
1201
|
+
function isAnchorEvent(event) {
|
|
1202
|
+
if (!event || typeof event.composedPath !== "function") {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
const path = event.composedPath();
|
|
1206
|
+
for (const node of path) {
|
|
1207
|
+
if (node instanceof HTMLAnchorElement && node.hasAttribute("href")) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* @private
|
|
1216
|
+
* @param {string} type
|
|
1217
|
+
* @param {Object} detail
|
|
1218
|
+
* @return {CustomEvent}
|
|
1219
|
+
*/
|
|
1220
|
+
function dispatchEntryEvent(type, detail) {
|
|
1221
|
+
const event = new CustomEvent(type, {
|
|
1222
|
+
bubbles: true,
|
|
1223
|
+
cancelable: true,
|
|
1224
|
+
composed: true,
|
|
1225
|
+
detail,
|
|
1226
|
+
});
|
|
1227
|
+
this.dispatchEvent(event);
|
|
1228
|
+
return event;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* @private
|
|
1233
|
+
* @param {Object} entry
|
|
1234
|
+
* @param {number} index
|
|
1235
|
+
* @param {HTMLElement} container
|
|
1236
|
+
* @param {Event|undefined} event
|
|
1237
|
+
*/
|
|
1238
|
+
function applySelection(entry, index, container, event) {
|
|
1239
|
+
this.shadowRoot
|
|
1240
|
+
.querySelectorAll("[data-monster-role=entry].selected")
|
|
1241
|
+
.forEach((node) => {
|
|
1242
|
+
node.classList.remove("selected");
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
let intend = entry.intend;
|
|
1246
|
+
if (intend > 0) {
|
|
1247
|
+
let ref = container.previousElementSibling;
|
|
1248
|
+
while (ref?.hasAttribute(ATTRIBUTE_INTEND)) {
|
|
1249
|
+
const i = Number.parseInt(ref.getAttribute(ATTRIBUTE_INTEND));
|
|
1250
|
+
|
|
1251
|
+
if (Number.isNaN(i)) {
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (i < intend) {
|
|
1256
|
+
ref.classList.add("selected");
|
|
1257
|
+
if (i === 0) {
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
intend = i;
|
|
1261
|
+
}
|
|
1262
|
+
ref = ref.previousElementSibling;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
container.classList.add("selected");
|
|
1267
|
+
|
|
1268
|
+
const doSelect = getAction.call(this, ["onselect", "select"]);
|
|
1269
|
+
if (isFunction(doSelect)) {
|
|
1270
|
+
doSelect.call(this, entry, index, event);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
fireCustomEvent(this, "monster-html-tree-menu-select", {
|
|
1274
|
+
entry,
|
|
1275
|
+
index,
|
|
1276
|
+
event,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* @private
|
|
1282
|
+
* @return {string}
|
|
1283
|
+
*/
|
|
1284
|
+
function getTemplate() {
|
|
1285
|
+
// language=HTML
|
|
1286
|
+
return `
|
|
1287
|
+
<slot></slot>
|
|
1288
|
+
|
|
1289
|
+
<template id="entries">
|
|
1290
|
+
<div data-monster-role="entry"
|
|
1291
|
+
data-monster-attributes="
|
|
1292
|
+
data-monster-intend path:entries.intend,
|
|
1293
|
+
data-monster-state path:entries.state,
|
|
1294
|
+
data-monster-visibility path:entries.visibility,
|
|
1295
|
+
data-monster-filtered path:entries.filtered,
|
|
1296
|
+
data-monster-has-children path:entries.has-children">
|
|
1297
|
+
<div data-monster-role="button"
|
|
1298
|
+
data-monster-attributes="
|
|
1299
|
+
value path:entries.value | tostring
|
|
1300
|
+
" tabindex="0">
|
|
1301
|
+
<div data-monster-role="status-badges"></div>
|
|
1302
|
+
<div data-monster-role="icon" data-monster-replace="path:entries.icon"></div>
|
|
1303
|
+
<div data-monster-replace="path:entries.label"
|
|
1304
|
+
part="entry-label"
|
|
1305
|
+
data-monster-attributes="class static:id"></div>
|
|
1306
|
+
</div>
|
|
1307
|
+
</template>
|
|
1308
|
+
|
|
1309
|
+
<div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control">
|
|
1310
|
+
<div part="entries" data-monster-role="entries"
|
|
1311
|
+
data-monster-insert="entries path:entries"
|
|
1312
|
+
tabindex="-1"></div>
|
|
1313
|
+
</div>
|
|
1314
|
+
`;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
registerCustomElement(HtmlTreeMenu);
|