@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,173 lines (1,039 loc) • 27.5 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
ATTRIBUTE_ERRORMESSAGE,
ATTRIBUTE_ROLE,
ATTRIBUTE_TEMPLATE_PREFIX,
ATTRIBUTE_UPDATER_INSERT,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { getDocumentTheme } from "../../dom/theme.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { datasourceLinkedElementSymbol } from "../datatable/util.mjs";
import { Observer } from "../../types/observer.mjs";
import { isArray, isNumber, isObject, isString } from "../../types/is.mjs";
import { ID } from "../../types/id.mjs";
import { clone } from "../../util/clone.mjs";
import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs";
import { RepeatFieldSetStyleSheet } from "./stylesheet/repeat-field-set.mjs";
import { RepeatFieldSetItemsStyleSheet } from "./stylesheet/repeat-field-set-items.mjs";
export { RepeatFieldSet };
/**
* @private
* @type {symbol}
*/
const addButtonSymbol = Symbol("addButton");
/**
* @private
* @type {symbol}
*/
const removeButtonSymbol = Symbol("removeButton");
/**
* @private
* @type {symbol}
*/
const itemSlotSymbol = Symbol("itemSlot");
/**
* @private
* @type {symbol}
*/
const itemsContainerSymbol = Symbol("itemsContainer");
const itemDefaultsSymbol = Symbol("itemDefaults");
/**
* A repeatable field-set control that manages array data.
*
* @fragments /fragments/components/form/repeat-field-set/
*
* @since 3.78.0
* @summary A repeatable field-set control
* @fires monster-repeat-add
* @fires monster-repeat-remove
* @fires monster-repeat-change
*/
class RepeatFieldSet extends CustomControl {
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for(
"@schukai/monster/components/form/repeat-field-set@@instance",
);
}
/**
*
* @return {RepeatFieldSet}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
ensureItemsStyleSheet.call(this);
initTemplateBridge.call(this);
syncOptionData.call(this);
initEventHandler.call(this);
initObservers.call(this);
updateColumns.call(this);
normalizeOnLoad.call(this);
syncButtons.call(this);
return this;
}
/**
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} labels Label definitions
* @property {string} labels.add Add label
* @property {string} labels.remove Remove label
* @property {Object} classes Class definitions
* @property {string} classes.content Content class
* @property {string} path Data path relative to the form record
* @property {number|null} min Minimum items
* @property {number|null} max Maximum items
* @property {boolean} disabled Disabled state
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: getTranslations(),
classes: {
content: "collapse-alignment-no-padding",
},
features: {
multipleColumns: true,
},
path: null,
min: null,
max: null,
disabled: false,
value: null,
});
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-repeat-field-set";
}
/**
*
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [FieldSetStyleSheet, RepeatFieldSetStyleSheet];
}
/**
* The current value of the control.
*
* @return {string}
*/
get value() {
return this.getOption("value");
}
/**
* Set the value of the control.
*
* @param {string} value
* @return {void}
*/
set value(value) {
this.setOption("value", value);
try {
this?.setFormValue(this.value);
} catch (e) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
}
}
}
/**
* @private
* @returns {object}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return { add: "Hinzufügen", remove: "Entfernen" };
case "fr":
return { add: "Ajouter", remove: "Supprimer" };
case "es":
return { add: "Agregar", remove: "Eliminar" };
case "zh":
return { add: "添加", remove: "删除" };
case "hi":
return { add: "जोड़ें", remove: "हटाएं" };
case "bn":
return { add: "যোগ করুন", remove: "মুছে ফেলুন" };
case "pt":
return { add: "Adicionar", remove: "Remover" };
case "ru":
return { add: "Добавить", remove: "Удалить" };
case "ja":
return { add: "追加", remove: "削除" };
case "pa":
return { add: "ਸ਼ਾਮਲ ਕਰੋ", remove: "ਹਟਾਓ" };
case "mr":
return { add: "जोडा", remove: "काढा" };
case "it":
return { add: "Aggiungi", remove: "Rimuovi" };
case "nl":
return { add: "Toevoegen", remove: "Verwijderen" };
case "sv":
return { add: "Lägg till", remove: "Ta bort" };
case "pl":
return { add: "Dodaj", remove: "Usuń" };
case "da":
return { add: "Tilføj", remove: "Fjern" };
case "fi":
return { add: "Lisää", remove: "Poista" };
case "no":
return { add: "Legg til", remove: "Fjern" };
case "cs":
return { add: "Přidat", remove: "Odebrat" };
default:
return { add: "Add", remove: "Remove" };
}
}
/**
* @private
*/
function initControlReferences() {
this[addButtonSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="add"]`,
);
this[removeButtonSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="remove"]`,
);
this[itemSlotSymbol] = this.shadowRoot.querySelector('slot[name="item"]');
this[itemsContainerSymbol] = ensureItemsContainer.call(this);
}
/**
* @private
* @return {HTMLElement}
*/
function ensureItemsContainer() {
let container = this.querySelector(`[${ATTRIBUTE_ROLE}="items"]`);
if (!(container instanceof HTMLElement)) {
container = document.createElement("div");
container.setAttribute(ATTRIBUTE_ROLE, "items");
this.appendChild(container);
}
container.classList.add("grid-span-full");
container.setAttribute("slot", "items");
return container;
}
/**
* @private
* @return {HTMLStyleElement}
*/
function ensureItemsStyleSheet() {
const root = this.getRootNode?.() || this.ownerDocument || document;
if (!root || !("adoptedStyleSheets" in root)) {
return;
}
const sheets = root.adoptedStyleSheets || [];
if (!sheets.includes(RepeatFieldSetItemsStyleSheet)) {
root.adoptedStyleSheets = [...sheets, RepeatFieldSetItemsStyleSheet];
}
}
/**
* @private
*/
function initTemplateBridge() {
const slot = this[itemSlotSymbol];
if (slot) {
slot.addEventListener("slotchange", () => {
this[itemDefaultsSymbol] = null;
syncInsertDefinition.call(this);
});
}
syncInsertDefinition.call(this);
}
/**
* @private
*/
function initEventHandler() {
if (this[addButtonSymbol]) {
this[addButtonSymbol].addEventListener("click", (event) => {
event.stopPropagation();
addItem.call(this);
});
}
if (this[removeButtonSymbol]) {
this[removeButtonSymbol].addEventListener("click", (event) => {
event.stopPropagation();
removeItem.call(this);
});
}
}
/**
* @private
*/
function updateColumns() {
if (this.getOption("features.multipleColumns") !== true) {
this.classList.remove("multiple-columns");
return;
}
this.classList.add("multiple-columns");
}
/**
* @private
*/
function initObservers() {
this.attachObserver(
new Observer(() => {
syncInsertDefinition.call(this);
syncButtons.call(this);
}),
);
const form = this.closest("monster-form");
if (form && typeof form.attachObserver === "function") {
form.attachObserver(
new Observer(() => {
syncOptionData.call(this);
syncButtons.call(this);
}),
);
}
const datasource = getDatasourceElement.call(this);
if (datasource?.datasource?.attachObserver) {
datasource.datasource.attachObserver(
new Observer(() => {
syncOptionData.call(this);
syncButtons.call(this);
}),
);
}
if (datasource) {
datasource.addEventListener("monster-datasource-fetched", () => {
syncOptionData.call(this);
normalizeOnLoad.call(this);
});
}
}
/**
* @private
* @return {string|null}
*/
function getPathOption() {
const path = this.getOption("path");
if (isString(path) && path.trim() !== "") {
return path.trim();
}
return null;
}
/**
* @private
* @param {unknown} value
* @return {number|null}
*/
function normalizeLimit(value) {
if (value === null || value === undefined) {
return null;
}
if (isNumber(value) && Number.isFinite(value)) {
return value;
}
if (isString(value)) {
const trimmed = value.trim();
if (trimmed === "" || trimmed.toLowerCase() === "null") {
return null;
}
const numeric = Number(trimmed);
return Number.isFinite(numeric) ? numeric : null;
}
return null;
}
/**
* @private
* @return {number|null}
*/
function getMinLimit() {
return normalizeLimit(this.getOption("min"));
}
/**
* @private
* @return {number|null}
*/
function getMaxLimit() {
return normalizeLimit(this.getOption("max"));
}
/**
* @private
* @return {HTMLTemplateElement|null}
*/
function getItemTemplate() {
const slot = this[itemSlotSymbol];
if (slot && typeof slot.assignedElements === "function") {
const assigned = slot.assignedElements({ flatten: true });
for (const element of assigned) {
if (element instanceof HTMLTemplateElement) {
return element;
}
}
}
const fallback = this.querySelector('template[slot="item"]');
if (fallback instanceof HTMLTemplateElement) {
return fallback;
}
return null;
}
/**
* @private
* @return {string}
*/
function ensureTemplatePrefix() {
const container = this[itemsContainerSymbol];
if (!(container instanceof HTMLElement)) {
throw new Error("RepeatFieldSet is missing the items container.");
}
let prefix = container.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
if (!isString(prefix) || prefix.trim() === "") {
prefix = new ID("repeat-field").toString();
container.setAttribute(ATTRIBUTE_TEMPLATE_PREFIX, prefix);
}
return prefix;
}
/**
* @private
*/
function syncInsertDefinition() {
const path = getPathOption.call(this);
if (!path) {
addErrorAttribute(this, "RepeatFieldSet requires a path option.");
return;
}
const container = this[itemsContainerSymbol];
if (!(container instanceof HTMLElement)) {
addErrorAttribute(this, "RepeatFieldSet is missing the items container.");
return;
}
const template = getItemTemplate.call(this);
if (!template) {
addErrorAttribute(
this,
'RepeatFieldSet requires a <template slot="item"> definition.',
);
return;
}
const prefix = ensureTemplatePrefix.call(this);
const themeName = getDocumentTheme().getName();
const nextId = `${prefix}-item-${themeName}`;
if (template.id !== nextId) {
template.id = nextId;
}
const form = this.closest("monster-form");
let record = null;
if (form && typeof form.getOption === "function") {
const formData = form.getOption("data");
if (isObject(formData) || isArray(formData)) {
record = formData;
}
}
if (!record) {
record = resolveFormRecord(form, getDatasourceElement.call(this));
}
const normalizedPath = normalizePathForRecord(path, record);
const dataPath = normalizedPath.startsWith("data.")
? normalizedPath
: `data.${normalizedPath}`;
const insertValue = `item path:${dataPath}`;
if (container.getAttribute(ATTRIBUTE_UPDATER_INSERT) !== insertValue) {
container.setAttribute(ATTRIBUTE_UPDATER_INSERT, insertValue);
}
if (this.getAttribute(ATTRIBUTE_UPDATER_INSERT)) {
this.removeAttribute(ATTRIBUTE_UPDATER_INSERT);
}
}
/**
* @private
* @return {HTMLElement|null}
*/
function getDatasourceElement() {
const form = this.closest("monster-form");
if (!form) {
return null;
}
if (form[datasourceLinkedElementSymbol]) {
return form[datasourceLinkedElementSymbol];
}
if (typeof form.getOption === "function") {
const selector = form.getOption("datasource.selector");
if (isString(selector) && selector.trim() !== "") {
return document.querySelector(selector);
}
}
return null;
}
/**
* @private
* @return {object|null}
*/
function getTargetContext() {
const form = this.closest("monster-form");
const path = getPathOption.call(this);
if (!path) {
return null;
}
if (form && typeof form.getOption === "function") {
const formData = form.getOption("data");
if (isObject(formData) || isArray(formData)) {
const formPath = normalizePathForForm(path);
return { type: "form", target: form, path: formPath };
}
}
const datasource = getDatasourceElement.call(this);
if (datasource) {
const mappingData = form?.getOption?.("mapping.data");
const mappingIndex = form?.getOption?.("mapping.index");
const record = resolveFormRecord(form, datasource);
const normalizedPath = normalizePathForRecord(path, record);
let basePath = "";
if (isString(mappingData) && mappingData.trim() !== "") {
basePath = mappingData.trim();
}
if (mappingIndex !== null && mappingIndex !== undefined) {
const indexValue = String(mappingIndex);
basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
}
const finalPath = basePath
? `${basePath}.${normalizedPath}`
: normalizedPath;
return { type: "datasource", target: datasource, path: finalPath };
}
if (form && typeof form.getOption === "function") {
return { type: "form", target: form, path };
}
return null;
}
/**
* @private
* @param {string} path
* @return {string}
*/
function normalizePathForForm(path) {
if (!isString(path)) {
return path;
}
const trimmed = path.trim();
if (trimmed.startsWith("data.")) {
return trimmed;
}
return `data.${trimmed}`;
}
/**
* @private
* @param {string} path
* @param {object} record
* @return {string}
*/
function normalizePathForRecord(path, record) {
if (!isString(path)) {
return path;
}
const trimmed = path.trim();
if (!trimmed.startsWith("data.")) {
return trimmed;
}
if (isObject(record?.data) || isArray(record?.data)) {
return trimmed;
}
return trimmed.slice(5);
}
/**
* @private
* @return {object}
*/
function getCurrentDataSnapshot() {
const context = getTargetContext.call(this);
if (!context) {
return { context: null, data: {} };
}
let data;
if (context.type === "datasource") {
data = context.target?.data;
} else {
data = context.target?.getOption?.("data");
}
if (!isObject(data) && !isArray(data)) {
data = {};
}
return { context, data };
}
/**
* @private
* @return {Array}
*/
function getCurrentItems() {
const { context, data } = getCurrentDataSnapshot.call(this);
if (!context) {
return [];
}
try {
if (context.type === "form") {
const value = context.target?.getOption?.(context.path);
return isArray(value) ? [...value] : [];
}
const value = new Pathfinder(data).getVia(context.path);
return isArray(value) ? [...value] : [];
} catch {
return [];
}
}
/**
* @private
* @return {object}
*/
function buildEmptyItem() {
const defaults = getItemDefaults.call(this);
return clone(defaults);
}
/**
* @private
* @return {object}
*/
function getItemDefaults() {
if (this[itemDefaultsSymbol]) {
return this[itemDefaultsSymbol];
}
const template = getItemTemplate.call(this);
const defaults = {};
if (!template?.content) {
this[itemDefaultsSymbol] = defaults;
return defaults;
}
const elements = template.content.querySelectorAll(
"[data-monster-bind],[data-monster-attributes]",
);
for (const element of elements) {
const bindAttr = element.getAttribute("data-monster-bind");
const attrAttr = element.getAttribute("data-monster-attributes");
const paths = new Set([
...extractItemPaths(bindAttr),
...extractItemPathsFromAttributes(attrAttr),
]);
if (paths.size === 0) {
continue;
}
const value = getDefaultValueForElement(element);
for (const path of paths) {
try {
new Pathfinder(defaults).setVia(path, value);
} catch (error) {
addErrorAttribute(
this,
error?.message || "Failed to build default repeat item.",
);
}
}
}
this[itemDefaultsSymbol] = defaults;
return defaults;
}
/**
* @private
* @param {string|null} value
* @return {string[]}
*/
function extractItemPaths(value) {
if (!isString(value)) {
return [];
}
const paths = [];
const regex = /path:item\.([A-Za-z0-9_.-]+)/g;
for (const match of value.matchAll(regex)) {
if (match[1]) {
paths.push(match[1]);
}
}
return paths;
}
/**
* @private
* @param {string|null} value
* @return {string[]}
*/
function extractItemPathsFromAttributes(value) {
if (!isString(value)) {
return [];
}
const paths = [];
const entries = value.split(",");
for (const entry of entries) {
const def = entry.trim();
if (
def.startsWith("value ") ||
def.startsWith("checked ") ||
def.startsWith("selected ")
) {
paths.push(...extractItemPaths(def));
}
}
return paths;
}
/**
* @private
* @param {Element} element
* @return {*}
*/
function getDefaultValueForElement(element) {
const bindType = element.getAttribute("data-monster-bind-type");
const defaultValue = element.getAttribute("data-monster-defaultvalue");
if (defaultValue !== null) {
return coerceDefaultValue(defaultValue, bindType);
}
if (bindType === "boolean") {
return false;
}
if (bindType === "number") {
return null;
}
const tagName = element.tagName?.toLowerCase();
if (tagName === "monster-toggle-switch") {
return false;
}
if (tagName === "monster-select") {
const optionType = element.getAttribute("data-monster-option-type");
if (optionType === "checkbox" || element.hasAttribute("multiple")) {
return [];
}
return "";
}
if (tagName === "select" && element.hasAttribute("multiple")) {
return [];
}
const inputType = element.getAttribute("type");
if (inputType === "checkbox" || inputType === "radio") {
return false;
}
return "";
}
/**
* @private
* @param {string} value
* @param {string|null} bindType
* @return {*}
*/
function coerceDefaultValue(value, bindType) {
if (bindType === "boolean") {
const normalized = value.trim().toLowerCase();
return normalized === "true" || normalized === "1";
}
if (bindType === "number") {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
return value;
}
/**
* @private
* @param {Array} items
* @return {Array}
*/
function enforceLimits(items) {
let list = isArray(items) ? [...items] : [];
const min = getMinLimit.call(this);
const max = getMaxLimit.call(this);
if (isNumber(max) && Number.isFinite(max)) {
if (list.length > max) {
list = list.slice(0, max);
}
}
if (isNumber(min) && Number.isFinite(min)) {
while (list.length < min) {
list.push(buildEmptyItem.call(this));
}
}
return list;
}
/**
* @private
* @param {Array} items
*/
function persistItems(items, options = {}) {
const { context, data } = getCurrentDataSnapshot.call(this);
if (!context) {
return;
}
const { syncDatasource = true } = options;
if (context.type === "form") {
try {
context.target.setOption(context.path, items);
} catch (error) {
addErrorAttribute(this, error?.message || "Failed to update form data.");
return;
}
if (syncDatasource) {
const datasourceContext = getDatasourceContext.call(this);
const datasourceData = datasourceContext?.target?.data;
if (
datasourceContext &&
(isObject(datasourceData) || isArray(datasourceData))
) {
const nextData = clone(datasourceData);
try {
new Pathfinder(nextData).setVia(datasourceContext.path, items);
datasourceContext.target.data = nextData;
} catch (error) {
addErrorAttribute(
this,
error?.message || "Failed to update datasource data.",
);
return;
}
}
}
} else {
const nextData = clone(data);
try {
new Pathfinder(nextData).setVia(context.path, items);
} catch (error) {
addErrorAttribute(this, error?.message || "Failed to update path.");
return;
}
context.target.data = nextData;
}
syncOptionData.call(this);
syncButtons.call(this);
fireCustomEvent(this, "monster-repeat-change", {
detail: {
length: items.length,
path: context.path,
},
});
}
/**
* @private
*/
function addItem() {
if (this.getOption("disabled") === true) {
return;
}
const items = getCurrentItems.call(this);
const max = getMaxLimit.call(this);
if (isNumber(max) && Number.isFinite(max) && items.length >= max) {
return;
}
const nextItems = [...items, buildEmptyItem.call(this)];
persistItems.call(this, nextItems);
fireCustomEvent(this, "monster-repeat-add", {
detail: { length: nextItems.length },
});
}
/**
* @private
*/
function removeItem() {
if (this.getOption("disabled") === true) {
return;
}
const items = getCurrentItems.call(this);
const min = getMinLimit.call(this);
if (isNumber(min) && Number.isFinite(min) && items.length <= min) {
return;
}
if (items.length === 0) {
return;
}
const nextItems = items.slice(0, -1);
persistItems.call(this, nextItems);
fireCustomEvent(this, "monster-repeat-remove", {
detail: { length: nextItems.length },
});
}
/**
* @private
*/
function normalizeOnLoad() {
const items = getCurrentItems.call(this);
const normalized = enforceLimits.call(this, items);
if (normalized.length !== items.length) {
persistItems.call(this, normalized, { syncDatasource: false });
return;
}
syncOptionData.call(this);
}
/**
* @private
*/
function syncButtons() {
const items = getCurrentItems.call(this);
const min = getMinLimit.call(this);
const max = getMaxLimit.call(this);
const isDisabled = this.getOption("disabled") === true;
if (this[addButtonSymbol]) {
const reachedMax =
isNumber(max) && Number.isFinite(max) && items.length >= max;
this[addButtonSymbol].disabled = isDisabled || reachedMax;
}
if (this[removeButtonSymbol]) {
const reachedMin =
isNumber(min) && Number.isFinite(min) && items.length <= min;
this[removeButtonSymbol].disabled =
isDisabled || reachedMin || items.length === 0;
}
}
/**
* @private
*/
function syncOptionData() {
const form = this.closest("monster-form");
if (!form) {
return;
}
const formData = form.getOption?.("data");
if (isObject(formData) || isArray(formData)) {
this.setOption("data", clone(formData));
} else {
const datasource = getDatasourceElement.call(this);
if (datasource) {
const record = resolveFormRecord.call(this, form, datasource);
if (record) {
this.setOption("data", clone(record));
}
}
}
}
/**
* @private
* @param {HTMLElement} form
* @param {HTMLElement} datasource
* @return {object}
*/
function resolveFormRecord(form, datasource) {
const { data, dataPath } = resolveDatasourceBasePath(form, datasource);
if (!isObject(data) && !isArray(data)) {
return {};
}
let resolvedData = data;
if (isObject(resolvedData) && !isArray(resolvedData)) {
if (isArray(resolvedData.data)) {
resolvedData = resolvedData.data;
} else if (isArray(resolvedData.dataset)) {
resolvedData = resolvedData.dataset;
} else if (dataPath === "") {
resolvedData = Object.values(resolvedData);
}
}
const mappingIndex = form.getOption?.("mapping.index");
if (mappingIndex !== null && mappingIndex !== undefined) {
resolvedData = resolvedData?.[mappingIndex];
}
if (!isObject(resolvedData) && !isArray(resolvedData)) {
return {};
}
return resolvedData;
}
/**
* @private
* @return {{target: HTMLElement, path: string}|null}
*/
function getDatasourceContext() {
const form = this.closest("monster-form");
const datasource = getDatasourceElement.call(this);
const path = getPathOption.call(this);
if (!datasource || !path) {
return null;
}
const mappingIndex = form?.getOption?.("mapping.index");
const { dataPath } = resolveDatasourceBasePath(form, datasource);
const record = resolveFormRecord(form, datasource);
const normalizedPath = normalizePathForRecord(path, record);
let basePath = "";
if (isString(dataPath) && dataPath.trim() !== "") {
basePath = dataPath.trim();
}
if (mappingIndex !== null && mappingIndex !== undefined) {
const indexValue = String(mappingIndex);
basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
}
const finalPath = basePath ? `${basePath}.${normalizedPath}` : normalizedPath;
return { target: datasource, path: finalPath };
}
/**
* @private
* @param {HTMLElement} form
* @param {HTMLElement} datasource
* @return {{data: object|Array, dataPath: string}}
*/
function resolveDatasourceBasePath(form, datasource) {
let data = datasource?.data;
if (!isObject(data) && !isArray(data)) {
return { data: {}, dataPath: "" };
}
let dataPath = "";
const mappingData = form?.getOption?.("mapping.data");
if (isString(mappingData) && mappingData.trim() !== "") {
const trimmed = mappingData.trim();
try {
const mapped = new Pathfinder(data).getVia(trimmed);
if (mapped !== undefined) {
data = mapped;
dataPath = trimmed;
}
} catch {}
}
if (dataPath === "" && isObject(data) && !isArray(data)) {
if (isArray(data.data)) {
dataPath = "data";
} else if (isArray(data.dataset)) {
dataPath = "dataset";
}
}
return { data, dataPath };
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<div data-monster-role="container" part="container">
<div data-monster-attributes="class path:classes.content" part="content">
<slot></slot>
<slot name="items"></slot>
</div>
<div data-monster-role="actions" part="actions">
<slot name="actions"></slot>
<button type="button" data-monster-role="add" part="add">
<span data-monster-replace="path:labels.add"></span>
</button>
<button type="button" data-monster-role="remove" part="remove">
<span data-monster-replace="path:labels.remove"></span>
</button>
</div>
<slot name="item"></slot>
</div>
</div>`;
}
/**
* @private
* @return {string}
*/
registerCustomElement(RepeatFieldSet);