@schukai/monster 3.37.0 → 3.38.1

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schukai/monster",
3
- "version": "3.37.0",
3
+ "version": "3.38.1",
4
4
  "description": "Monster is a simple library for creating fast, robust and lightweight websites.",
5
5
  "keywords": [
6
6
  "framework",
@@ -30,6 +30,7 @@ import { addObjectWithUpdaterToElement } from "./updater.mjs";
30
30
  import { instanceSymbol } from "../constants.mjs";
31
31
  import { getDocumentTranslations, Translations } from "../i18n/translations.mjs";
32
32
  import { getSlottedElements } from "./slotted.mjs";
33
+ import {initOptionsFromAttributes} from "./util/init-options-from-attributes.mjs";
33
34
 
34
35
  export {
35
36
  CustomElement,
@@ -197,8 +198,9 @@ class CustomElement extends HTMLElement {
197
198
  */
198
199
  constructor() {
199
200
  super();
201
+
200
202
  this[internalSymbol] = new ProxyObserver({
201
- options: extend({}, this.defaults),
203
+ options: initOptionsFromAttributes(this, extend({}, this.defaults)),
202
204
  });
203
205
  this[attributeObserverSymbol] = {};
204
206
  initOptionObserver.call(this);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
3
+ * Node module: @schukai/monster
4
+ * This file is licensed under the AGPLv3 License.
5
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
6
+ */
7
+
8
+ import {Pathfinder} from '../../data/pathfinder.mjs';
9
+ import {isFunction} from '../../types/is.mjs';
10
+
11
+ export {initOptionsFromAttributes};
12
+
13
+ /**
14
+ * Initializes the given options object based on the attributes of the current DOM element.
15
+ * The function looks for attributes with the prefix 'data-monster-option-', and maps them to
16
+ * properties in the options object. It replaces the dashes with dots to form the property path.
17
+ * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object.
18
+ *
19
+ * With the mapping parameter, the attribute value can be mapped to a different value.
20
+ * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object.
21
+ *
22
+ * The mapping object would look like this:
23
+ * {
24
+ * 'foo': (value) => value + 'bar'
25
+ * // the value of the attribute 'data-monster-option-foo' is appended with 'bar'
26
+ * // and assigned to the 'bar' property in the options object.
27
+ * // e.g. <div data-monster-option-foo="foo"></div>
28
+ * 'bar.baz': (value) => value + 'bar'
29
+ * // the value of the attribute 'data-monster-option-bar-baz' is appended with 'bar'
30
+ * // and assigned to the 'bar.baz' property in the options object.
31
+ * // e.g. <div data-monster-option-bar-baz="foo"></div>
32
+ * }
33
+ *
34
+ * @since 3.38.0
35
+ * @param {HTMLElement} element - The DOM element to be used as the source of the attributes.
36
+ * @param {Object} options - The options object to be initialized.
37
+ * @param {Object} mapping - A mapping between the attribute value and the property value.
38
+ * @param {string} prefix - The prefix of the attributes to be considered.
39
+ * @returns {Object} - The initialized options object.
40
+ * @this HTMLElement - The context of the DOM element.
41
+ */
42
+ function initOptionsFromAttributes(element, options, mapping = {}, prefix = 'data-monster-option-') {
43
+ if (!(element instanceof HTMLElement)) return options;
44
+ if (!element.hasAttributes()) return options;
45
+
46
+ const keyMap = extractKeys(options);
47
+
48
+ const finder = new Pathfinder(options);
49
+
50
+ element.getAttributeNames().forEach((name) => {
51
+ if (!name.startsWith(prefix)) return;
52
+
53
+ // check if the attribute name is a valid option.
54
+ // the mapping between the attribute is simple. The dash is replaced by a dot.
55
+ // e.g. data-monster-url => url
56
+ const optionName = keyMap.get(name.substring(prefix.length).toLowerCase());
57
+ if (!finder.exists(optionName)) return;
58
+
59
+ if (element.hasAttribute(name)) {
60
+ let value = element.getAttribute(name);
61
+ if (mapping.hasOwnProperty(optionName) && isFunction(mapping[optionName])) {
62
+ value = mapping[optionName](value);
63
+ }
64
+
65
+ const typeOfOptionValue = typeof finder.getVia(optionName);
66
+ if (typeOfOptionValue === 'boolean') {
67
+ value = value === 'true';
68
+ } else if (typeOfOptionValue === 'number') {
69
+ value = Number(value);
70
+ } else if (typeOfOptionValue === 'string') {
71
+ value = String(value);
72
+ } else if (typeOfOptionValue === 'object') {
73
+ value = JSON.parse(value);
74
+ }
75
+
76
+ finder.setVia(optionName, value);
77
+ }
78
+ })
79
+
80
+ return options;
81
+ }
82
+
83
+ /**
84
+ * Extracts the keys from the given object and returns a map with the keys and values.
85
+ *
86
+ * @private
87
+ * @param {object} obj
88
+ * @param {string} keyPrefix
89
+ * @param {string} keySeparator
90
+ * @param {string} valueSeparator
91
+ * @returns {Map<any, any>}
92
+ */
93
+ function extractKeys(obj, keyPrefix = '', keySeparator = '-', valueSeparator = '.') {
94
+ const resultMap = new Map();
95
+
96
+ function helper(currentObj, currentKeyPrefix, currentValuePrefix) {
97
+ for (const key in currentObj) {
98
+ if (typeof currentObj[key] === 'object' && !Array.isArray(currentObj[key])) {
99
+ const newKeyPrefix = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
100
+ const newValuePrefix = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
101
+ helper(currentObj[key], newKeyPrefix, newValuePrefix);
102
+ } else {
103
+ const finalKey = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
104
+ const finalValue = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
105
+ resultMap.set(finalKey, finalValue);
106
+ }
107
+ }
108
+ }
109
+
110
+ helper(obj, keyPrefix, keyPrefix);
111
+ return resultMap;
112
+ }
@@ -142,7 +142,7 @@ function getMonsterVersion() {
142
142
  }
143
143
 
144
144
  /** don't touch, replaced by make with package.json version */
145
- monsterVersion = new Version("3.37.0");
145
+ monsterVersion = new Version("3.38.1");
146
146
 
147
147
  return monsterVersion;
148
148
  }
@@ -8,7 +8,7 @@ import {initJSDOM} from "../../util/jsdom.mjs";
8
8
  describe('Events', function () {
9
9
 
10
10
  before(async function () {
11
- initJSDOM();
11
+ await initJSDOM();
12
12
  })
13
13
 
14
14
  describe('findTargetElementFromEvent()', function () {
@@ -23,10 +23,10 @@ describe('Events', function () {
23
23
  expect(e.getAttribute('data-monster')).to.be.equal('hello')
24
24
  done();
25
25
  })
26
- setTimeout(()=>{
26
+ setTimeout(() => {
27
27
  fireEvent(div, 'click');
28
- },0)
29
-
28
+ }, 0)
29
+
30
30
  });
31
31
 
32
32
  });
@@ -76,10 +76,10 @@ describe('Events', function () {
76
76
 
77
77
  it('should throw error', function () {
78
78
  expect(() => fireEvent({}, 'touch')).to.throw(Error);
79
-
79
+
80
80
  });
81
- });
82
-
81
+ });
82
+
83
83
  describe('fireCustomEvent()', function () {
84
84
  it('should fire a click event', function (done) {
85
85
  let div = document.createElement('div');
@@ -100,8 +100,8 @@ describe('Events', function () {
100
100
  it('should fire a touch event on collection1', function (done) {
101
101
  let div = document.createElement('div');
102
102
  div.addEventListener('touch', (e) => {
103
- if(e.detail.detail!=='hello world') {
104
- done('error');
103
+ if (e.detail.detail !== 'hello world') {
104
+ done('error');
105
105
  }
106
106
  done();
107
107
  })
@@ -111,12 +111,12 @@ describe('Events', function () {
111
111
 
112
112
  fireCustomEvent(collection, 'touch', "hello world");
113
113
  });
114
-
114
+
115
115
  it('should fire a touch event on collection2', function (done) {
116
116
  let div = document.createElement('div');
117
117
  div.addEventListener('touch', (e) => {
118
- if(e.detail.a!=='hello world') {
119
- done('error');
118
+ if (e.detail.a !== 'hello world') {
119
+ done('error');
120
120
  }
121
121
  done();
122
122
  })
@@ -124,7 +124,7 @@ describe('Events', function () {
124
124
  div.appendChild(document.createElement('div'));
125
125
  let collection = div.querySelectorAll('div');
126
126
 
127
- fireCustomEvent(collection, 'touch', {a:"hello world"});
127
+ fireCustomEvent(collection, 'touch', {a: "hello world"});
128
128
  });
129
129
 
130
130
  it('should fire a touch event', function (done) {
@@ -0,0 +1,162 @@
1
+ import {expect} from "chai"
2
+
3
+ import {initOptionsFromAttributes} from "../../../../..//application/source/dom/util/init-options-from-attributes.mjs";
4
+ import {initJSDOM} from "../../../util/jsdom.mjs";
5
+
6
+ describe('initOptionsFromAttributes', () => {
7
+ let element;
8
+ let options;
9
+
10
+ before(async function () {
11
+ await initJSDOM();
12
+ })
13
+
14
+ beforeEach(() => {
15
+ options = { url: "", key: { subkey: "", caseSensitive: true } };
16
+ element = document.createElement('div');
17
+ });
18
+
19
+ it('should initialize options with matching attributes', () => {
20
+ element.setAttribute('data-monster-option-url', 'https://example.com');
21
+ element.setAttribute('data-monster-option-key-subkey', 'test');
22
+
23
+ const result = initOptionsFromAttributes(element, options);
24
+
25
+ expect(result.url).to.equal('https://example.com');
26
+ expect(result.key.subkey).to.equal('test');
27
+ });
28
+
29
+ it('should not modify options without matching attributes', () => {
30
+ const result = initOptionsFromAttributes(element, options);
31
+
32
+ expect(result.url).to.equal('');
33
+ expect(result.key.subkey).to.equal('');
34
+ });
35
+
36
+ it('should ignore attributes without the correct prefix', () => {
37
+ element.setAttribute('data-some-option-url', 'https://example.com');
38
+
39
+ const result = initOptionsFromAttributes(element, options);
40
+
41
+ expect(result.url).to.equal('');
42
+ });
43
+
44
+ it('should ignore attributes with invalid option paths', () => {
45
+ element.setAttribute('data-monster-option-nonexistent', 'value');
46
+
47
+ const result = initOptionsFromAttributes(element, options);
48
+
49
+ expect(result).to.deep.equal(options);
50
+ });
51
+
52
+ it('should apply mapping for a single attribute', () => {
53
+ element.setAttribute('data-monster-option-url', 'example');
54
+ const mapping = {
55
+ 'url': (value) => 'https://' + value + '.com'
56
+ };
57
+
58
+ const result = initOptionsFromAttributes(element, options, mapping);
59
+
60
+ expect(result.url).to.equal('https://example.com');
61
+ });
62
+
63
+ it('should apply mapping for a nested attribute', () => {
64
+ element.setAttribute('data-monster-option-key-subkey', '123');
65
+ const mapping = {
66
+ 'key.subkey': (value) => parseInt(value, 10) * 2
67
+ };
68
+
69
+ const result = initOptionsFromAttributes(element, options, mapping);
70
+
71
+ expect(result.key.subkey).to.equal("246");
72
+ });
73
+
74
+ it('should apply multiple mappings', () => {
75
+ element.setAttribute('data-monster-option-url', 'example');
76
+ element.setAttribute('data-monster-option-key-subkey', '123');
77
+ const mapping = {
78
+ 'url': (value) => 'https://' + value + '.com',
79
+ 'key.subkey': (value) => parseInt(value, 10) * 2
80
+ };
81
+
82
+ const result = initOptionsFromAttributes(element, options, mapping);
83
+
84
+ expect(result.url).to.equal('https://example.com');
85
+ expect(result.key.subkey).to.equal("246");
86
+ });
87
+
88
+ it('should ignore mappings for non-existing attributes', () => {
89
+ const mapping = {
90
+ 'url': (value) => 'https://' + value + '.com'
91
+ };
92
+
93
+ const result = initOptionsFromAttributes(element, options, mapping);
94
+
95
+ expect(result.url).to.equal('');
96
+ });
97
+
98
+ it('should ignore mappings for invalid option paths', () => {
99
+ element.setAttribute('data-monster-option-nonexistent', 'value');
100
+ const mapping = {
101
+ 'nonexistent': (value) => value + 'bar'
102
+ };
103
+
104
+ const result = initOptionsFromAttributes(element, options, mapping);
105
+
106
+ expect(result).to.deep.equal(options);
107
+ });
108
+
109
+ it('should apply mapping only to specified attributes', () => {
110
+ element.setAttribute('data-monster-option-url', 'example');
111
+ element.setAttribute('data-monster-option-key-subkey', '123');
112
+ const mapping = {
113
+ 'url': (value) => 'https://' + value + '.com'
114
+ };
115
+
116
+ const result = initOptionsFromAttributes(element, options, mapping);
117
+
118
+ expect(result.url).to.equal('https://example.com');
119
+ expect(result.key.subkey).to.equal('123');
120
+ });
121
+
122
+ it('should not apply mapping if not a function', () => {
123
+ element.setAttribute('data-monster-option-url', 'example');
124
+ const mapping = {
125
+ 'url': 'https://example.com'
126
+ };
127
+
128
+ const result = initOptionsFromAttributes(element, options, mapping);
129
+
130
+ expect(result.url).to.equal('example');
131
+ });
132
+
133
+ it('should apply mapping with custom prefix', () => {
134
+ element.setAttribute('data-custom-option-url', 'example');
135
+ const mapping = {
136
+ 'url': (value) => 'https://' + value + '.com'
137
+ };
138
+
139
+ const result = initOptionsFromAttributes(element, options, mapping, 'data-custom-option-');
140
+
141
+ expect(result.url).to.equal('https://example.com');
142
+ });
143
+
144
+ it('should not apply mapping with incorrect custom prefix', () => {
145
+ element.setAttribute('data-custom-option-url', 'example');
146
+ const mapping = {
147
+ 'url': (value) => 'https://' + value + '.com'
148
+ };
149
+
150
+ const result = initOptionsFromAttributes(element, options, mapping);
151
+
152
+ expect(result.url).to.equal('');
153
+ });
154
+
155
+ it('should apply case sensitive mapping', () => {
156
+ element.setAttribute('data-monster-option-key-caseSensitive', 'false');
157
+ const result = initOptionsFromAttributes(element, options);
158
+
159
+ expect(result.key.caseSensitive).to.equal(false);
160
+ });
161
+
162
+ });
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("3.37.0")
10
+ monsterVersion = new Version("3.38.1")
11
11
 
12
12
  let m = getMonsterVersion();
13
13