@schukai/monster 4.90.0 → 4.91.1

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.
@@ -0,0 +1,1138 @@
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_TEMPLATE_PREFIX,
21
+ ATTRIBUTE_UPDATER_INSERT,
22
+ } from "../../dom/constants.mjs";
23
+ import { CustomControl } from "../../dom/customcontrol.mjs";
24
+ import {
25
+ assembleMethodSymbol,
26
+ registerCustomElement,
27
+ } from "../../dom/customelement.mjs";
28
+ import { fireCustomEvent } from "../../dom/events.mjs";
29
+ import { addErrorAttribute } from "../../dom/error.mjs";
30
+ import { getDocumentTheme } from "../../dom/theme.mjs";
31
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
32
+ import { Pathfinder } from "../../data/pathfinder.mjs";
33
+ import { datasourceLinkedElementSymbol } from "../datatable/util.mjs";
34
+ import { Observer } from "../../types/observer.mjs";
35
+ import { isArray, isNumber, isObject, isString } from "../../types/is.mjs";
36
+ import { ID } from "../../types/id.mjs";
37
+ import { clone } from "../../util/clone.mjs";
38
+ import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs";
39
+ import { RepeatFieldSetStyleSheet } from "./stylesheet/repeat-field-set.mjs";
40
+ import { RepeatFieldSetItemsStyleSheet } from "./stylesheet/repeat-field-set-items.mjs";
41
+
42
+ export { RepeatFieldSet };
43
+
44
+ /**
45
+ * @private
46
+ * @type {symbol}
47
+ */
48
+ const addButtonSymbol = Symbol("addButton");
49
+
50
+ /**
51
+ * @private
52
+ * @type {symbol}
53
+ */
54
+ const removeButtonSymbol = Symbol("removeButton");
55
+
56
+ /**
57
+ * @private
58
+ * @type {symbol}
59
+ */
60
+ const itemSlotSymbol = Symbol("itemSlot");
61
+
62
+ /**
63
+ * @private
64
+ * @type {symbol}
65
+ */
66
+ const itemsContainerSymbol = Symbol("itemsContainer");
67
+ const itemDefaultsSymbol = Symbol("itemDefaults");
68
+
69
+ /**
70
+ * A repeatable field-set control that manages array data.
71
+ *
72
+ * @fragments /fragments/components/form/repeat-field-set/
73
+ *
74
+ * @since 3.78.0
75
+ * @summary A repeatable field-set control
76
+ * @fires monster-repeat-add
77
+ * @fires monster-repeat-remove
78
+ * @fires monster-repeat-change
79
+ */
80
+ class RepeatFieldSet extends CustomControl {
81
+ /**
82
+ * This method is called by the `instanceof` operator.
83
+ * @return {symbol}
84
+ */
85
+ static get [instanceSymbol]() {
86
+ return Symbol.for(
87
+ "@schukai/monster/components/form/repeat-field-set@@instance",
88
+ );
89
+ }
90
+
91
+ /**
92
+ *
93
+ * @return {RepeatFieldSet}
94
+ */
95
+ [assembleMethodSymbol]() {
96
+ super[assembleMethodSymbol]();
97
+ initControlReferences.call(this);
98
+ ensureItemsStyleSheet.call(this);
99
+ initTemplateBridge.call(this);
100
+ syncOptionData.call(this);
101
+ initEventHandler.call(this);
102
+ initObservers.call(this);
103
+ updateColumns.call(this);
104
+ normalizeOnLoad.call(this);
105
+ syncButtons.call(this);
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
111
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
112
+ *
113
+ * @property {Object} templates Template definitions
114
+ * @property {string} templates.main Main template
115
+ * @property {Object} labels Label definitions
116
+ * @property {string} labels.add Add label
117
+ * @property {string} labels.remove Remove label
118
+ * @property {Object} classes Class definitions
119
+ * @property {string} classes.content Content class
120
+ * @property {string} path Data path relative to the form record
121
+ * @property {number|null} min Minimum items
122
+ * @property {number|null} max Maximum items
123
+ * @property {boolean} disabled Disabled state
124
+ */
125
+ get defaults() {
126
+ return Object.assign({}, super.defaults, {
127
+ templates: {
128
+ main: getTemplate(),
129
+ },
130
+ labels: getTranslations(),
131
+ classes: {
132
+ content: "collapse-alignment-no-padding",
133
+ },
134
+ features: {
135
+ multipleColumns: true,
136
+ },
137
+ path: null,
138
+ min: null,
139
+ max: null,
140
+ disabled: false,
141
+ value: null,
142
+ });
143
+ }
144
+
145
+ /**
146
+ *
147
+ * @return {string}
148
+ */
149
+ static getTag() {
150
+ return "monster-repeat-field-set";
151
+ }
152
+
153
+ /**
154
+ *
155
+ * @return {CSSStyleSheet[]}
156
+ */
157
+ static getCSSStyleSheet() {
158
+ return [FieldSetStyleSheet, RepeatFieldSetStyleSheet];
159
+ }
160
+
161
+ /**
162
+ * The current value of the control.
163
+ *
164
+ * @return {string}
165
+ */
166
+ get value() {
167
+ return this.getOption("value");
168
+ }
169
+
170
+ /**
171
+ * Set the value of the control.
172
+ *
173
+ * @param {string} value
174
+ * @return {void}
175
+ */
176
+ set value(value) {
177
+ this.setOption("value", value);
178
+ try {
179
+ this?.setFormValue(this.value);
180
+ } catch (e) {
181
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * @private
188
+ * @returns {object}
189
+ */
190
+ function getTranslations() {
191
+ const locale = getLocaleOfDocument();
192
+ switch (locale.language) {
193
+ case "de":
194
+ return { add: "Hinzufügen", remove: "Entfernen" };
195
+ case "fr":
196
+ return { add: "Ajouter", remove: "Supprimer" };
197
+ case "es":
198
+ return { add: "Agregar", remove: "Eliminar" };
199
+ case "zh":
200
+ return { add: "添加", remove: "删除" };
201
+ case "hi":
202
+ return { add: "जोड़ें", remove: "हटाएं" };
203
+ case "bn":
204
+ return { add: "যোগ করুন", remove: "মুছে ফেলুন" };
205
+ case "pt":
206
+ return { add: "Adicionar", remove: "Remover" };
207
+ case "ru":
208
+ return { add: "Добавить", remove: "Удалить" };
209
+ case "ja":
210
+ return { add: "追加", remove: "削除" };
211
+ case "pa":
212
+ return { add: "ਸ਼ਾਮਲ ਕਰੋ", remove: "ਹਟਾਓ" };
213
+ case "mr":
214
+ return { add: "जोडा", remove: "काढा" };
215
+ case "it":
216
+ return { add: "Aggiungi", remove: "Rimuovi" };
217
+ case "nl":
218
+ return { add: "Toevoegen", remove: "Verwijderen" };
219
+ case "sv":
220
+ return { add: "Lägg till", remove: "Ta bort" };
221
+ case "pl":
222
+ return { add: "Dodaj", remove: "Usuń" };
223
+ case "da":
224
+ return { add: "Tilføj", remove: "Fjern" };
225
+ case "fi":
226
+ return { add: "Lisää", remove: "Poista" };
227
+ case "no":
228
+ return { add: "Legg til", remove: "Fjern" };
229
+ case "cs":
230
+ return { add: "Přidat", remove: "Odebrat" };
231
+ default:
232
+ return { add: "Add", remove: "Remove" };
233
+ }
234
+ }
235
+
236
+ /**
237
+ * @private
238
+ */
239
+ function initControlReferences() {
240
+ this[addButtonSymbol] = this.shadowRoot.querySelector(
241
+ `[${ATTRIBUTE_ROLE}="add"]`,
242
+ );
243
+ this[removeButtonSymbol] = this.shadowRoot.querySelector(
244
+ `[${ATTRIBUTE_ROLE}="remove"]`,
245
+ );
246
+ this[itemSlotSymbol] = this.shadowRoot.querySelector('slot[name="item"]');
247
+ this[itemsContainerSymbol] = ensureItemsContainer.call(this);
248
+ }
249
+
250
+ /**
251
+ * @private
252
+ * @return {HTMLElement}
253
+ */
254
+ function ensureItemsContainer() {
255
+ let container = this.querySelector(`[${ATTRIBUTE_ROLE}="items"]`);
256
+ if (!(container instanceof HTMLElement)) {
257
+ container = document.createElement("div");
258
+ container.setAttribute(ATTRIBUTE_ROLE, "items");
259
+ this.appendChild(container);
260
+ }
261
+
262
+ container.classList.add("grid-span-full");
263
+ container.setAttribute("slot", "items");
264
+ return container;
265
+ }
266
+
267
+ /**
268
+ * @private
269
+ * @return {HTMLStyleElement}
270
+ */
271
+ function ensureItemsStyleSheet() {
272
+ const root = this.getRootNode?.() || this.ownerDocument || document;
273
+ if (!root || !("adoptedStyleSheets" in root)) {
274
+ return;
275
+ }
276
+ const sheets = root.adoptedStyleSheets || [];
277
+ if (!sheets.includes(RepeatFieldSetItemsStyleSheet)) {
278
+ root.adoptedStyleSheets = [...sheets, RepeatFieldSetItemsStyleSheet];
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @private
284
+ */
285
+ function initTemplateBridge() {
286
+ const slot = this[itemSlotSymbol];
287
+ if (slot) {
288
+ slot.addEventListener("slotchange", () => {
289
+ this[itemDefaultsSymbol] = null;
290
+ syncInsertDefinition.call(this);
291
+ });
292
+ }
293
+ syncInsertDefinition.call(this);
294
+ }
295
+
296
+ /**
297
+ * @private
298
+ */
299
+ function initEventHandler() {
300
+ if (this[addButtonSymbol]) {
301
+ this[addButtonSymbol].addEventListener("click", (event) => {
302
+ event.stopPropagation();
303
+ addItem.call(this);
304
+ });
305
+ }
306
+
307
+ if (this[removeButtonSymbol]) {
308
+ this[removeButtonSymbol].addEventListener("click", (event) => {
309
+ event.stopPropagation();
310
+ removeItem.call(this);
311
+ });
312
+ }
313
+ }
314
+
315
+ /**
316
+ * @private
317
+ */
318
+ function updateColumns() {
319
+ if (this.getOption("features.multipleColumns") !== true) {
320
+ this.classList.remove("multiple-columns");
321
+ return;
322
+ }
323
+
324
+ this.classList.add("multiple-columns");
325
+ }
326
+
327
+ /**
328
+ * @private
329
+ */
330
+ function initObservers() {
331
+ this.attachObserver(
332
+ new Observer(() => {
333
+ syncInsertDefinition.call(this);
334
+ syncButtons.call(this);
335
+ }),
336
+ );
337
+
338
+ const form = this.closest("monster-form");
339
+ if (form && typeof form.attachObserver === "function") {
340
+ form.attachObserver(
341
+ new Observer(() => {
342
+ syncOptionData.call(this);
343
+ syncButtons.call(this);
344
+ }),
345
+ );
346
+ }
347
+
348
+ const datasource = getDatasourceElement.call(this);
349
+ if (datasource?.datasource?.attachObserver) {
350
+ datasource.datasource.attachObserver(
351
+ new Observer(() => {
352
+ syncOptionData.call(this);
353
+ syncButtons.call(this);
354
+ }),
355
+ );
356
+ }
357
+
358
+ if (datasource) {
359
+ datasource.addEventListener("monster-datasource-fetched", () => {
360
+ syncOptionData.call(this);
361
+ normalizeOnLoad.call(this);
362
+ });
363
+ }
364
+ }
365
+
366
+ /**
367
+ * @private
368
+ * @return {string|null}
369
+ */
370
+ function getPathOption() {
371
+ const path = this.getOption("path");
372
+ if (isString(path) && path.trim() !== "") {
373
+ return path.trim();
374
+ }
375
+ return null;
376
+ }
377
+
378
+ /**
379
+ * @private
380
+ * @param {unknown} value
381
+ * @return {number|null}
382
+ */
383
+ function normalizeLimit(value) {
384
+ if (value === null || value === undefined) {
385
+ return null;
386
+ }
387
+ if (isNumber(value) && Number.isFinite(value)) {
388
+ return value;
389
+ }
390
+ if (isString(value)) {
391
+ const trimmed = value.trim();
392
+ if (trimmed === "" || trimmed.toLowerCase() === "null") {
393
+ return null;
394
+ }
395
+ const numeric = Number(trimmed);
396
+ return Number.isFinite(numeric) ? numeric : null;
397
+ }
398
+ return null;
399
+ }
400
+
401
+ /**
402
+ * @private
403
+ * @return {number|null}
404
+ */
405
+ function getMinLimit() {
406
+ return normalizeLimit(this.getOption("min"));
407
+ }
408
+
409
+ /**
410
+ * @private
411
+ * @return {number|null}
412
+ */
413
+ function getMaxLimit() {
414
+ return normalizeLimit(this.getOption("max"));
415
+ }
416
+
417
+ /**
418
+ * @private
419
+ * @return {HTMLTemplateElement|null}
420
+ */
421
+ function getItemTemplate() {
422
+ const slot = this[itemSlotSymbol];
423
+ if (slot && typeof slot.assignedElements === "function") {
424
+ const assigned = slot.assignedElements({ flatten: true });
425
+ for (const element of assigned) {
426
+ if (element instanceof HTMLTemplateElement) {
427
+ return element;
428
+ }
429
+ }
430
+ }
431
+
432
+ const fallback = this.querySelector('template[slot="item"]');
433
+ if (fallback instanceof HTMLTemplateElement) {
434
+ return fallback;
435
+ }
436
+
437
+ return null;
438
+ }
439
+
440
+ /**
441
+ * @private
442
+ * @return {string}
443
+ */
444
+ function ensureTemplatePrefix() {
445
+ const container = this[itemsContainerSymbol];
446
+ if (!(container instanceof HTMLElement)) {
447
+ throw new Error("RepeatFieldSet is missing the items container.");
448
+ }
449
+
450
+ let prefix = container.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
451
+ if (!isString(prefix) || prefix.trim() === "") {
452
+ prefix = new ID("repeat-field").toString();
453
+ container.setAttribute(ATTRIBUTE_TEMPLATE_PREFIX, prefix);
454
+ }
455
+ return prefix;
456
+ }
457
+
458
+ /**
459
+ * @private
460
+ */
461
+ function syncInsertDefinition() {
462
+ const path = getPathOption.call(this);
463
+ if (!path) {
464
+ addErrorAttribute(this, "RepeatFieldSet requires a path option.");
465
+ return;
466
+ }
467
+
468
+ const container = this[itemsContainerSymbol];
469
+ if (!(container instanceof HTMLElement)) {
470
+ addErrorAttribute(this, "RepeatFieldSet is missing the items container.");
471
+ return;
472
+ }
473
+
474
+ const template = getItemTemplate.call(this);
475
+ if (!template) {
476
+ addErrorAttribute(
477
+ this,
478
+ 'RepeatFieldSet requires a <template slot="item"> definition.',
479
+ );
480
+ return;
481
+ }
482
+
483
+ const prefix = ensureTemplatePrefix.call(this);
484
+ const themeName = getDocumentTheme().getName();
485
+ const nextId = `${prefix}-item-${themeName}`;
486
+ if (template.id !== nextId) {
487
+ template.id = nextId;
488
+ }
489
+
490
+ const form = this.closest("monster-form");
491
+ let record = null;
492
+ if (form && typeof form.getOption === "function") {
493
+ const formData = form.getOption("data");
494
+ if (isObject(formData) || isArray(formData)) {
495
+ record = formData;
496
+ }
497
+ }
498
+ if (!record) {
499
+ record = resolveFormRecord(form, getDatasourceElement.call(this));
500
+ }
501
+ const normalizedPath = normalizePathForRecord(path, record);
502
+ const dataPath = normalizedPath.startsWith("data.")
503
+ ? normalizedPath
504
+ : `data.${normalizedPath}`;
505
+ const insertValue = `item path:${dataPath}`;
506
+ if (container.getAttribute(ATTRIBUTE_UPDATER_INSERT) !== insertValue) {
507
+ container.setAttribute(ATTRIBUTE_UPDATER_INSERT, insertValue);
508
+ }
509
+
510
+ if (this.getAttribute(ATTRIBUTE_UPDATER_INSERT)) {
511
+ this.removeAttribute(ATTRIBUTE_UPDATER_INSERT);
512
+ }
513
+ }
514
+
515
+ /**
516
+ * @private
517
+ * @return {HTMLElement|null}
518
+ */
519
+ function getDatasourceElement() {
520
+ const form = this.closest("monster-form");
521
+ if (!form) {
522
+ return null;
523
+ }
524
+
525
+ if (form[datasourceLinkedElementSymbol]) {
526
+ return form[datasourceLinkedElementSymbol];
527
+ }
528
+
529
+ if (typeof form.getOption === "function") {
530
+ const selector = form.getOption("datasource.selector");
531
+ if (isString(selector) && selector.trim() !== "") {
532
+ return document.querySelector(selector);
533
+ }
534
+ }
535
+
536
+ return null;
537
+ }
538
+
539
+ /**
540
+ * @private
541
+ * @return {object|null}
542
+ */
543
+ function getTargetContext() {
544
+ const form = this.closest("monster-form");
545
+ const path = getPathOption.call(this);
546
+ if (!path) {
547
+ return null;
548
+ }
549
+
550
+ if (form && typeof form.getOption === "function") {
551
+ const formData = form.getOption("data");
552
+ if (isObject(formData) || isArray(formData)) {
553
+ const formPath = normalizePathForForm(path);
554
+ return { type: "form", target: form, path: formPath };
555
+ }
556
+ }
557
+
558
+ const datasource = getDatasourceElement.call(this);
559
+ if (datasource) {
560
+ const mappingData = form?.getOption?.("mapping.data");
561
+ const mappingIndex = form?.getOption?.("mapping.index");
562
+ const record = resolveFormRecord(form, datasource);
563
+ const normalizedPath = normalizePathForRecord(path, record);
564
+ let basePath = "";
565
+
566
+ if (isString(mappingData) && mappingData.trim() !== "") {
567
+ basePath = mappingData.trim();
568
+ }
569
+
570
+ if (mappingIndex !== null && mappingIndex !== undefined) {
571
+ const indexValue = String(mappingIndex);
572
+ basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
573
+ }
574
+
575
+ const finalPath = basePath
576
+ ? `${basePath}.${normalizedPath}`
577
+ : normalizedPath;
578
+ return { type: "datasource", target: datasource, path: finalPath };
579
+ }
580
+
581
+ if (form && typeof form.getOption === "function") {
582
+ return { type: "form", target: form, path };
583
+ }
584
+
585
+ return null;
586
+ }
587
+
588
+ /**
589
+ * @private
590
+ * @param {string} path
591
+ * @return {string}
592
+ */
593
+ function normalizePathForForm(path) {
594
+ if (!isString(path)) {
595
+ return path;
596
+ }
597
+
598
+ const trimmed = path.trim();
599
+ if (trimmed.startsWith("data.")) {
600
+ return trimmed;
601
+ }
602
+
603
+ return `data.${trimmed}`;
604
+ }
605
+
606
+ /**
607
+ * @private
608
+ * @param {string} path
609
+ * @param {object} record
610
+ * @return {string}
611
+ */
612
+ function normalizePathForRecord(path, record) {
613
+ if (!isString(path)) {
614
+ return path;
615
+ }
616
+
617
+ const trimmed = path.trim();
618
+ if (!trimmed.startsWith("data.")) {
619
+ return trimmed;
620
+ }
621
+
622
+ if (isObject(record?.data) || isArray(record?.data)) {
623
+ return trimmed;
624
+ }
625
+
626
+ return trimmed.slice(5);
627
+ }
628
+
629
+ /**
630
+ * @private
631
+ * @return {object}
632
+ */
633
+ function getCurrentDataSnapshot() {
634
+ const context = getTargetContext.call(this);
635
+ if (!context) {
636
+ return { context: null, data: {} };
637
+ }
638
+
639
+ let data;
640
+ if (context.type === "datasource") {
641
+ data = context.target?.data;
642
+ } else {
643
+ data = context.target?.getOption?.("data");
644
+ }
645
+
646
+ if (!isObject(data) && !isArray(data)) {
647
+ data = {};
648
+ }
649
+
650
+ return { context, data };
651
+ }
652
+
653
+ /**
654
+ * @private
655
+ * @return {Array}
656
+ */
657
+ function getCurrentItems() {
658
+ const { context, data } = getCurrentDataSnapshot.call(this);
659
+ if (!context) {
660
+ return [];
661
+ }
662
+
663
+ try {
664
+ if (context.type === "form") {
665
+ const value = context.target?.getOption?.(context.path);
666
+ return isArray(value) ? [...value] : [];
667
+ }
668
+
669
+ const value = new Pathfinder(data).getVia(context.path);
670
+ return isArray(value) ? [...value] : [];
671
+ } catch {
672
+ return [];
673
+ }
674
+ }
675
+
676
+ /**
677
+ * @private
678
+ * @return {object}
679
+ */
680
+ function buildEmptyItem() {
681
+ const defaults = getItemDefaults.call(this);
682
+ return clone(defaults);
683
+ }
684
+
685
+ /**
686
+ * @private
687
+ * @return {object}
688
+ */
689
+ function getItemDefaults() {
690
+ if (this[itemDefaultsSymbol]) {
691
+ return this[itemDefaultsSymbol];
692
+ }
693
+
694
+ const template = getItemTemplate.call(this);
695
+ const defaults = {};
696
+ if (!template?.content) {
697
+ this[itemDefaultsSymbol] = defaults;
698
+ return defaults;
699
+ }
700
+
701
+ const elements = template.content.querySelectorAll(
702
+ "[data-monster-bind],[data-monster-attributes]",
703
+ );
704
+
705
+ for (const element of elements) {
706
+ const bindAttr = element.getAttribute("data-monster-bind");
707
+ const attrAttr = element.getAttribute("data-monster-attributes");
708
+ const paths = new Set([
709
+ ...extractItemPaths(bindAttr),
710
+ ...extractItemPathsFromAttributes(attrAttr),
711
+ ]);
712
+
713
+ if (paths.size === 0) {
714
+ continue;
715
+ }
716
+
717
+ const value = getDefaultValueForElement(element);
718
+ for (const path of paths) {
719
+ try {
720
+ new Pathfinder(defaults).setVia(path, value);
721
+ } catch (error) {
722
+ addErrorAttribute(
723
+ this,
724
+ error?.message || "Failed to build default repeat item.",
725
+ );
726
+ }
727
+ }
728
+ }
729
+
730
+ this[itemDefaultsSymbol] = defaults;
731
+ return defaults;
732
+ }
733
+
734
+ /**
735
+ * @private
736
+ * @param {string|null} value
737
+ * @return {string[]}
738
+ */
739
+ function extractItemPaths(value) {
740
+ if (!isString(value)) {
741
+ return [];
742
+ }
743
+
744
+ const paths = [];
745
+ const regex = /path:item\.([A-Za-z0-9_.-]+)/g;
746
+ for (const match of value.matchAll(regex)) {
747
+ if (match[1]) {
748
+ paths.push(match[1]);
749
+ }
750
+ }
751
+ return paths;
752
+ }
753
+
754
+ /**
755
+ * @private
756
+ * @param {string|null} value
757
+ * @return {string[]}
758
+ */
759
+ function extractItemPathsFromAttributes(value) {
760
+ if (!isString(value)) {
761
+ return [];
762
+ }
763
+
764
+ const paths = [];
765
+ const entries = value.split(",");
766
+ for (const entry of entries) {
767
+ const def = entry.trim();
768
+ if (
769
+ def.startsWith("value ") ||
770
+ def.startsWith("checked ") ||
771
+ def.startsWith("selected ")
772
+ ) {
773
+ paths.push(...extractItemPaths(def));
774
+ }
775
+ }
776
+
777
+ return paths;
778
+ }
779
+
780
+ /**
781
+ * @private
782
+ * @param {Element} element
783
+ * @return {*}
784
+ */
785
+ function getDefaultValueForElement(element) {
786
+ const bindType = element.getAttribute("data-monster-bind-type");
787
+ const defaultValue = element.getAttribute("data-monster-defaultvalue");
788
+
789
+ if (defaultValue !== null) {
790
+ return coerceDefaultValue(defaultValue, bindType);
791
+ }
792
+
793
+ if (bindType === "boolean") {
794
+ return false;
795
+ }
796
+
797
+ if (bindType === "number") {
798
+ return null;
799
+ }
800
+
801
+ const tagName = element.tagName?.toLowerCase();
802
+ if (tagName === "monster-toggle-switch") {
803
+ return false;
804
+ }
805
+
806
+ if (tagName === "monster-select") {
807
+ const optionType = element.getAttribute("data-monster-option-type");
808
+ if (optionType === "checkbox" || element.hasAttribute("multiple")) {
809
+ return [];
810
+ }
811
+ return "";
812
+ }
813
+
814
+ if (tagName === "select" && element.hasAttribute("multiple")) {
815
+ return [];
816
+ }
817
+
818
+ const inputType = element.getAttribute("type");
819
+ if (inputType === "checkbox" || inputType === "radio") {
820
+ return false;
821
+ }
822
+
823
+ return "";
824
+ }
825
+
826
+ /**
827
+ * @private
828
+ * @param {string} value
829
+ * @param {string|null} bindType
830
+ * @return {*}
831
+ */
832
+ function coerceDefaultValue(value, bindType) {
833
+ if (bindType === "boolean") {
834
+ const normalized = value.trim().toLowerCase();
835
+ return normalized === "true" || normalized === "1";
836
+ }
837
+
838
+ if (bindType === "number") {
839
+ const numeric = Number(value);
840
+ return Number.isFinite(numeric) ? numeric : null;
841
+ }
842
+
843
+ return value;
844
+ }
845
+
846
+ /**
847
+ * @private
848
+ * @param {Array} items
849
+ * @return {Array}
850
+ */
851
+ function enforceLimits(items) {
852
+ let list = isArray(items) ? [...items] : [];
853
+ const min = getMinLimit.call(this);
854
+ const max = getMaxLimit.call(this);
855
+
856
+ if (isNumber(max) && Number.isFinite(max)) {
857
+ if (list.length > max) {
858
+ list = list.slice(0, max);
859
+ }
860
+ }
861
+
862
+ if (isNumber(min) && Number.isFinite(min)) {
863
+ while (list.length < min) {
864
+ list.push(buildEmptyItem.call(this));
865
+ }
866
+ }
867
+
868
+ return list;
869
+ }
870
+
871
+ /**
872
+ * @private
873
+ * @param {Array} items
874
+ */
875
+ function persistItems(items, options = {}) {
876
+ const { context, data } = getCurrentDataSnapshot.call(this);
877
+ if (!context) {
878
+ return;
879
+ }
880
+
881
+ const { syncDatasource = true } = options;
882
+
883
+ if (context.type === "form") {
884
+ try {
885
+ context.target.setOption(context.path, items);
886
+ } catch (error) {
887
+ addErrorAttribute(this, error?.message || "Failed to update form data.");
888
+ return;
889
+ }
890
+
891
+ if (syncDatasource) {
892
+ const datasourceContext = getDatasourceContext.call(this);
893
+ const datasourceData = datasourceContext?.target?.data;
894
+ if (
895
+ datasourceContext &&
896
+ (isObject(datasourceData) || isArray(datasourceData))
897
+ ) {
898
+ const nextData = clone(datasourceData);
899
+ try {
900
+ new Pathfinder(nextData).setVia(datasourceContext.path, items);
901
+ datasourceContext.target.data = nextData;
902
+ } catch (error) {
903
+ addErrorAttribute(
904
+ this,
905
+ error?.message || "Failed to update datasource data.",
906
+ );
907
+ return;
908
+ }
909
+ }
910
+ }
911
+ } else {
912
+ const nextData = clone(data);
913
+ try {
914
+ new Pathfinder(nextData).setVia(context.path, items);
915
+ } catch (error) {
916
+ addErrorAttribute(this, error?.message || "Failed to update path.");
917
+ return;
918
+ }
919
+
920
+ context.target.data = nextData;
921
+ }
922
+
923
+ syncOptionData.call(this);
924
+ syncButtons.call(this);
925
+ fireCustomEvent(this, "monster-repeat-change", {
926
+ detail: {
927
+ length: items.length,
928
+ path: context.path,
929
+ },
930
+ });
931
+ }
932
+
933
+ /**
934
+ * @private
935
+ */
936
+ function addItem() {
937
+ if (this.getOption("disabled") === true) {
938
+ return;
939
+ }
940
+
941
+ const items = getCurrentItems.call(this);
942
+ const max = getMaxLimit.call(this);
943
+ if (isNumber(max) && Number.isFinite(max) && items.length >= max) {
944
+ return;
945
+ }
946
+
947
+ const nextItems = [...items, buildEmptyItem.call(this)];
948
+ persistItems.call(this, nextItems);
949
+ fireCustomEvent(this, "monster-repeat-add", {
950
+ detail: { length: nextItems.length },
951
+ });
952
+ }
953
+
954
+ /**
955
+ * @private
956
+ */
957
+ function removeItem() {
958
+ if (this.getOption("disabled") === true) {
959
+ return;
960
+ }
961
+
962
+ const items = getCurrentItems.call(this);
963
+ const min = getMinLimit.call(this);
964
+ if (isNumber(min) && Number.isFinite(min) && items.length <= min) {
965
+ return;
966
+ }
967
+ if (items.length === 0) {
968
+ return;
969
+ }
970
+
971
+ const nextItems = items.slice(0, -1);
972
+ persistItems.call(this, nextItems);
973
+ fireCustomEvent(this, "monster-repeat-remove", {
974
+ detail: { length: nextItems.length },
975
+ });
976
+ }
977
+
978
+ /**
979
+ * @private
980
+ */
981
+ function normalizeOnLoad() {
982
+ const items = getCurrentItems.call(this);
983
+ const normalized = enforceLimits.call(this, items);
984
+
985
+ if (normalized.length !== items.length) {
986
+ persistItems.call(this, normalized, { syncDatasource: false });
987
+ return;
988
+ }
989
+
990
+ syncOptionData.call(this);
991
+ }
992
+
993
+ /**
994
+ * @private
995
+ */
996
+ function syncButtons() {
997
+ const items = getCurrentItems.call(this);
998
+ const min = getMinLimit.call(this);
999
+ const max = getMaxLimit.call(this);
1000
+ const isDisabled = this.getOption("disabled") === true;
1001
+
1002
+ if (this[addButtonSymbol]) {
1003
+ const reachedMax =
1004
+ isNumber(max) && Number.isFinite(max) && items.length >= max;
1005
+ this[addButtonSymbol].disabled = isDisabled || reachedMax;
1006
+ }
1007
+
1008
+ if (this[removeButtonSymbol]) {
1009
+ const reachedMin =
1010
+ isNumber(min) && Number.isFinite(min) && items.length <= min;
1011
+ this[removeButtonSymbol].disabled =
1012
+ isDisabled || reachedMin || items.length === 0;
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * @private
1018
+ */
1019
+ function syncOptionData() {
1020
+ const form = this.closest("monster-form");
1021
+ if (!form) {
1022
+ return;
1023
+ }
1024
+
1025
+ const formData = form.getOption?.("data");
1026
+ if (isObject(formData) || isArray(formData)) {
1027
+ this.setOption("data", clone(formData));
1028
+ } else {
1029
+ const datasource = getDatasourceElement.call(this);
1030
+ if (datasource) {
1031
+ const record = resolveFormRecord.call(this, form, datasource);
1032
+ if (record) {
1033
+ this.setOption("data", clone(record));
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ /**
1040
+ * @private
1041
+ * @param {HTMLElement} form
1042
+ * @param {HTMLElement} datasource
1043
+ * @return {object}
1044
+ */
1045
+ function resolveFormRecord(form, datasource) {
1046
+ let data = datasource?.data;
1047
+ if (!isObject(data) && !isArray(data)) {
1048
+ return {};
1049
+ }
1050
+
1051
+ const mappingData = form.getOption?.("mapping.data");
1052
+ if (isString(mappingData) && mappingData.trim() !== "") {
1053
+ try {
1054
+ data = new Pathfinder(data).getVia(mappingData.trim());
1055
+ } catch {
1056
+ data = {};
1057
+ }
1058
+ }
1059
+
1060
+ if (isObject(data)) {
1061
+ data = Object.values(data);
1062
+ }
1063
+
1064
+ const mappingIndex = form.getOption?.("mapping.index");
1065
+ if (mappingIndex !== null && mappingIndex !== undefined) {
1066
+ data = data?.[mappingIndex];
1067
+ }
1068
+
1069
+ if (!isObject(data) && !isArray(data)) {
1070
+ return {};
1071
+ }
1072
+
1073
+ return data;
1074
+ }
1075
+
1076
+ /**
1077
+ * @private
1078
+ * @return {{target: HTMLElement, path: string}|null}
1079
+ */
1080
+ function getDatasourceContext() {
1081
+ const form = this.closest("monster-form");
1082
+ const datasource = getDatasourceElement.call(this);
1083
+ const path = getPathOption.call(this);
1084
+ if (!datasource || !path) {
1085
+ return null;
1086
+ }
1087
+
1088
+ const mappingData = form?.getOption?.("mapping.data");
1089
+ const mappingIndex = form?.getOption?.("mapping.index");
1090
+ const record = resolveFormRecord(form, datasource);
1091
+ const normalizedPath = normalizePathForRecord(path, record);
1092
+ let basePath = "";
1093
+
1094
+ if (isString(mappingData) && mappingData.trim() !== "") {
1095
+ basePath = mappingData.trim();
1096
+ }
1097
+
1098
+ if (mappingIndex !== null && mappingIndex !== undefined) {
1099
+ const indexValue = String(mappingIndex);
1100
+ basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
1101
+ }
1102
+
1103
+ const finalPath = basePath ? `${basePath}.${normalizedPath}` : normalizedPath;
1104
+ return { target: datasource, path: finalPath };
1105
+ }
1106
+
1107
+ /**
1108
+ * @private
1109
+ * @return {string}
1110
+ */
1111
+ function getTemplate() {
1112
+ // language=HTML
1113
+ return `
1114
+ <div data-monster-role="control" part="control">
1115
+ <div data-monster-role="container" part="container">
1116
+ <div data-monster-attributes="class path:classes.content" part="content">
1117
+ <slot></slot>
1118
+ <slot name="items"></slot>
1119
+ </div>
1120
+ <div data-monster-role="actions" part="actions">
1121
+ <slot name="actions"></slot>
1122
+ <button type="button" data-monster-role="add" part="add">
1123
+ <span data-monster-replace="path:labels.add"></span>
1124
+ </button>
1125
+ <button type="button" data-monster-role="remove" part="remove">
1126
+ <span data-monster-replace="path:labels.remove"></span>
1127
+ </button>
1128
+ </div>
1129
+ <slot name="item"></slot>
1130
+ </div>
1131
+ </div>`;
1132
+ }
1133
+
1134
+ /**
1135
+ * @private
1136
+ * @return {string}
1137
+ */
1138
+ registerCustomElement(RepeatFieldSet);