@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.62.0"}
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);