@schukai/monster 3.44.1 → 3.47.0
Sign up to get free protection for your applications and to get access to all the features.
- 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