diff --git a/application/source/dom/util/init-options-from-attributes.mjs b/application/source/dom/util/init-options-from-attributes.mjs new file mode 100644 index 0000000000000000000000000000000000000000..1c7e43813ec52a7448484d8bd0cf2dee2eaa7391 --- /dev/null +++ b/application/source/dom/util/init-options-from-attributes.mjs @@ -0,0 +1,78 @@ +/** + * Copyright schukai GmbH and contributors 2023. All Rights Reserved. + * Node module: @schukai/monster + * This file is licensed under the AGPLv3 License. + * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html + */ + +import {Pathfinder} from '../../data/pathfinder.mjs'; +import {isFunction} from '../../types/is.mjs'; + +export {initOptionsFromAttributes }; + +/** + * Initializes the given options object based on the attributes of the current DOM element. + * The function looks for attributes with the prefix 'data-monster-option-', and maps them to + * properties in the options object. It replaces the dashes with dots to form the property path. + * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object. + * + * With the mapping parameter, the attribute value can be mapped to a different value. + * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object. + * + * The mapping object would look like this: + * { + * 'foo': (value) => value + 'bar' + * // the value of the attribute 'data-monster-option-foo' is appended with 'bar' + * // and assigned to the 'bar' property in the options object. + * // e.g. <div data-monster-option-foo="foo"></div> + * 'bar.baz': (value) => value + 'bar' + * // the value of the attribute 'data-monster-option-bar-baz' is appended with 'bar' + * // and assigned to the 'bar.baz' property in the options object. + * // e.g. <div data-monster-option-bar-baz="foo"></div> + * } + * + * @param {HTMLElement} element - The DOM element to be used as the source of the attributes. + * @param {Object} options - The options object to be initialized. + * @param {Object} mapping - A mapping between the attribute value and the property value. + * @param {string} prefix - The prefix of the attributes to be considered. + * @returns {Object} - The initialized options object. + * @this HTMLElement - The context of the DOM element. + */ +function initOptionsFromAttributes(element, options, mapping={},prefix = 'data-monster-option-') { + if (!(element instanceof HTMLElement)) return options; + if (!element.hasAttributes()) return options; + + const finder = new Pathfinder(options); + + element.getAttributeNames().forEach((name) => { + if (!name.startsWith(prefix)) return; + + // check if the attribute name is a valid option. + // the mapping between the attribute is simple. The dash is replaced by a dot. + // e.g. data-monster-url => url + const optionName = name.replace(prefix, '').replace(/-/g, '.'); + if (!finder.exists(optionName)) return; + + if (element.hasAttribute(name)) { + let value = element.getAttribute(name); + if (mapping.hasOwnProperty(optionName)&&isFunction(mapping[optionName])) { + value = mapping[optionName](value); + } + + const typeOfOptionValue = typeof finder.getVia(optionName); + if (typeOfOptionValue === 'boolean') { + value = value === 'true'; + } else if (typeOfOptionValue === 'number') { + value = Number(value); + } else if (typeOfOptionValue === 'string') { + value = String(value); + } else if (typeOfOptionValue === 'object') { + value = JSON.parse(value); + } + + finder.setVia(optionName, value); + } + }) + + return options; +} \ No newline at end of file diff --git a/development/pnpm-lock.yaml b/development/pnpm-lock.yaml index de9815ee8909981f2e5332626d9f1d63913440e0..6d9d1a74f9b499ec728db0a342ad2ec11ff56b5e 100644 --- a/development/pnpm-lock.yaml +++ b/development/pnpm-lock.yaml @@ -1611,7 +1611,7 @@ packages: execa: 4.1.0 polyfill-library: 3.111.0 semver: 7.3.8 - snyk: 1.1133.0 + snyk: 1.1134.0 yargs: 15.4.1 transitivePeerDependencies: - supports-color @@ -4091,8 +4091,8 @@ packages: supports-color: 7.2.0 dev: true - /snyk@1.1133.0: - resolution: {integrity: sha512-op/OCcfZZcR1nZDwKS4sVYB51Dqc327/t47a0nyzVTtB8qVQJyujnIcsWaOeY4uEoaJnUNxp1Mp5kgN6jhwJFA==} + /snyk@1.1134.0: + resolution: {integrity: sha512-gy+Aas1F10AEkAH40f8ewPavFCVjUW/izTv1RJNRwuOt52wqFgb4mkW3F0U6xq4HCNiEzmWCkz8BN21uzdMPUA==} engines: {node: '>=12'} hasBin: true requiresBuild: true diff --git a/development/test/cases/dom/events.mjs b/development/test/cases/dom/events.mjs index 9abf85cd72f866f25418846ce638d582a1bdf77e..e255fd5178a4400577f20ffe256cfe3ce3be0fc4 100644 --- a/development/test/cases/dom/events.mjs +++ b/development/test/cases/dom/events.mjs @@ -8,7 +8,7 @@ import {initJSDOM} from "../../util/jsdom.mjs"; describe('Events', function () { before(async function () { - initJSDOM(); + await initJSDOM(); }) describe('findTargetElementFromEvent()', function () { @@ -23,10 +23,10 @@ describe('Events', function () { expect(e.getAttribute('data-monster')).to.be.equal('hello') done(); }) - setTimeout(()=>{ + setTimeout(() => { fireEvent(div, 'click'); - },0) - + }, 0) + }); }); @@ -76,10 +76,10 @@ describe('Events', function () { it('should throw error', function () { expect(() => fireEvent({}, 'touch')).to.throw(Error); - + }); - }); - + }); + describe('fireCustomEvent()', function () { it('should fire a click event', function (done) { let div = document.createElement('div'); @@ -100,8 +100,8 @@ describe('Events', function () { it('should fire a touch event on collection1', function (done) { let div = document.createElement('div'); div.addEventListener('touch', (e) => { - if(e.detail.detail!=='hello world') { - done('error'); + if (e.detail.detail !== 'hello world') { + done('error'); } done(); }) @@ -111,12 +111,12 @@ describe('Events', function () { fireCustomEvent(collection, 'touch', "hello world"); }); - + it('should fire a touch event on collection2', function (done) { let div = document.createElement('div'); div.addEventListener('touch', (e) => { - if(e.detail.a!=='hello world') { - done('error'); + if (e.detail.a !== 'hello world') { + done('error'); } done(); }) @@ -124,7 +124,7 @@ describe('Events', function () { div.appendChild(document.createElement('div')); let collection = div.querySelectorAll('div'); - fireCustomEvent(collection, 'touch', {a:"hello world"}); + fireCustomEvent(collection, 'touch', {a: "hello world"}); }); it('should fire a touch event', function (done) { diff --git a/development/test/cases/dom/util/init-options-from-attributes.mjs b/development/test/cases/dom/util/init-options-from-attributes.mjs new file mode 100644 index 0000000000000000000000000000000000000000..2e030d9dc8c6e67247a1f9ab3335314712af9cd9 --- /dev/null +++ b/development/test/cases/dom/util/init-options-from-attributes.mjs @@ -0,0 +1,155 @@ +import {expect} from "chai" + +import {initOptionsFromAttributes} from "../../../../..//application/source/dom/util/init-options-from-attributes.mjs"; +import {initJSDOM} from "../../../util/jsdom.mjs"; + +describe('initOptionsFromAttributes', () => { + let element; + let options; + + before(async function () { + await initJSDOM(); + }) + + beforeEach(() => { + options = { url: "", key: { subkey: "" } }; + element = document.createElement('div'); + }); + + it('should initialize options with matching attributes', () => { + element.setAttribute('data-monster-option-url', 'https://example.com'); + element.setAttribute('data-monster-option-key.subkey', 'test'); + + const result = initOptionsFromAttributes(element, options); + + expect(result.url).to.equal('https://example.com'); + expect(result.key.subkey).to.equal('test'); + }); + + it('should not modify options without matching attributes', () => { + const result = initOptionsFromAttributes(element, options); + + expect(result.url).to.equal(''); + expect(result.key.subkey).to.equal(''); + }); + + it('should ignore attributes without the correct prefix', () => { + element.setAttribute('data-some-option-url', 'https://example.com'); + + const result = initOptionsFromAttributes(element, options); + + expect(result.url).to.equal(''); + }); + + it('should ignore attributes with invalid option paths', () => { + element.setAttribute('data-monster-option-nonexistent', 'value'); + + const result = initOptionsFromAttributes(element, options); + + expect(result).to.deep.equal(options); + }); + + it('should apply mapping for a single attribute', () => { + element.setAttribute('data-monster-option-url', 'example'); + const mapping = { + 'url': (value) => 'https://' + value + '.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal('https://example.com'); + }); + + it('should apply mapping for a nested attribute', () => { + element.setAttribute('data-monster-option-key-subkey', '123'); + const mapping = { + 'key.subkey': (value) => parseInt(value, 10) * 2 + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.key.subkey).to.equal("246"); + }); + + it('should apply multiple mappings', () => { + element.setAttribute('data-monster-option-url', 'example'); + element.setAttribute('data-monster-option-key.subkey', '123'); + const mapping = { + 'url': (value) => 'https://' + value + '.com', + 'key.subkey': (value) => parseInt(value, 10) * 2 + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal('https://example.com'); + expect(result.key.subkey).to.equal("246"); + }); + + it('should ignore mappings for non-existing attributes', () => { + const mapping = { + 'url': (value) => 'https://' + value + '.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal(''); + }); + + it('should ignore mappings for invalid option paths', () => { + element.setAttribute('data-monster-option-nonexistent', 'value'); + const mapping = { + 'nonexistent': (value) => value + 'bar' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result).to.deep.equal(options); + }); + + it('should apply mapping only to specified attributes', () => { + element.setAttribute('data-monster-option-url', 'example'); + element.setAttribute('data-monster-option-key.subkey', '123'); + const mapping = { + 'url': (value) => 'https://' + value + '.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal('https://example.com'); + expect(result.key.subkey).to.equal('123'); + }); + + it('should not apply mapping if not a function', () => { + element.setAttribute('data-monster-option-url', 'example'); + const mapping = { + 'url': 'https://example.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal('example'); + }); + + it('should apply mapping with custom prefix', () => { + element.setAttribute('data-custom-option-url', 'example'); + const mapping = { + 'url': (value) => 'https://' + value + '.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping, 'data-custom-option-'); + + expect(result.url).to.equal('https://example.com'); + }); + + it('should not apply mapping with incorrect custom prefix', () => { + element.setAttribute('data-custom-option-url', 'example'); + const mapping = { + 'url': (value) => 'https://' + value + '.com' + }; + + const result = initOptionsFromAttributes(element, options, mapping); + + expect(result.url).to.equal(''); + }); + +});