@schukai/monster 4.43.5 → 4.43.7

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.
@@ -1,216 +1,444 @@
1
- /**
2
- * Copyright © schukai GmbH 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 schukai GmbH.
11
- */
12
-
13
1
  import { instanceSymbol } from "../../constants.mjs";
14
- import { addAttributeToken } from "../../dom/attributes.mjs";
2
+ import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
15
3
  import {
16
- ATTRIBUTE_ERRORMESSAGE,
17
- ATTRIBUTE_ROLE,
18
- } from "../../dom/constants.mjs";
19
- import { CustomControl } from "../../dom/customcontrol.mjs";
20
- import { CustomElement, getSlottedElements } from "../../dom/customelement.mjs";
21
- import {
22
- assembleMethodSymbol,
23
- registerCustomElement,
4
+ assembleMethodSymbol,
5
+ CustomElement,
6
+ getSlottedElements,
7
+ registerCustomElement,
24
8
  } from "../../dom/customelement.mjs";
25
- import { findTargetElementFromEvent, fireEvent } from "../../dom/events.mjs";
9
+ import {
10
+ findTargetElementFromEvent,
11
+ fireCustomEvent,
12
+ } from "../../dom/events.mjs";
26
13
  import { isFunction } from "../../types/is.mjs";
27
14
  import { WizardNavigationStyleSheet } from "./stylesheet/wizard-navigation.mjs";
28
- import { fireCustomEvent } from "../../dom/events.mjs";
29
- import { buildTree } from "../../data/buildtree.mjs";
30
- import { NodeRecursiveIterator } from "../../types/noderecursiveiterator.mjs";
31
- import { validateInstance } from "../../types/validate.mjs";
32
- import { Node } from "../../types/node.mjs";
33
- import { Formatter } from "../../text/formatter.mjs";
34
-
35
- export { WizardNavigation };
36
15
 
37
- /**
38
- * @private
39
- * @type {symbol}
40
- */
41
16
  const wizardNavigationElementSymbol = Symbol("wizardNavigationElement");
42
-
43
- /**
44
- * @private
45
- * @type {symbol}
46
- */
47
17
  const wizardNavigationListElementSymbol = Symbol("wizardNavigationListElement");
18
+ const currentStepIndexSymbol = Symbol("currentStepIndex");
19
+ const stepsSymbol = Symbol("steps");
20
+ const isTransitioningSymbol = Symbol("isTransitioning");
21
+ const currentSubStepIndexSymbol = Symbol("currentSubStepIndex");
48
22
 
49
23
  /**
50
- * A WizardNavigation
51
- *
52
- * @fragments /fragments/components/navigation/wizard-navigation/
24
+ * A WizardNavigation component to display progress and allow navigation through a series of steps,
25
+ * including support for nested sub-steps.
53
26
  *
27
+ * @fragments /fragments/components/navigation/wizard-navigation
54
28
  * @example /examples/components/navigation/wizard-navigation-simple
55
- *
56
- * Work in Progress, currently only the basic functionality is implemented.
57
- *
58
29
  * @since 4.26.0
59
30
  * @copyright schukai GmbH
60
- * @summary A beautiful WizardNavigation that can make your life easier and also looks good.
31
+ * @summary A vertical step-by-step navigation component for wizards.
32
+ * @fires monster-wizard-step-changed - Fired when the main active step changes.
33
+ * @fires monster-wizard-substep-changed - Fired when the active sub-step changes.
34
+ * @fires monster-wizard-completed - Fired when the entire wizard is marked as complete via completeAll().
61
35
  */
