Skip to content
Snippets Groups Projects
Verified Commit 4aa3ea60 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: configuration via callback host

parent 3d7aaaa2
No related branches found
No related tags found
No related merge requests found
......@@ -59,6 +59,9 @@ export {
ATTRIBUTE_HIDDEN,
objectUpdaterLinkSymbol,
customElementUpdaterLinkSymbol,
optionCallbackName,
ATTRIBUTE_SCRIPT_HOST,
ATTRIBUTE_OPTION_CALLBACK
};
/**
......@@ -86,6 +89,16 @@ const ATTRIBUTE_PREFIX = "data-monster-";
*/
const ATTRIBUTE_OPTIONS = `${ATTRIBUTE_PREFIX}options`;
/**
* This is name of the attribute to pass the script host to a control
*
* @memberOf Monster.DOM
* @license AGPLv3
* @since 3.48.0
* @type {string}
*/
const ATTRIBUTE_SCRIPT_HOST = `${ATTRIBUTE_PREFIX}script-host`;
/**
* This is the name of the attribute to pass options to a control
*
......@@ -96,6 +109,26 @@ const ATTRIBUTE_OPTIONS = `${ATTRIBUTE_PREFIX}options`;
*/
const ATTRIBUTE_OPTIONS_SELECTOR = `${ATTRIBUTE_PREFIX}options-selector`;
/**
* This is the name of the attribute to pass the callback to a control
*
* @memberOf Monster.DOM
* @license AGPLv3
* @since 3.48.0
* @type {string}
*/
const ATTRIBUTE_OPTION_CALLBACK = `${ATTRIBUTE_PREFIX}option-callback`;
/**
* This is the name of the callback to pass the callback to a control
*
* @memberOf Monster.DOM
* @license AGPLv3
* @since 3.48.0
* @type {string}
*/
const optionCallbackName = `initCustomControlOptionsCallback`;
/**
* @memberOf Monster.DOM
* @type {string}
......
......@@ -5,6 +5,7 @@
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
*/
import {findElementWithIdUpwards} from "./util.mjs";
import {internalSymbol} from "../constants.mjs";
import {extend} from "../data/extend.mjs";
import {Pathfinder} from "../data/pathfinder.mjs";
......@@ -22,8 +23,11 @@ import {
ATTRIBUTE_DISABLED,
ATTRIBUTE_ERRORMESSAGE,
ATTRIBUTE_OPTIONS,
ATTRIBUTE_OPTION_CALLBACK,
ATTRIBUTE_OPTIONS_SELECTOR,
ATTRIBUTE_SCRIPT_HOST,
customElementUpdaterLinkSymbol,
optionCallbackName
} from "./constants.mjs";
import {findDocumentTemplate, Template} from "./template.mjs";
import {addObjectWithUpdaterToElement} from "./updater.mjs";
......@@ -67,6 +71,12 @@ const attributeObserverSymbol = Symbol.for("@schukai/monster/dom/@@attributeObse
*/
const attributeMutationObserverSymbol = Symbol("@schukai/monster/dom/@@mutationObserver");
/**
* @private
* @type {symbol}
*/
const scriptHostElementSymbol = Symbol("scriptHostElement");
/**
* HTMLElement
* @external HTMLElement
......@@ -212,6 +222,7 @@ class CustomElement extends HTMLElement {
});
this[initMethodSymbol]();
initOptionObserver.call(this);
this[scriptHostElementSymbol] = [];
}
......@@ -520,6 +531,7 @@ class CustomElement extends HTMLElement {
self.setOptions(ScriptOptions);
}
if (self.getOption("shadowMode", false) !== false) {
try {
initShadowRoot.call(self);
......@@ -541,6 +553,8 @@ class CustomElement extends HTMLElement {
}
}
initFromCallbackHost.call(this);
try {
nodeList = new Set([...elements, ...getSlottedElements.call(self)]);
} catch (e) {
......@@ -607,7 +621,7 @@ class CustomElement extends HTMLElement {
const self = this;
if (attrName.startsWith("data-monster-option-")) {
setOptionFromAttribute(self, attrName, this[internalSymbol].getSubject()["options"])
setOptionFromAttribute(self, attrName, this[internalSymbol].getSubject()["options"])
}
const callback = self[attributeObserverSymbol]?.[attrName];
......@@ -641,6 +655,99 @@ class CustomElement extends HTMLElement {
return containChildNode.call(self.shadowRoot, node);
}
/**
* Calls a callback function if it exists.
*
* @param {string} name
* @param {*} args
* @returns {*}
*/
callCallback(name, args) {
const self = this;
return callControlCallback.call(self, name, ...args);
}
}
/**
* @param {string} callBackFunctionName
* @param {*} args
* @return {any}
*/
function callControlCallback(callBackFunctionName, ...args) {
const self = this;
if (!isString(callBackFunctionName) || callBackFunctionName === "") {
return;
}
if (callBackFunctionName in self) {
return self[callBackFunctionName](self, ...args);
}
if (!self.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) {
return;
}
if (self[scriptHostElementSymbol].length === 0) {
const targetId = self.getAttribute(ATTRIBUTE_SCRIPT_HOST);
if (!targetId) {
return;
}
const list = targetId.split(",")
for (const id of list) {
const host = findElementWithIdUpwards(self, targetId);
if (!(host instanceof HTMLElement)) {
continue;
}
self[scriptHostElementSymbol].push(host);
}
}
for (const host of self[scriptHostElementSymbol]) {
if (callBackFunctionName in host) {
try {
return host[callBackFunctionName](self, ...args);
} catch (e) {
addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.toString());
}
}
}
addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `callback ${callBackFunctionName} not found`);
}
/**
* This Function is called when the element is attached to the DOM.
*
* It looks for the attribute `data-monster-option-callback`. Is this attribute is not set, the default callback
* `initCustomControlOptionsCallback` is called.
*
* The callback is searched in this element and in the host element. If the callback is found, it is called with the
* element as parameter.
*
* The `monster
*
* @this CustomElement
*/
function initFromCallbackHost() {
const self = this;
let callBackFunctionName = optionCallbackName // default callback
if (self.hasAttribute(ATTRIBUTE_OPTION_CALLBACK)) {
callBackFunctionName = self.getAttribute(ATTRIBUTE_OPTION_CALLBACK);
}
callControlCallback.call(self, callBackFunctionName);
}
/**
......
......@@ -33,14 +33,14 @@
"crypt": "^0.0.2",
"cssnano": "^6.0.1",
"esbuild": "^0.17.18",
"flow-bin": "^0.205.0",
"flow-bin": "^0.205.1",
"fs": "0.0.1-security",
"glob": "^10.2.2",
"graphviz": "^0.0.9",
"jsdoc": "^4.0.2",
"jsdoc-external-example": "github:volker-schukai/jsdoc-external-example",
"jsdoc-plantuml": "^1.0.3",
"jsdom": "^21.1.2",
"jsdom": "^22.0.0",
"jsdom-global": "^3.0.2",
"mocha": "^10.2.0",
"node-plantuml": "^0.9.0",
......@@ -54,18 +54,18 @@
"postcss-nesting": "^11.2.2",
"postcss-normalize": "^10.0.1",
"postcss-responsive-type": "^1.0.0",
"postcss-rtlcss": "^4.0.5",
"postcss-rtlcss": "^4.0.6",
"postcss-strip-units": "^2.0.1",
"rome": "^12.0.0",
"sinon": "^15.0.4",
"url": "^0.11.0",
"url-exist": "3.0.1",
"util": "^0.12.5",
"vite": "^4.3.3",
"vite": "^4.3.5",
"vite-plugin-banner": "^0.7.0",
"vite-plugin-list-directory-contents": "^1.4.5",
"vite-plugin-minify": "^1.5.2",
"vite-plugin-mkcert": "^1.14.1",
"vite-plugin-mkcert": "^1.15.0",
"ws": "^8.13.0"
}
}
This diff is collapsed.
'use strict';
import chai from "chai"
// import {internalSymbol} from "../../../../application/source/constants.mjs";
// import {ATTRIBUTE_OPTIONS} from "../../../../application/source/dom/constants.mjs";
import {getDocument} from "../../../../application/source/dom/util.mjs";
// import {ProxyObserver} from "../../../../application/source/types/proxyobserver.mjs";
// import {addObjectWithUpdaterToElement} from "../../../../application/source/dom/updater.mjs";
import {chaiDom} from "../../util/chai-dom.mjs";
import {initJSDOM} from "../../util/jsdom.mjs";
let expect = chai.expect;
chai.use(chaiDom);
// let html1 = `
// <div id="scripthost">
// </div>
//
// <div>
// <
// </div>
// `;
// defined in constants.mjs
// const updaterSymbolKey = "@schukai/monster/dom/custom-element@@options-updater-link"
// const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);
describe('DOM', function () {
let CustomElement, registerCustomElement, TestComponent, document, TestComponent2,assignUpdaterToElement;
describe('initFromScriptHost()', function () {
const randomTagNumber = "monster-test"+Math.floor(Math.random() * 1000000);
before(function (done) {
initJSDOM().then(() => {
import("../../../../application/source/dom/customelement.mjs").then((m) => {
try {
CustomElement = m['CustomElement'];
registerCustomElement = m['registerCustomElement'];
TestComponent2 = class extends CustomElement {
static getTag() {
return randomTagNumber;
}
/**
*
* @return {Object}
*/
get defaults() {
return Object.assign({}, super.defaults, {
test: 0,
templates: {
main: '<h1></h1><article><p>test</p><div id="container"></div></article>'
},
})
}
}
registerCustomElement(TestComponent2)
document = getDocument();
done()
} catch (e) {
done(e);
}
}).catch((e) => {
done(e);
});
});
})
afterEach(() => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = "";
})
describe('call callback', function () {
it('should not found callback and add error attribute', function () {
let mocks = document.getElementById('mocks');
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
let control = document.createElement(randomTagNumber);
control.setAttribute('data-monster-script-host', "call-back-host");
document.getElementById('container').appendChild(control);
expect(control.getOption('test')).is.eql(0);
expect(control.hasAttribute('data-monster-error')).is.true;
});
it('should found callback initCustomControlOptionsCallback', function () {
let mocks = document.getElementById('mocks');
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
const container = document.getElementById('call-back-host');
container.initCustomControlOptionsCallback = function (control) {
control.setOption('test', 1);
}
let control = document.createElement(randomTagNumber);
control.setAttribute('data-monster-script-host', "call-back-host");
document.getElementById('container').appendChild(control);
expect(control.getOption('test')).is.eql(1);
expect(control.hasAttribute('data-monster-error')).is.false;
});
it('should found callback initCustomControlOptionsCallback from self', function () {
let mocks = document.getElementById('mocks');
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
let control = document.createElement(randomTagNumber);
expect(control.getOption('test')).is.eql(0);
control.initCustomControlOptionsCallback = function (control) {
control.setOption('test', 2);
}
control.setAttribute('data-monster-script-host', "call-back-host");
document.getElementById('container').appendChild(control);
expect(control.getOption('test')).is.eql(2);
expect(control.hasAttribute('data-monster-error')).is.false;
});
})
});
})
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment