@schukai/monster 3.44.1 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/dom/constants.mjs +33 -0
- package/source/dom/customcontrol.mjs +55 -14
- package/source/dom/customelement.mjs +115 -4
- package/source/dom/util/extract-keys.mjs +39 -0
- package/source/dom/util/init-options-from-attributes.mjs +1 -53
- package/source/dom/util/set-option-from-attribute.mjs +83 -0
- package/source/types/version.mjs +1 -1
- package/test/cases/dom/customcontrol.mjs +48 -41
- package/test/cases/dom/customelement-initfromscripthost.mjs +143 -0
- package/test/cases/dom/customelement.mjs +6 -6
- package/test/cases/dom/focusmanager.mjs +0 -1
- package/test/cases/monster.mjs +1 -1
- package/test/util/cleanupdom.mjs +0 -4
package/package.json
CHANGED
package/source/dom/constants.mjs
CHANGED
|
@@ -59,6 +59,9 @@ export {
|
|
|
59
59
|
ATTRIBUTE_HIDDEN,
|
|
60
60
|
objectUpdaterLinkSymbol,
|
|
61
61
|
customElementUpdaterLinkSymbol,
|
|
62
|
+
optionCallbackName,
|
|
63
|
+
ATTRIBUTE_SCRIPT_HOST,
|
|
64
|
+
ATTRIBUTE_OPTION_CALLBACK
|
|
62
65
|
};
|
|
63
66
|
|
|
64
67
|
/**
|
|
@@ -86,6 +89,16 @@ const ATTRIBUTE_PREFIX = "data-monster-";
|
|
|
86
89
|
*/
|
|
87
90
|
const ATTRIBUTE_OPTIONS = `${ATTRIBUTE_PREFIX}options`;
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* This is name of the attribute to pass the script host to a control
|
|
94
|
+
*
|
|
95
|
+
* @memberOf Monster.DOM
|
|
96
|
+
* @license AGPLv3
|
|
97
|
+
* @since 3.48.0
|
|
98
|
+
* @type {string}
|
|
99
|
+
*/
|
|
100
|
+
const ATTRIBUTE_SCRIPT_HOST = `${ATTRIBUTE_PREFIX}script-host`;
|
|
101
|
+
|
|
89
102
|
/**
|
|
90
103
|
* This is the name of the attribute to pass options to a control
|
|
91
104
|
*
|
|
@@ -96,6 +109,26 @@ const ATTRIBUTE_OPTIONS = `${ATTRIBUTE_PREFIX}options`;
|
|
|
96
109
|
*/
|
|
97
110
|
const ATTRIBUTE_OPTIONS_SELECTOR = `${ATTRIBUTE_PREFIX}options-selector`;
|
|
98
111
|
|
|
112
|
+
/**
|
|
113
|
+
* This is the name of the attribute to pass the callback to a control
|
|
114
|
+
*
|
|
115
|
+
* @memberOf Monster.DOM
|
|
116
|
+
* @license AGPLv3
|
|
117
|
+
* @since 3.48.0
|
|
118
|
+
* @type {string}
|
|
119
|
+
*/
|
|
120
|
+
const ATTRIBUTE_OPTION_CALLBACK = `${ATTRIBUTE_PREFIX}option-callback`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* This is the name of the callback to pass the callback to a control
|
|
124
|
+
*
|
|
125
|
+
* @memberOf Monster.DOM
|
|
126
|
+
* @license AGPLv3
|
|
127
|
+
* @since 3.48.0
|
|
128
|
+
* @type {string}
|
|
129
|
+
*/
|
|
130
|
+
const optionCallbackName = `initCustomControlOptionsCallback`;
|
|
131
|
+
|
|
99
132
|
/**
|
|
100
133
|
* @memberOf Monster.DOM
|
|
101
134
|
* @type {string}
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
8
|
+
import {extend} from "../data/extend.mjs";
|
|
9
|
+
import {ATTRIBUTE_VALUE} from "./constants.mjs";
|
|
10
|
+
import {CustomElement, attributeObserverSymbol} from "./customelement.mjs";
|
|
11
|
+
import {instanceSymbol} from "../constants.mjs";
|
|
12
|
+
|
|
13
|
+
export {CustomControl};
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @private
|
|
@@ -41,6 +42,7 @@ const attachedInternalSymbol = Symbol("attachedInternal");
|
|
|
41
42
|
* @see {@link https://www.npmjs.com/package/element-internals-polyfill}
|
|
42
43
|
* @see {@link https://github.com/WICG/webcomponents}
|
|
43
44
|
* @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements}
|
|
45
|
+
* @see {@link https://html.spec.whatwg.org/dev/custom-elements.html#custom-element-reactions}
|
|
44
46
|
* @license AGPLv3
|
|
45
47
|
* @since 1.14.0
|
|
46
48
|
* @copyright schukai GmbH
|
|
@@ -74,7 +76,7 @@ class CustomControl extends CustomElement {
|
|
|
74
76
|
* @since 2.1.0
|
|
75
77
|
*/
|
|
76
78
|
static get [instanceSymbol]() {
|
|
77
|
-
return Symbol.for("@schukai/monster/dom/custom-control");
|
|
79
|
+
return Symbol.for("@schukai/monster/dom/custom-control@@instance");
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -84,20 +86,18 @@ class CustomControl extends CustomElement {
|
|
|
84
86
|
* @since 1.15.0
|
|
85
87
|
*/
|
|
86
88
|
static get observedAttributes() {
|
|
87
|
-
|
|
88
|
-
list.push(ATTRIBUTE_VALUE);
|
|
89
|
-
return list;
|
|
89
|
+
return super.observedAttributes;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
+
* Adding a static formAssociated property, with a true value, makes an autonomous custom element a form-associated custom element.
|
|
93
94
|
*
|
|
94
95
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
|
|
96
|
+
* @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example}
|
|
95
97
|
* @since 1.14.0
|
|
96
98
|
* @return {boolean}
|
|
97
99
|
*/
|
|
98
|
-
static
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
100
|
+
static formAssociated = true
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Derived classes can override and extend this method as follows.
|
|
@@ -116,7 +116,8 @@ class CustomControl extends CustomElement {
|
|
|
116
116
|
* @since 1.14.0
|
|
117
117
|
*/
|
|
118
118
|
get defaults() {
|
|
119
|
-
return extend({
|
|
119
|
+
return extend({
|
|
120
|
+
}, super.defaults);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
/**
|
|
@@ -298,6 +299,46 @@ class CustomControl extends CustomElement {
|
|
|
298
299
|
reportValidity() {
|
|
299
300
|
return getInternal.call(this)?.reportValidity();
|
|
300
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {string} form
|
|
305
|
+
*/
|
|
306
|
+
formAssociatedCallback(form) {
|
|
307
|
+
if (form) {
|
|
308
|
+
if(form.id) {
|
|
309
|
+
this.setAttribute("form", form.id);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
this.removeAttribute("form");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {string} disabled
|
|
318
|
+
*/
|
|
319
|
+
formDisabledCallback(disabled) {
|
|
320
|
+
if (disabled) {
|
|
321
|
+
this.setAttribute("disabled", "");
|
|
322
|
+
} else {
|
|
323
|
+
this.removeAttribute("disabled");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} state
|
|
329
|
+
* @param {string} mode
|
|
330
|
+
*/
|
|
331
|
+
formStateRestoreCallback(state, mode) {
|
|
332
|
+
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
*
|
|
337
|
+
*/
|
|
338
|
+
formResetCallback() {
|
|
339
|
+
this.value = "";
|
|
340
|
+
}
|
|
341
|
+
|
|
301
342
|
}
|
|
302
343
|
|
|
303
344
|
/**
|
|
@@ -313,7 +354,7 @@ function getInternal() {
|
|
|
313
354
|
throw new Error("ElementInternals is not supported and a polyfill is necessary");
|
|
314
355
|
}
|
|
315
356
|
|
|
316
|
-
return
|
|
357
|
+
return self[attachedInternalSymbol];
|
|
317
358
|
}
|
|
318
359
|
|
|
319
360
|
/**
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {findElementWithIdUpwards} from "./util.mjs";
|
|
8
9
|
import {internalSymbol} from "../constants.mjs";
|
|
9
10
|
import {extend} from "../data/extend.mjs";
|
|
10
11
|
import {Pathfinder} from "../data/pathfinder.mjs";
|
|
@@ -22,8 +23,11 @@ import {
|
|
|
22
23
|
ATTRIBUTE_DISABLED,
|
|
23
24
|
ATTRIBUTE_ERRORMESSAGE,
|
|
24
25
|
ATTRIBUTE_OPTIONS,
|
|
26
|
+
ATTRIBUTE_OPTION_CALLBACK,
|
|
25
27
|
ATTRIBUTE_OPTIONS_SELECTOR,
|
|
28
|
+
ATTRIBUTE_SCRIPT_HOST,
|
|
26
29
|
customElementUpdaterLinkSymbol,
|
|
30
|
+
optionCallbackName
|
|
27
31
|
} from "./constants.mjs";
|
|
28
32
|
import {findDocumentTemplate, Template} from "./template.mjs";
|
|
29
33
|
import {addObjectWithUpdaterToElement} from "./updater.mjs";
|
|
@@ -31,6 +35,7 @@ import {instanceSymbol} from "../constants.mjs";
|
|
|
31
35
|
import {getDocumentTranslations, Translations} from "../i18n/translations.mjs";
|
|
32
36
|
import {getSlottedElements} from "./slotted.mjs";
|
|
33
37
|
import {initOptionsFromAttributes} from "./util/init-options-from-attributes.mjs";
|
|
38
|
+
import {setOptionFromAttribute} from "./util/set-option-from-attribute.mjs";
|
|
34
39
|
|
|
35
40
|
export {
|
|
36
41
|
CustomElement,
|
|
@@ -66,6 +71,12 @@ const attributeObserverSymbol = Symbol.for("@schukai/monster/dom/@@attributeObse
|
|
|
66
71
|
*/
|
|
67
72
|
const attributeMutationObserverSymbol = Symbol("@schukai/monster/dom/@@mutationObserver");
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @private
|
|
76
|
+
* @type {symbol}
|
|
77
|
+
*/
|
|
78
|
+
const scriptHostElementSymbol = Symbol("scriptHostElement");
|
|
79
|
+
|
|
69
80
|
/**
|
|
70
81
|
* HTMLElement
|
|
71
82
|
* @external HTMLElement
|
|
@@ -211,6 +222,7 @@ class CustomElement extends HTMLElement {
|
|
|
211
222
|
});
|
|
212
223
|
this[initMethodSymbol]();
|
|
213
224
|
initOptionObserver.call(this);
|
|
225
|
+
this[scriptHostElementSymbol] = [];
|
|
214
226
|
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -220,7 +232,7 @@ class CustomElement extends HTMLElement {
|
|
|
220
232
|
* @since 2.1.0
|
|
221
233
|
*/
|
|
222
234
|
static get [instanceSymbol]() {
|
|
223
|
-
return Symbol.for("@schukai/monster/dom/custom-element");
|
|
235
|
+
return Symbol.for("@schukai/monster/dom/custom-element@@instance");
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
|
@@ -232,7 +244,7 @@ class CustomElement extends HTMLElement {
|
|
|
232
244
|
* @since 1.15.0
|
|
233
245
|
*/
|
|
234
246
|
static get observedAttributes() {
|
|
235
|
-
return [
|
|
247
|
+
return [];
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
/**
|
|
@@ -309,7 +321,7 @@ class CustomElement extends HTMLElement {
|
|
|
309
321
|
*/
|
|
310
322
|
get defaults() {
|
|
311
323
|
return {
|
|
312
|
-
|
|
324
|
+
disabled: false,
|
|
313
325
|
shadowMode: "open",
|
|
314
326
|
delegatesFocus: true,
|
|
315
327
|
templates: {
|
|
@@ -519,6 +531,7 @@ class CustomElement extends HTMLElement {
|
|
|
519
531
|
self.setOptions(ScriptOptions);
|
|
520
532
|
}
|
|
521
533
|
|
|
534
|
+
|
|
522
535
|
if (self.getOption("shadowMode", false) !== false) {
|
|
523
536
|
try {
|
|
524
537
|
initShadowRoot.call(self);
|
|
@@ -540,6 +553,8 @@ class CustomElement extends HTMLElement {
|
|
|
540
553
|
}
|
|
541
554
|
}
|
|
542
555
|
|
|
556
|
+
initFromCallbackHost.call(this);
|
|
557
|
+
|
|
543
558
|
try {
|
|
544
559
|
nodeList = new Set([...elements, ...getSlottedElements.call(self)]);
|
|
545
560
|
} catch (e) {
|
|
@@ -605,8 +620,11 @@ class CustomElement extends HTMLElement {
|
|
|
605
620
|
attributeChangedCallback(attrName, oldVal, newVal) {
|
|
606
621
|
const self = this;
|
|
607
622
|
|
|
608
|
-
|
|
623
|
+
if (attrName.startsWith("data-monster-option-")) {
|
|
624
|
+
setOptionFromAttribute(self, attrName, this[internalSymbol].getSubject()["options"])
|
|
625
|
+
}
|
|
609
626
|
|
|
627
|
+
const callback = self[attributeObserverSymbol]?.[attrName];
|
|
610
628
|
if (isFunction(callback)) {
|
|
611
629
|
try {
|
|
612
630
|
callback.call(self, newVal, oldVal);
|
|
@@ -637,6 +655,99 @@ class CustomElement extends HTMLElement {
|
|
|
637
655
|
|
|
638
656
|
return containChildNode.call(self.shadowRoot, node);
|
|
639
657
|
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Calls a callback function if it exists.
|
|
661
|
+
*
|
|
662
|
+
* @param {string} name
|
|
663
|
+
* @param {*} args
|
|
664
|
+
* @returns {*}
|
|
665
|
+
*/
|
|
666
|
+
callCallback(name, args) {
|
|
667
|
+
const self = this;
|
|
668
|
+
return callControlCallback.call(self, name, ...args);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* @param {string} callBackFunctionName
|
|
676
|
+
* @param {*} args
|
|
677
|
+
* @return {any}
|
|
678
|
+
*/
|
|
679
|
+
function callControlCallback(callBackFunctionName, ...args) {
|
|
680
|
+
const self = this;
|
|
681
|
+
|
|
682
|
+
if (!isString(callBackFunctionName) || callBackFunctionName === "") {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (callBackFunctionName in self) {
|
|
687
|
+
return self[callBackFunctionName](self, ...args);
|
|
688
|
+
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!self.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (self[scriptHostElementSymbol].length === 0) {
|
|
696
|
+
|
|
697
|
+
const targetId = self.getAttribute(ATTRIBUTE_SCRIPT_HOST);
|
|
698
|
+
if (!targetId) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const list = targetId.split(",")
|
|
703
|
+
for (const id of list) {
|
|
704
|
+
const host = findElementWithIdUpwards(self, targetId);
|
|
705
|
+
if (!(host instanceof HTMLElement)) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
self[scriptHostElementSymbol].push(host);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const host of self[scriptHostElementSymbol]) {
|
|
714
|
+
if (callBackFunctionName in host) {
|
|
715
|
+
try {
|
|
716
|
+
return host[callBackFunctionName](self, ...args);
|
|
717
|
+
} catch (e) {
|
|
718
|
+
addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, e.toString());
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
addAttributeToken(self, ATTRIBUTE_ERRORMESSAGE, `callback ${callBackFunctionName} not found`);
|
|
724
|
+
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* This Function is called when the element is attached to the DOM.
|
|
729
|
+
*
|
|
730
|
+
* It looks for the attribute `data-monster-option-callback`. Is this attribute is not set, the default callback
|
|
731
|
+
* `initCustomControlOptionsCallback` is called.
|
|
732
|
+
*
|
|
733
|
+
* The callback is searched in this element and in the host element. If the callback is found, it is called with the
|
|
734
|
+
* element as parameter.
|
|
735
|
+
*
|
|
736
|
+
* The `monster
|
|
737
|
+
*
|
|
738
|
+
* @this CustomElement
|
|
739
|
+
*/
|
|
740
|
+
function initFromCallbackHost() {
|
|
741
|
+
const self = this;
|
|
742
|
+
|
|
743
|
+
let callBackFunctionName = optionCallbackName // default callback
|
|
744
|
+
if (self.hasAttribute(ATTRIBUTE_OPTION_CALLBACK)) {
|
|
745
|
+
callBackFunctionName = self.getAttribute(ATTRIBUTE_OPTION_CALLBACK);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
callControlCallback.call(self, callBackFunctionName);
|
|
749
|
+
|
|
750
|
+
|
|
640
751
|
}
|
|
641
752
|
|
|
642
753
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export {extractKeys}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts the keys from the given object and returns a map with the keys and values.
|
|
12
|
+
*
|
|
13
|
+
* @private
|
|
14
|
+
* @param {object} obj
|
|
15
|
+
* @param {string} keyPrefix
|
|
16
|
+
* @param {string} keySeparator
|
|
17
|
+
* @param {string} valueSeparator
|
|
18
|
+
* @returns {Map<any, any>}
|
|
19
|
+
*/
|
|
20
|
+
function extractKeys(obj, keyPrefix = '', keySeparator = '-', valueSeparator = '.') {
|
|
21
|
+
const resultMap = new Map();
|
|
22
|
+
|
|
23
|
+
function helper(currentObj, currentKeyPrefix, currentValuePrefix) {
|
|
24
|
+
for (const key in currentObj) {
|
|
25
|
+
if (typeof currentObj[key] === 'object' && !Array.isArray(currentObj[key])) {
|
|
26
|
+
const newKeyPrefix = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
|
|
27
|
+
const newValuePrefix = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
|
|
28
|
+
helper(currentObj[key], newKeyPrefix, newValuePrefix);
|
|
29
|
+
} else {
|
|
30
|
+
const finalKey = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
|
|
31
|
+
const finalValue = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
|
|
32
|
+
resultMap.set(finalKey, finalValue);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
helper(obj, keyPrefix, keyPrefix);
|
|
38
|
+
return resultMap;
|
|
39
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {Pathfinder} from '../../data/pathfinder.mjs';
|
|
9
9
|
import {isFunction} from '../../types/is.mjs';
|
|
10
10
|
import {attributeObserverSymbol} from "../customelement.mjs";
|
|
11
|
+
import {extractKeys} from "./extract-keys.mjs";
|
|
11
12
|
|
|
12
13
|
export {initOptionsFromAttributes};
|
|
13
14
|
|
|
@@ -75,63 +76,10 @@ function initOptionsFromAttributes(element, options, mapping = {}, prefix = 'dat
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
finder.setVia(optionName, value);
|
|
78
|
-
|
|
79
|
-
// if element has an attribute observer, then register the attribute observer
|
|
80
|
-
if (element?.[attributeObserverSymbol]) {
|
|
81
|
-
element[attributeObserverSymbol][name] = (newValue, oldValue) => {
|
|
82
|
-
|
|
83
|
-
if (newValue === oldValue) return;
|
|
84
|
-
|
|
85
|
-
let changedValue = newValue;
|
|
86
|
-
|
|
87
|
-
if (typeOfOptionValue === 'boolean') {
|
|
88
|
-
changedValue = changedValue === 'true';
|
|
89
|
-
} else if (typeOfOptionValue === 'number') {
|
|
90
|
-
changedValue = Number(changedValue);
|
|
91
|
-
} else if (typeOfOptionValue === 'string') {
|
|
92
|
-
changedValue = String(changedValue);
|
|
93
|
-
} else if (typeOfOptionValue === 'object') {
|
|
94
|
-
changedValue = JSON.parse(changedValue);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
finder.setVia(optionName, changedValue);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
79
|
}
|
|
103
80
|
})
|
|
104
81
|
|
|
105
82
|
return options;
|
|
106
83
|
}
|
|
107
84
|
|
|
108
|
-
/**
|
|
109
|
-
* Extracts the keys from the given object and returns a map with the keys and values.
|
|
110
|
-
*
|
|
111
|
-
* @private
|
|
112
|
-
* @param {object} obj
|
|
113
|
-
* @param {string} keyPrefix
|
|
114
|
-
* @param {string} keySeparator
|
|
115
|
-
* @param {string} valueSeparator
|
|
116
|
-
* @returns {Map<any, any>}
|
|
117
|
-
*/
|
|
118
|
-
function extractKeys(obj, keyPrefix = '', keySeparator = '-', valueSeparator = '.') {
|
|
119
|
-
const resultMap = new Map();
|
|
120
85
|
|
|
121
|
-
function helper(currentObj, currentKeyPrefix, currentValuePrefix) {
|
|
122
|
-
for (const key in currentObj) {
|
|
123
|
-
if (typeof currentObj[key] === 'object' && !Array.isArray(currentObj[key])) {
|
|
124
|
-
const newKeyPrefix = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
|
|
125
|
-
const newValuePrefix = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
|
|
126
|
-
helper(currentObj[key], newKeyPrefix, newValuePrefix);
|
|
127
|
-
} else {
|
|
128
|
-
const finalKey = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase();
|
|
129
|
-
const finalValue = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key;
|
|
130
|
-
resultMap.set(finalKey, finalValue);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
helper(obj, keyPrefix, keyPrefix);
|
|
136
|
-
return resultMap;
|
|
137
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
import {attributeObserverSymbol} from "../customelement.mjs";
|
|
11
|
+
import {extractKeys} from "./extract-keys.mjs";
|
|
12
|
+
|
|
13
|
+
export {setOptionFromAttribute};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set the given options object based on the attributes of the current DOM element.
|
|
17
|
+
* The function looks for attributes with the prefix 'data-monster-option-', and maps them to
|
|
18
|
+
* properties in the options object. It replaces the dashes with dots to form the property path.
|
|
19
|
+
* For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object.
|
|
20
|
+
*
|
|
21
|
+
* With the mapping parameter, the attribute value can be mapped to a different value.
|
|
22
|
+
* For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object.
|
|
23
|
+
*
|
|
24
|
+
* The mapping object would look like this:
|
|
25
|
+
* {
|
|
26
|
+
* 'foo': (value) => value + 'bar'
|
|
27
|
+
* // the value of the attribute 'data-monster-option-foo' is appended with 'bar'
|
|
28
|
+
* // and assigned to the 'bar' property in the options object.
|
|
29
|
+
* // e.g. <div data-monster-option-foo="foo"></div>
|
|
30
|
+
* 'bar.baz': (value) => value + 'bar'
|
|
31
|
+
* // the value of the attribute 'data-monster-option-bar-baz' is appended with 'bar'
|
|
32
|
+
* // and assigned to the 'bar.baz' property in the options object.
|
|
33
|
+
* // e.g. <div data-monster-option-bar-baz="foo"></div>
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* @since 3.45.0
|
|
37
|
+
* @param {HTMLElement} element - The DOM element to be used as the source of the attributes.
|
|
38
|
+
* @param {Object} name - The attribute object to be used as the source of the attributes.
|
|
39
|
+
* @param {Object} options - The options object to be initialized.
|
|
40
|
+
* @param {Object} mapping - A mapping between the attribute value and the property value.
|
|
41
|
+
* @param {string} prefix - The prefix of the attributes to be considered.
|
|
42
|
+
* @returns {Object} - The initialized options object.
|
|
43
|
+
* @this HTMLElement - The context of the DOM element.
|
|
44
|
+
*/
|
|
45
|
+
function setOptionFromAttribute(element, name, options, mapping = {}, prefix = 'data-monster-option-') {
|
|
46
|
+
if (!(element instanceof HTMLElement)) return options;
|
|
47
|
+
if (!element.hasAttributes()) return options;
|
|
48
|
+
|
|
49
|
+
const keyMap = extractKeys(options);
|
|
50
|
+
const finder = new Pathfinder(options);
|
|
51
|
+
|
|
52
|
+
// check if the attribute name is a valid option.
|
|
53
|
+
// the mapping between the attribute is simple. The dash is replaced by a dot.
|
|
54
|
+
// e.g. data-monster-url => url
|
|
55
|
+
const optionName = keyMap.get(name.substring(prefix.length).toLowerCase());
|
|
56
|
+
if (!finder.exists(optionName)) return;
|
|
57
|
+
|
|
58
|
+
if (!element.hasAttribute(name)) {
|
|
59
|
+
return options;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let value = element.getAttribute(name);
|
|
63
|
+
if (mapping.hasOwnProperty(optionName) && isFunction(mapping[optionName])) {
|
|
64
|
+
value = mapping[optionName](value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const typeOfOptionValue = typeof finder.getVia(optionName);
|
|
68
|
+
if (typeOfOptionValue === 'boolean') {
|
|
69
|
+
value = value === 'true';
|
|
70
|
+
} else if (typeOfOptionValue === 'number') {
|
|
71
|
+
value = Number(value);
|
|
72
|
+
} else if (typeOfOptionValue === 'string') {
|
|
73
|
+
value = String(value);
|
|
74
|
+
} else if (typeOfOptionValue === 'object') {
|
|
75
|
+
value = JSON.parse(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
finder.setVia(optionName, value);
|
|
79
|
+
|
|
80
|
+
return options;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
package/source/types/version.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import chai from "chai"
|
|
|
4
4
|
import {ATTRIBUTE_OPTIONS} from "../../../../application/source/dom/constants.mjs";
|
|
5
5
|
import {getDocument} from "../../../../application/source/dom/util.mjs";
|
|
6
6
|
import {chaiDom} from "../../util/chai-dom.mjs";
|
|
7
|
+
import {cleanupDOMFromTesting, initMutationObserverForTesting} from "../../util/cleanupdom.mjs";
|
|
7
8
|
import {initJSDOM} from "../../util/jsdom.mjs";
|
|
8
9
|
|
|
9
|
-
|
|
10
10
|
let expect = chai.expect;
|
|
11
11
|
chai.use(chaiDom);
|
|
12
12
|
|
|
@@ -19,45 +19,46 @@ describe('DOM', function () {
|
|
|
19
19
|
|
|
20
20
|
let CustomControl, registerCustomElement, TestComponent, document, jsdomFlag;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
before(function (done) {
|
|
25
|
-
initJSDOM().then(() => {
|
|
22
|
+
before(function (done) {
|
|
23
|
+
initJSDOM().then(() => {
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// jsdom does not support ElementInternals
|
|
26
|
+
jsdomFlag = navigator.userAgent.includes("jsdom");
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
import("../../../../application/source/dom/customelement.mjs").then((m) => {
|
|
29
|
+
registerCustomElement = m['registerCustomElement'];
|
|
32
30
|
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
import("../../../../application/source/dom/customcontrol.mjs").then((m) => {
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
document = getDocument();
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
try {
|
|
37
|
+
CustomControl = m['CustomControl'];
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
39
|
+
TestComponent = class extends CustomControl {
|
|
40
|
+
static getTag() {
|
|
41
|
+
return "monster-customcontrol"
|
|
45
42
|
}
|
|
46
|
-
|
|
43
|
+
}
|
|
44
|
+
registerCustomElement(TestComponent)
|
|
47
45
|
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
done()
|
|
48
|
+
} catch (e) {
|
|
49
|
+
done(e);
|
|
50
|
+
}
|
|
53
51
|
|
|
54
52
|
|
|
55
|
-
});
|
|
56
53
|
});
|
|
57
54
|
});
|
|
58
|
-
})
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('CustomControl()', function () {
|
|
59
59
|
|
|
60
60
|
beforeEach(() => {
|
|
61
|
+
initMutationObserverForTesting()
|
|
61
62
|
let mocks = document.getElementById('mocks');
|
|
62
63
|
mocks.innerHTML = html1;
|
|
63
64
|
})
|
|
@@ -65,6 +66,8 @@ describe('DOM', function () {
|
|
|
65
66
|
afterEach(() => {
|
|
66
67
|
let mocks = document.getElementById('mocks');
|
|
67
68
|
mocks.innerHTML = "";
|
|
69
|
+
cleanupDOMFromTesting();
|
|
70
|
+
|
|
68
71
|
})
|
|
69
72
|
|
|
70
73
|
describe('create', function () {
|
|
@@ -76,28 +79,33 @@ describe('DOM', function () {
|
|
|
76
79
|
|
|
77
80
|
describe('connect empty element', function () {
|
|
78
81
|
it('document should contain monster-customcontrol', function () {
|
|
79
|
-
|
|
82
|
+
|
|
80
83
|
let d = document.createElement('monster-customcontrol');
|
|
81
84
|
document.getElementById('test1').appendChild(d);
|
|
82
85
|
expect(document.getElementsByTagName('monster-customcontrol').length).is.equal(1);
|
|
83
|
-
// no data-monster-objectlink="Symbol(monsterUpdater)" because it has nothing to update
|
|
86
|
+
// no data-monster-objectlink="Symbol(monsterUpdater)" because it has nothing to update
|
|
84
87
|
expect(document.getElementById('test1')).contain.html('<monster-customcontrol></monster-customcontrol>');
|
|
85
88
|
});
|
|
86
89
|
});
|
|
87
90
|
|
|
88
|
-
describe('Options change', function () {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
})
|
|
91
|
+
// describe('Options change', function () {
|
|
92
|
+
//
|
|
93
|
+
// it('delegatesFocus should change from true to false', function (done) {
|
|
94
|
+
// let element = document.createElement('monster-customcontrol')
|
|
95
|
+
//
|
|
96
|
+
// expect(element.getOption('delegatesFocus')).to.be.true;
|
|
97
|
+
// setTimeout(() => {
|
|
98
|
+
// element.setAttribute(ATTRIBUTE_OPTIONS, JSON.stringify({delegatesFocus: false}));
|
|
99
|
+
// setTimeout(() => {
|
|
100
|
+
// expect(element.getOption('delegatesFocus')).to.be.false;
|
|
101
|
+
// done();
|
|
102
|
+
// }, 10);
|
|
103
|
+
// }, 10);
|
|
104
|
+
//
|
|
105
|
+
//
|
|
106
|
+
// }).timeout(100);
|
|
107
|
+
//
|
|
108
|
+
// })
|
|
101
109
|
|
|
102
110
|
describe('Test ElementInternals', function () {
|
|
103
111
|
|
|
@@ -115,6 +123,7 @@ describe('DOM', function () {
|
|
|
115
123
|
expect(d.constructor.formAssociated).to.be.true;
|
|
116
124
|
|
|
117
125
|
});
|
|
126
|
+
|
|
118
127
|
it('form', function () {
|
|
119
128
|
|
|
120
129
|
let d = document.createElement('monster-customcontrol');
|
|
@@ -147,7 +156,6 @@ describe('DOM', function () {
|
|
|
147
156
|
|
|
148
157
|
});
|
|
149
158
|
|
|
150
|
-
|
|
151
159
|
it('setFormValue', function () {
|
|
152
160
|
|
|
153
161
|
let d = document.createElement('monster-customcontrol');
|
|
@@ -161,7 +169,6 @@ describe('DOM', function () {
|
|
|
161
169
|
|
|
162
170
|
});
|
|
163
171
|
|
|
164
|
-
|
|
165
172
|
it('name getter', function () {
|
|
166
173
|
|
|
167
174
|
let d = document.createElement('monster-customcontrol');
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import chai from "chai"
|
|
4
|
+
// import {internalSymbol} from "../../../../application/source/constants.mjs";
|
|
5
|
+
// import {ATTRIBUTE_OPTIONS} from "../../../../application/source/dom/constants.mjs";
|
|
6
|
+
import {getDocument} from "../../../../application/source/dom/util.mjs";
|
|
7
|
+
// import {ProxyObserver} from "../../../../application/source/types/proxyobserver.mjs";
|
|
8
|
+
// import {addObjectWithUpdaterToElement} from "../../../../application/source/dom/updater.mjs";
|
|
9
|
+
import {chaiDom} from "../../util/chai-dom.mjs";
|
|
10
|
+
import {initJSDOM} from "../../util/jsdom.mjs";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let expect = chai.expect;
|
|
14
|
+
chai.use(chaiDom);
|
|
15
|
+
|
|
16
|
+
// let html1 = `
|
|
17
|
+
// <div id="scripthost">
|
|
18
|
+
// </div>
|
|
19
|
+
//
|
|
20
|
+
// <div>
|
|
21
|
+
// <
|
|
22
|
+
// </div>
|
|
23
|
+
// `;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// defined in constants.mjs
|
|
27
|
+
// const updaterSymbolKey = "@schukai/monster/dom/custom-element@@options-updater-link"
|
|
28
|
+
// const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
describe('DOM', function () {
|
|
33
|
+
|
|
34
|
+
let CustomElement, registerCustomElement, TestComponent, document, TestComponent2,assignUpdaterToElement;
|
|
35
|
+
|
|
36
|
+
describe('initFromScriptHost()', function () {
|
|
37
|
+
|
|
38
|
+
const randomTagNumber = "monster-test"+Math.floor(Math.random() * 1000000);
|
|
39
|
+
|
|
40
|
+
before(function (done) {
|
|
41
|
+
initJSDOM().then(() => {
|
|
42
|
+
|
|
43
|
+
import("../../../../application/source/dom/customelement.mjs").then((m) => {
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
CustomElement = m['CustomElement'];
|
|
47
|
+
registerCustomElement = m['registerCustomElement'];
|
|
48
|
+
|
|
49
|
+
TestComponent2 = class extends CustomElement {
|
|
50
|
+
static getTag() {
|
|
51
|
+
return randomTagNumber;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @return {Object}
|
|
57
|
+
*/
|
|
58
|
+
get defaults() {
|
|
59
|
+
|
|
60
|
+
return Object.assign({}, super.defaults, {
|
|
61
|
+
test: 0,
|
|
62
|
+
templates: {
|
|
63
|
+
main: '<h1></h1><article><p>test</p><div id="container"></div></article>'
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registerCustomElement(TestComponent2)
|
|
71
|
+
|
|
72
|
+
document = getDocument();
|
|
73
|
+
done()
|
|
74
|
+
} catch (e) {
|
|
75
|
+
done(e);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
}).catch((e) => {
|
|
80
|
+
done(e);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
});
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
let mocks = document.getElementById('mocks');
|
|
88
|
+
mocks.innerHTML = "";
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('call callback', function () {
|
|
92
|
+
it('should not found callback and add error attribute', function () {
|
|
93
|
+
|
|
94
|
+
let mocks = document.getElementById('mocks');
|
|
95
|
+
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
|
|
96
|
+
|
|
97
|
+
let control = document.createElement(randomTagNumber);
|
|
98
|
+
control.setAttribute('data-monster-script-host', "call-back-host");
|
|
99
|
+
document.getElementById('container').appendChild(control);
|
|
100
|
+
expect(control.getOption('test')).is.eql(0);
|
|
101
|
+
expect(control.hasAttribute('data-monster-error')).is.true;
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should found callback initCustomControlOptionsCallback', function () {
|
|
106
|
+
|
|
107
|
+
let mocks = document.getElementById('mocks');
|
|
108
|
+
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
|
|
109
|
+
|
|
110
|
+
const container = document.getElementById('call-back-host');
|
|
111
|
+
container.initCustomControlOptionsCallback = function (control) {
|
|
112
|
+
control.setOption('test', 1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let control = document.createElement(randomTagNumber);
|
|
116
|
+
control.setAttribute('data-monster-script-host', "call-back-host");
|
|
117
|
+
document.getElementById('container').appendChild(control);
|
|
118
|
+
expect(control.getOption('test')).is.eql(1);
|
|
119
|
+
expect(control.hasAttribute('data-monster-error')).is.false;
|
|
120
|
+
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should found callback initCustomControlOptionsCallback from self', function () {
|
|
124
|
+
|
|
125
|
+
let mocks = document.getElementById('mocks');
|
|
126
|
+
mocks.innerHTML = `<div id="call-back-host"></div><div id="container"></div>`;
|
|
127
|
+
|
|
128
|
+
let control = document.createElement(randomTagNumber);
|
|
129
|
+
expect(control.getOption('test')).is.eql(0);
|
|
130
|
+
control.initCustomControlOptionsCallback = function (control) {
|
|
131
|
+
control.setOption('test', 2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
control.setAttribute('data-monster-script-host', "call-back-host");
|
|
135
|
+
document.getElementById('container').appendChild(control);
|
|
136
|
+
expect(control.getOption('test')).is.eql(2);
|
|
137
|
+
expect(control.hasAttribute('data-monster-error')).is.false;
|
|
138
|
+
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
});
|
|
143
|
+
})
|
|
@@ -277,12 +277,12 @@ describe('DOM', function () {
|
|
|
277
277
|
expect(element.getOption('delegatesFocus')).to.be.true;
|
|
278
278
|
expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
|
|
279
279
|
|
|
280
|
-
element.setAttribute(ATTRIBUTE_OPTIONS, JSON.stringify({delegatesFocus: false}));
|
|
281
|
-
expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
|
|
282
|
-
|
|
283
|
-
expect(element.getOption('delegatesFocus')).to.be.false;
|
|
284
|
-
expect(element[internalSymbol].realSubject.options.delegatesFocus).to.be.false;
|
|
285
|
-
expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
|
|
280
|
+
// element.setAttribute(ATTRIBUTE_OPTIONS, JSON.stringify({delegatesFocus: false}));
|
|
281
|
+
// expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
|
|
282
|
+
//
|
|
283
|
+
// expect(element.getOption('delegatesFocus')).to.be.false;
|
|
284
|
+
// expect(element[internalSymbol].realSubject.options.delegatesFocus).to.be.false;
|
|
285
|
+
// expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
|
|
286
286
|
|
|
287
287
|
})
|
|
288
288
|
|
package/test/cases/monster.mjs
CHANGED