62
36
  class WizardNavigation extends CustomElement {
63
- /**
64
- * This method is called by the `instanceof` operator.
65
- * @returns {symbol}
66
- */
67
- static get [instanceSymbol]() {
68
- return Symbol.for(
69
- "@schukai/monster/components/navigation/wizard-navigation@@instance",
70
- );
71
- }
72
-
73
- /**
74
- *
75
- * @return {Components.Navigation.WizardNavigation
76
- */
77
- [assembleMethodSymbol]() {
78
- super[assembleMethodSymbol]();
79
- initControlReferences.call(this);
80
- initEventHandler.call(this);
81
- queueMicrotask(() => {
82
- importContent.call(this);
83
- });
84
- return this;
85
- }
86
-
87
- /**
88
- * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
89
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
90
- *
91
- * The individual configuration values can be found in the table.
92
- *
93
- * @property {Object} templates Template definitions
94
- * @property {string} templates.main Main template
95
- * @property {Object} labels Label definitions
96
- * @property {Object} actions Callbacks
97
- * @property {string} actions.click="throw Error" Callback when clicked
98
- * @property {Object} features Features
99
- * @property {Object} classes CSS classes
100
- * @property {boolean} disabled=false Disabled state
101
- */
102
- get defaults() {
103
- return Object.assign({}, super.defaults, {
104
- templates: {
105
- main: getTemplate(),
106
- },
107
- labels: {},
108
- classes: {},
109
- disabled: false,
110
- features: {},
111
- actions: {
112
- click: () => {
113
- throw new Error("the click action is not defined");
114
- },
115
- },
116
- });
117
- }
118
-
119
- /**
120
- * @return {string}
121
- */
122
- static getTag() {
123
- return "monster-wizard-navigation";
124
- }
125
-
126
- /**
127
- * @return {CSSStyleSheet[]}
128
- */
129
- static getCSSStyleSheet() {
130
- return [WizardNavigationStyleSheet];
131
- }
132
- }
37
+ static get [instanceSymbol]() {
38
+ return Symbol.for(
39
+ "@schukai/monster/components/navigation/wizard-navigation@@instance",
40
+ );
41
+ }
133
42
 
