Something went wrong on our end
Select Git revision
save-button.mjs
-
Volker Schukai authoredVolker Schukai authored
save-button.mjs 9.95 KiB
/**
* Copyright © schukai GmbH 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 schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol, internalSymbol } from "../../constants.mjs";
import { diff } from "../../data/diff.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import {
assembleMethodSymbol,
CustomElement,
attributeObserverSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { isString, isArray } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { TokenList } from "../../types/tokenlist.mjs";
import { clone } from "../../util/clone.mjs";
import { State } from "../form/types/state.mjs";
import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs";
import { Datasource } from "./datasource.mjs";
import { BadgeStyleSheet } from "../stylesheet/badge.mjs";
import { SaveButtonStyleSheet } from "./stylesheet/save-button.mjs";
import {
handleDataSourceChanges,
datasourceLinkedElementSymbol,
} from "./util.mjs";
export { SaveButton };
/**
* @private
* @type {symbol}
*/
const stateButtonElementSymbol = Symbol("stateButtonElement");
/**
* @private
* @type {symbol}
*/
const originValuesSymbol = Symbol("originValues");
/**
* @private
* @type {symbol}
*/
const badgeElementSymbol = Symbol("badgeElement");
class SaveButton extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for(
"@schukai/monster/components/datasource/save-button@@instance",
);
}
/**
* 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}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {object} datasource The datasource
* @property {string} datasource.selector The selector of the datasource
* @property {string} labels.button The button label
* @property {Object} classes The classes
* @property {string} classes.bar The bar class
* @property {string} classes.badge The badge class
* @property {Array} ignoreChanges The ignore changes (regex)
* @property {Array} data The data
* @return {Object}
*/
get defaults() {
const obj = Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: {
button: "save",
},
classes: {
bar: "monster-button-primary",
badge: "monster-badge-secondary hidden",
},
datasource: {
selector: null,
},
changes: "0",
ignoreChanges: [],
data: {},
disabled: false,
});
updateOptionsFromArguments.call(this, obj);
return obj;
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datasource-save-button";
}
/**
* This method is responsible for assembling the component.
*
* It calls the parent's assemble method first, then initializes control references and event handlers.
* If the `datasource.selector` option is provided and is a string, it searches for the corresponding
* element in the DOM using that selector.
*
* If the selector matches exactly one element, it checks if the element is an instance of the `Datasource` class.
*
* If it is, the component's `datasourceLinkedElementSymbol` property is set to the element, and the component
* attaches an observer to the datasource's changes.
*
* The observer is a function that calls the `handleDataSourceChanges` method in the context of the component.
* Additionally, the component attaches an observer to itself, which also calls the `handleDataSourceChanges`
* method in the component's context.
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
const self = this;
initControlReferences.call(this);
initEventHandler.call(this);
const selector = this.getOption("datasource.selector");
if (isString(selector)) {
const element = findElementWithSelectorUpwards(this, selector);
if (element === null) {
throw new Error("the selector must match exactly one element");
}
if (!(element instanceof Datasource)) {
throw new TypeError("the element must be a datasource");
}
this[datasourceLinkedElementSymbol] = element;
element.datasource.attachObserver(
new Observer(handleDataSourceChanges.bind(this)),
);
self[originValuesSymbol] = null;
element.datasource.attachObserver(
new Observer(function () {
if (!self[originValuesSymbol]) {
self[originValuesSymbol] = clone(
self[datasourceLinkedElementSymbol].data,
);
}
const currentValues = this.getRealSubject();
const ignoreChanges = self.getOption("ignoreChanges");
const result = diff(self[originValuesSymbol], currentValues);
if (isArray(ignoreChanges) && ignoreChanges.length > 0) {
const itemsToRemove = [];
for (const item of result) {
for (const ignorePattern of ignoreChanges) {
const p = new RegExp(ignorePattern);
if (p.test(item.path)) {
itemsToRemove.push(item);
break;
}
}
}
for (const itemToRemove of itemsToRemove) {
const index = result.indexOf(itemToRemove);
if (index > -1) {
result.splice(index, 1);
}
}
}
if (isArray(result) && result.length > 0) {
self[stateButtonElementSymbol].setState("changed");
self[stateButtonElementSymbol].setOption("disabled", false);
self.setOption("changes", result.length);
self.setOption(
"classes.badge",
new TokenList(self.getOption("classes.badge"))
.remove("hidden")
.toString(),
);
} else {
self[stateButtonElementSymbol].removeState();
self[stateButtonElementSymbol].setOption("disabled", true);
self.setOption("changes", 0);
self.setOption(
"classes.badge",
new TokenList(self.getOption("classes.badge"))
.add("hidden")
.toString(),
);
}
}),
);
}
this.attachObserver(
new Observer(() => {
handleDataSourceChanges.call(this);
}),
);
}
/**
*
* @return [CSSStyleSheet]
*/
static getCSSStyleSheet() {
return [SaveButtonStyleSheet, BadgeStyleSheet];
}
}
/**
* @private
* @return {Monster.Components.Datatable.Form}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[stateButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=state-button]",
);
this[badgeElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=badge]",
);
if (this[stateButtonElementSymbol]) {
queueMicrotask(() => {
const states = {
changed: new State(
"changed",
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">\n' +
' <path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708z"/>\n' +
' <path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383m.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>\n' +
"</svg>",
),
};
this[stateButtonElementSymbol].removeState();
this[stateButtonElementSymbol].setOption("disabled", "disabled");
this[stateButtonElementSymbol].setOption("states", states);
this[stateButtonElementSymbol].setOption(
"labels.button",
this.getOption("labels.button"),
);
});
}
return this;
}
/**
* @private
*/
function initEventHandler() {
queueMicrotask(() => {
this[stateButtonElementSymbol].setOption("actions.click", () => {
this[datasourceLinkedElementSymbol]
.write()
.then(() => {
this[originValuesSymbol] = null;
this[stateButtonElementSymbol].removeState();
this[stateButtonElementSymbol].setOption("disabled", true);
this.setOption("changes", 0);
this.setOption(
"classes.badge",
new TokenList(this.getOption("classes.badge"))
.add("hidden")
.toString(),
);
})
.catch((error) => {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
});
});
});
}
/**
* @param {Object} options
*/
function updateOptionsFromArguments(options) {
const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
if (selector) {
options.datasource.selector = selector;
}
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control"
data-monster-attributes="disabled path:disabled | if:true">
<monster-state-button data-monster-role="state-button">save</monster-state-button>
<div data-monster-attributes="disabled path:disabled | if:true, class path:classes.badge"
data-monster-role="badge"
data-monster-replace="path:changes"></div>
</div>
`;
}
registerCustomElement(SaveButton);