UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,173 lines (1,039 loc) 27.5 kB
/** * 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);