134
- /**
135
- * @private
136
- * @return {initEventHandler}
137
- * @fires monster-wizard-navigation-clicked
138
- */
139
- function initEventHandler() {
140
- const self = this;
141
- const element = this[wizardNavigationElementSymbol];
43
+ [assembleMethodSymbol]() {
44
+ super[assembleMethodSymbol]();
45
+ this[isTransitioningSymbol] = false;
46
+ this[currentStepIndexSymbol] = -1;
47
+ this[currentSubStepIndexSymbol] = -1;
48
+
49
+ initControlReferences.call(this);
50
+ initEventHandler.call(this);
51
+
52
+ queueMicrotask(() => {
53
+ importContent.call(this);
54
+ this.activate(0, 0, { force: true });
55
+ });
56
+
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
62
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
63
+ *
64
+ * The individual configuration values can be found in the table.
65
+ *
66
+ * @property {Object} templates Template definitions
67
+ * @property {string} templates.main Main template
68
+ * @property {Object} actions Action callbacks
69
+ * @property {function(number, number):(boolean|Promise<boolean>)} actions.beforeStepChange - Callback fired before a main step change. Must return `true` or a Promise that resolves to `true` to allow the transition. Parameters are `fromIndex` and `toIndex`.
70
+ * @property {function} actions.click - General click handler callback.
71
+ */
72
+ get defaults() {
73
+ return Object.assign({}, super.defaults, {
74
+ templates: {
75
+ main: getTemplate(),
76
+ },
77
+ actions: {
78
+ beforeStepChange: (fromIndex, toIndex) => true,
79
+ click: () => {},
80
+ },
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Navigates to the specified main step if the transition logic allows it.
86
+ * @param {number} index The index of the target step.
87
+ * @param {object} [options={}] Additional options.
88
+ * @param {boolean} [options.force=false] If true, the `beforeStepChange` callback is skipped.
89
+ * @returns {Promise<void>}
90
+ * @fires monster-wizard-step-changed
91
+ */
92
+ async goToStep(index, options = {}) {
93
+ if (typeof index !== "number") {
94
+ console.error("WizardNavigation.goToStep: index must be a number.");
95
+ return;
96
+ }
97
+
98
+ const { force = false } = options;
99
+ const stepsData = this[stepsSymbol];
100
+ const oldIndex = this[currentStepIndexSymbol];
101
+
102
+ if (
103
+ !stepsData ||
104
+ index < 0 ||
105
+ index >= stepsData.length ||
106
+ index === oldIndex ||
107
+ this[isTransitioningSymbol]
108
+ ) {
109
+ return;
110
+ }
111
+
112
+ this[isTransitioningSymbol] = true;
113
+
114
+ if (!force) {
115
+ const callback = this.getOption("actions.beforeStepChange");
116
+ if (isFunction(callback)) {
117
+ const isAllowed = await Promise.resolve(
118
+ callback.call(this, oldIndex, index),
119
+ );
120
+ if (!isAllowed) {
121
+ this[isTransitioningSymbol] = false;
122
+ return;
123
+ }
124
+ }
125
+ }
126
+
127
+ stepsData.forEach((stepData, i) => {
128
+ stepData.element.classList.remove("step-active");
129
+ stepData.element.setAttribute("aria-selected", "false");
130
+
131
+ if (stepData.subStepList) {
132
+ stepData.subStepList.style.maxHeight =
133
+ i === index ? stepData.subStepList.scrollHeight + "px" : "0px";
134
+ }
135
+
136
+ if (i < index) {
137
+ stepData.element.classList.add("step-completed");
138
+ stepData.subSteps.forEach((sub) =>
139
+ sub.classList.add("sub-item-completed"),
140
+ );
141
+ } else {
142
+ stepData.element.classList.remove("step-completed");
143
+ stepData.subSteps.forEach((sub) =>
144
+ sub.classList.remove("sub-item-completed"),
145
+ );
146
+ }
147
+ });
148
+
149
+ if (oldIndex !== -1 && stepsData[oldIndex]) {
150
+ stepsData[oldIndex].subSteps.forEach((sub) =>
151
+ sub.classList.remove("sub-item-active"),
152
+ );
153
+ }
154
+
155
+ this[currentSubStepIndexSymbol] = -1;
156
+ this[currentStepIndexSymbol] = index;
157
+
158
+ const currentStepData = stepsData[index];
159
+ currentStepData.element.classList.remove("step-completed");
160
+ currentStepData.element.classList.add("step-active");
161
+ currentStepData.element.setAttribute("aria-selected", "true");
162
+
163
+ fireCustomEvent(this, "monster-wizard-step-changed", {
164
+ newIndex: index,
165
+ oldIndex: oldIndex,
166
+ element: currentStepData.element,
167
+ });
168
+
169
+ this[isTransitioningSymbol] = false;
170
+ }
171
+
172
+ /**
173
+ * Activates a specific main step and sub-step.
174
+ * @param {number} mainIndex The index of the main step.
175
+ * @param {number} subIndex The index of the sub-step.
176
+ * @param {object} [options={}] Additional options.
177
+ * @param {boolean} [options.force=false] If true, the `beforeStepChange` callback is skipped.
178
+ * @returns {Promise<void>}
179
+ * @fires monster-wizard-substep-changed
180
+ */
181
+ async activate(mainIndex, subIndex, options = {}) {
182
+ if (typeof mainIndex !== "number" || typeof subIndex !== "number") {
183
+ console.error(
184
+ "WizardNavigation.activate: mainIndex and subIndex must be numbers.",
185
+ );
186
+ return;
187
+ }
188
+
189
+ await this.goToStep(mainIndex, options);
190
+
191
+ const stepsData = this[stepsSymbol];
192
+ if (!stepsData || !stepsData[mainIndex]) return;
142
193
 
143
- const type = "click";
194
+ const targetStep = stepsData[mainIndex];
195
+ if (
196
+ !targetStep.subSteps ||
197
+ subIndex < 0 ||
198
+ subIndex >= targetStep.subSteps.length
199
+ ) {
200
+ return;
201
+ }
144
202
 
145
- element.addEventListener(type, function (event) {
146
- const callback = self.getOption("actions.click");
203
+ stepsData.forEach((step) => {
204
+ step.subSteps.forEach((subStep) =>
205
+ subStep.classList.remove("sub-item-active"),
206
+ );
207
+ });
147
208
 
148
- fireCustomEvent(self, "monster-wizard-navigation-clicked", {
149
- element: self,
150
- });
209
+ for (let i = 0; i < subIndex; i++) {
210
+ if (targetStep.subSteps[i]) {
211
+ targetStep.subSteps[i].classList.add("sub-item-completed");
212
+ }
213
+ }
151
214
 
152
- if (!isFunction(callback)) {
153
- return;
154
- }
215
+ const targetSubStep = targetStep.subSteps[subIndex];
216
+ targetSubStep.classList.remove("sub-item-completed");
217
+ targetSubStep.classList.add("sub-item-active");
218
+ this[currentSubStepIndexSymbol] = subIndex;
155
219
 
156
- const element = findTargetElementFromEvent(
157
- event,
158
- ATTRIBUTE_ROLE,
159
- "control",
160
- );
220
+ fireCustomEvent(this, "monster-wizard-substep-changed", {
221
+ mainIndex,
222
+ subIndex,
223
+ element: targetSubStep,
224
+ });
225
+ }
161
226
 
162
- if (!(element instanceof Node && self.hasNode(element))) {
163
- return;
164
- }
227
+ /**
228
+ * Navigates to the next step or sub-step in sequence.
229
+ * @returns {Promise<void>}
230
+ */
231
+ async next() {
232
+ const mainIndex = this[currentStepIndexSymbol];
233
+ const subIndex = this[currentSubStepIndexSymbol];
234
+ const stepsData = this[stepsSymbol];
165
235
 
166
- callback.call(self, event);
167
- });
236
+ if (mainIndex === -1 || !stepsData || !stepsData[mainIndex]) return;
168
237
 
169
- return this;
238
+ const currentStepData = stepsData[mainIndex];
239
+ const hasSubSteps = currentStepData.subSteps.length > 0;
240
+
241
+ if (hasSubSteps && subIndex < currentStepData.subSteps.length - 1) {
242
+ await this.activate(mainIndex, subIndex + 1);
243
+ return;
244
+ }
245
+
246
+ const nextMainIndex = mainIndex + 1;
247
+ if (nextMainIndex < stepsData.length) {
248
+ if (stepsData[nextMainIndex].subSteps.length > 0) {
249
+ await this.activate(nextMainIndex, 0);
250
+ } else {
251
+ await this.goToStep(nextMainIndex);
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Navigates to the previous step or sub-step in sequence.
258
+ * @returns {Promise<void>}
259
+ */
260
+ async previous() {
261
+ const mainIndex = this[currentStepIndexSymbol];
262
+ const subIndex = this[currentSubStepIndexSymbol];
263
+ const stepsData = this[stepsSymbol];
264
+
265
+ if (subIndex > 0) {
266
+ await this.activate(mainIndex, subIndex - 1);
267
+ return;
268
+ }
269
+
270
+ const prevMainIndex = mainIndex - 1;
271
+ if (prevMainIndex >= 0) {
272
+ const prevStepData = stepsData[prevMainIndex];
273
+ if (prevStepData.subSteps.length > 0) {
274
+ await this.activate(prevMainIndex, prevStepData.subSteps.length - 1);
275
+ } else {
276
+ await this.goToStep(prevMainIndex);
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Marks the current step as completed and automatically navigates to the next step.
283
+ * @returns {Promise<void>}
284
+ */
285
+ async completeAndNext() {
286
+ const currentIndex = this[currentStepIndexSymbol];
287
+ const stepsData = this[stepsSymbol];
288
+
289
+ if (currentIndex === -1 || !stepsData || !stepsData[currentIndex]) {
290
+ return;
291
+ }
292
+
293
+ const currentStepData = stepsData[currentIndex];
294
+ currentStepData.element.classList.add("step-completed");
295
+ currentStepData.element.classList.remove("step-active");
296
+
297
+ currentStepData.subSteps.forEach((sub) => {
298
+ sub.classList.add("sub-item-completed");
299
+ sub.classList.remove("sub-item-active");
300
+ });
301
+
302
+ await this.next();
303
+ }
304
+
305
+ /**
306
+ * Marks the entire wizard workflow as completed.
307
+ * @fires monster-wizard-completed
308
+ */
309
+ completeAll() {
310
+ const stepsData = this[stepsSymbol];
311
+ if (!stepsData) return;
312
+
313
+ stepsData.forEach((stepData) => {
314
+ stepData.element.classList.add("step-completed");
315
+ stepData.element.classList.remove("step-active");
316
+ stepData.element.setAttribute("aria-selected", "false");
317
+
318
+ stepData.subSteps.forEach((sub) => {
319
+ sub.classList.add("sub-item-completed");
320
+ sub.classList.remove("sub-item-active");
321
+ });
322
+
323
+ if (stepData.subStepList) {
324
+ stepData.subStepList.style.maxHeight = "0px";
325
+ }
326
+ });
327
+
328
+ this[currentStepIndexSymbol] = -1;
329
+ this[currentSubStepIndexSymbol] = -1;
330
+
331
+ fireCustomEvent(this, "monster-wizard-completed", {
332
+ detail: { message: "All steps completed." },
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Gets the index of the currently active main step.
338
+ * @returns {number} The index of the current step, or -1 if none is active.
339
+ */
340
+ getCurrentStepIndex() {
341
+ return this[currentStepIndexSymbol];
342
+ }
343
+
344
+ /**
345
+ * Gets the index of the currently active sub-step within the main step.
346
+ * @returns {number} The index of the current sub-step, or -1 if none is active.
347
+ */
348
+ getCurrentSubStepIndex() {
349
+ return this[currentSubStepIndexSymbol];
350
+ }
351
+
352
+ static getTag() {
353
+ return "monster-wizard-navigation";
354
+ }
355
+
356
+ static getCSSStyleSheet() {
357
+ return [WizardNavigationStyleSheet];
358
+ }
170
359
  }
171
360
 
172
- /**
173
- * @private
174
- * @return {void}
175
- */
176
- function initControlReferences() {
177
- this[wizardNavigationElementSymbol] = this.shadowRoot.querySelector(
178
- `[${ATTRIBUTE_ROLE}="control"]`,
179
- );
361
+ /** @private */
362
+ function initEventHandler() {
363
+ const self = this;
364
+ this[wizardNavigationElementSymbol].addEventListener(
365
+ "click",
366
+ function (event) {
367
+ const targetStepElement = findTargetElementFromEvent(event, "li.step");
368
+ if (!targetStepElement) return;
369
+
370
+ const targetIndex = self[stepsSymbol].findIndex(
371
+ (stepData) => stepData.element === targetStepElement,
372
+ );
180
373
 
181
- this[wizardNavigationListElementSymbol] = this.shadowRoot.querySelector(
182
- `[${ATTRIBUTE_ROLE}="list"]`,
183
- );
374
+ if (targetIndex !== -1) {
375
+ self.goToStep(targetIndex);
376
+ }
377
+ },
378
+ );
184
379
  }
185
380
 
186
- /**
187
- * Import Menu Entries from dataset
188
- *
189
- * @since 1.0.0
190
- * @return {TreeMenu}
191
- * @throws {Error} map is not iterable
192
- * @private
193
- */
381
+ /** @private */
382
+ function initControlReferences() {
383
+ this[wizardNavigationElementSymbol] = this.shadowRoot.querySelector(
384
+ `[${ATTRIBUTE_ROLE}="control"]`,
385
+ );
386
+ this[wizardNavigationListElementSymbol] = this.shadowRoot.querySelector(
387
+ `[${ATTRIBUTE_ROLE}="list"]`,
388
+ );
389
+ }
390
+
391
+ /** @private */
194
392
  function importContent() {
195
- const elements = getSlottedElements.call(this, "ol.wizard-steps");
196
- elements.forEach((element) => {
197
- const clonedContent = element.cloneNode(true);
198
- this[wizardNavigationListElementSymbol].innerHTML = "";
199
- this[wizardNavigationListElementSymbol].appendChild(clonedContent);
200
- });
393
+ const elements = getSlottedElements.call(this, "ol.wizard-steps");
394
+ elements.forEach((element) => {
395
+ const clonedContent = element.cloneNode(true);
396
+ this[wizardNavigationListElementSymbol].innerHTML = "";
397
+ this[wizardNavigationListElementSymbol].appendChild(clonedContent);
398
+
399
+ const stepElements =
400
+ this[wizardNavigationListElementSymbol].querySelectorAll("li.step");
401
+
402
+ this[stepsSymbol] = Array.from(stepElements).map((stepEl) => {
403
+ const subStepList = stepEl.querySelector("ul");
404
+ const subSteps = subStepList
405
+ ? Array.from(subStepList.querySelectorAll("li"))
406
+ : [];
407
+
408
+ if (subStepList) {
409
+ subStepList.style.maxHeight = "0px";
410
+ subStepList.style.overflow = "hidden";
411
+ subStepList.style.transition = "max-height 0.4s ease-in-out";
412
+ }
413
+
414
+ return {
415
+ element: stepEl,
416
+ subStepList: subStepList,
417
+ subSteps: subSteps,
418
+ };
419
+ });
420
+
421
+ this[currentStepIndexSymbol] = -1;
422
+ this[currentSubStepIndexSymbol] = -1;
423
+
424
+ this[wizardNavigationListElementSymbol]
425
+ .querySelector("ol")
426
+ ?.setAttribute("role", "tablist");
427
+
428
+ this[stepsSymbol].forEach((stepData) => {
429
+ stepData.element.setAttribute("role", "tab");
430
+ stepData.element.setAttribute("aria-selected", "false");
431
+ });
432
+ });
201
433
  }
202
434
 
203
- /**
204
- * @private
205
- * @return {string}
206
- */
435
+ /** @private */
207
436
  function getTemplate() {
208
- // language=HTML
209
- return `
210
- <div data-monster-role="control" part="control">
211
- <slot style="display: none;"></slot>
212
- <div data-monster-role="list"></div>
213
- </div>`;
437
+ return `
438
+ <div data-monster-role="control" part="control">
439
+ <slot style="display: none;"></slot>
440
+ <div data-monster-role="list"></div>
441
+ </div>`;
214
442
  }
215
443
 
216
444
  registerCustomElement(WizardNavigation);
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.43.2");
159
+ monsterVersion = new Version("4.43.6");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("4.43.2")
10
+ monsterVersion = new Version("4.43.6")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -9,8 +9,8 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
12
- <h1 style='margin-bottom: 0.1em;'>Monster 4.43.2</h1>
13
- <div id="lastupdate" style='font-size:0.7em'>last update Fr 3. Okt 19:55:23 CEST 2025</div>
12
+ <h1 style='margin-bottom: 0.1em;'>Monster 4.43.6</h1>
13
+ <div id="lastupdate" style='font-size:0.7em'>last update Sa 11. Okt 18:38:25 CEST 2025</div>
14
14
  </div>
15
15
  <div id="mocha-errors"
16
16
  style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>