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