You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
5.6 KiB
JavaScript
213 lines
5.6 KiB
JavaScript
'use strict';
|
|
const {ELEMENT_NODE} = require('../shared/constants.js');
|
|
const {END, NEXT, UPGRADE} = require('../shared/symbols.js');
|
|
const {entries, setPrototypeOf} = require('../shared/object.js');
|
|
const {shadowRoots} = require('../shared/shadow-roots.js');
|
|
|
|
let reactive = false;
|
|
|
|
const Classes = new WeakMap;
|
|
exports.Classes = Classes;
|
|
|
|
const customElements = new WeakMap;
|
|
exports.customElements = customElements;
|
|
|
|
const attributeChangedCallback = (element, attributeName, oldValue, newValue) => {
|
|
if (
|
|
reactive &&
|
|
customElements.has(element) &&
|
|
element.attributeChangedCallback &&
|
|
element.constructor.observedAttributes.includes(attributeName)
|
|
) {
|
|
element.attributeChangedCallback(attributeName, oldValue, newValue);
|
|
}
|
|
};
|
|
exports.attributeChangedCallback = attributeChangedCallback;
|
|
|
|
const createTrigger = (method, isConnected) => element => {
|
|
if (customElements.has(element)) {
|
|
const info = customElements.get(element);
|
|
if (info.connected !== isConnected && element.isConnected === isConnected) {
|
|
info.connected = isConnected;
|
|
if (method in element)
|
|
element[method]();
|
|
}
|
|
}
|
|
};
|
|
|
|
const triggerConnected = createTrigger('connectedCallback', true);
|
|
const connectedCallback = element => {
|
|
if (reactive) {
|
|
triggerConnected(element);
|
|
if (shadowRoots.has(element))
|
|
element = shadowRoots.get(element).shadowRoot;
|
|
let {[NEXT]: next, [END]: end} = element;
|
|
while (next !== end) {
|
|
if (next.nodeType === ELEMENT_NODE)
|
|
triggerConnected(next);
|
|
next = next[NEXT];
|
|
}
|
|
}
|
|
};
|
|
exports.connectedCallback = connectedCallback;
|
|
|
|
const triggerDisconnected = createTrigger('disconnectedCallback', false);
|
|
const disconnectedCallback = element => {
|
|
if (reactive) {
|
|
triggerDisconnected(element);
|
|
if (shadowRoots.has(element))
|
|
element = shadowRoots.get(element).shadowRoot;
|
|
let {[NEXT]: next, [END]: end} = element;
|
|
while (next !== end) {
|
|
if (next.nodeType === ELEMENT_NODE)
|
|
triggerDisconnected(next);
|
|
next = next[NEXT];
|
|
}
|
|
}
|
|
};
|
|
exports.disconnectedCallback = disconnectedCallback;
|
|
|
|
/**
|
|
* @implements globalThis.CustomElementRegistry
|
|
*/
|
|
class CustomElementRegistry {
|
|
|
|
/**
|
|
* @param {Document} ownerDocument
|
|
*/
|
|
constructor(ownerDocument) {
|
|
/**
|
|
* @private
|
|
*/
|
|
this.ownerDocument = ownerDocument;
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this.registry = new Map;
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this.waiting = new Map;
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this.active = false;
|
|
}
|
|
|
|
/**
|
|
* @param {string} localName the custom element definition name
|
|
* @param {Function} Class the custom element **Class** definition
|
|
* @param {object?} options the optional object with an `extends` property
|
|
*/
|
|
define(localName, Class, options = {}) {
|
|
const {ownerDocument, registry, waiting} = this;
|
|
|
|
if (registry.has(localName))
|
|
throw new Error('unable to redefine ' + localName);
|
|
|
|
if (Classes.has(Class))
|
|
throw new Error('unable to redefine the same class: ' + Class);
|
|
|
|
this.active = (reactive = true);
|
|
|
|
const {extends: extend} = options;
|
|
|
|
Classes.set(Class, {
|
|
ownerDocument,
|
|
options: {is: extend ? localName : ''},
|
|
localName: extend || localName
|
|
});
|
|
|
|
const check = extend ?
|
|
element => {
|
|
return element.localName === extend &&
|
|
element.getAttribute('is') === localName;
|
|
} :
|
|
element => element.localName === localName;
|
|
registry.set(localName, {Class, check});
|
|
if (waiting.has(localName)) {
|
|
for (const resolve of waiting.get(localName))
|
|
resolve(Class);
|
|
waiting.delete(localName);
|
|
}
|
|
ownerDocument.querySelectorAll(
|
|
extend ? `${extend}[is="${localName}"]` : localName
|
|
).forEach(this.upgrade, this);
|
|
}
|
|
|
|
/**
|
|
* @param {Element} element
|
|
*/
|
|
upgrade(element) {
|
|
if (customElements.has(element))
|
|
return;
|
|
const {ownerDocument, registry} = this;
|
|
const ce = element.getAttribute('is') || element.localName;
|
|
if (registry.has(ce)) {
|
|
const {Class, check} = registry.get(ce);
|
|
if (check(element)) {
|
|
const {attributes, isConnected} = element;
|
|
for (const attr of attributes)
|
|
element.removeAttributeNode(attr);
|
|
|
|
const values = entries(element);
|
|
for (const [key] of values)
|
|
delete element[key];
|
|
|
|
setPrototypeOf(element, Class.prototype);
|
|
ownerDocument[UPGRADE] = {element, values};
|
|
new Class(ownerDocument, ce);
|
|
|
|
customElements.set(element, {connected: isConnected});
|
|
|
|
for (const attr of attributes)
|
|
element.setAttributeNode(attr);
|
|
|
|
if (isConnected && element.connectedCallback)
|
|
element.connectedCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} localName the custom element definition name
|
|
*/
|
|
whenDefined(localName) {
|
|
const {registry, waiting} = this;
|
|
return new Promise(resolve => {
|
|
if (registry.has(localName))
|
|
resolve(registry.get(localName).Class);
|
|
else {
|
|
if (!waiting.has(localName))
|
|
waiting.set(localName, []);
|
|
waiting.get(localName).push(resolve);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} localName the custom element definition name
|
|
* @returns {Function?} the custom element **Class**, if any
|
|
*/
|
|
get(localName) {
|
|
const info = this.registry.get(localName);
|
|
return info && info.Class;
|
|
}
|
|
|
|
/**
|
|
* @param {Function} Class **Class** of custom element
|
|
* @returns {string?} found tag name or null
|
|
*/
|
|
getName(Class) {
|
|
if (Classes.has(Class)) {
|
|
const { localName } = Classes.get(Class);
|
|
return localName;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
exports.CustomElementRegistry = CustomElementRegistry
|