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.

206 lines
5.4 KiB
JavaScript

import {ELEMENT_NODE} from '../shared/constants.js';
import {END, NEXT, UPGRADE} from '../shared/symbols.js';
import {entries, setPrototypeOf} from '../shared/object.js';
import {shadowRoots} from '../shared/shadow-roots.js';
let reactive = false;
export const Classes = new WeakMap;
export const customElements = new WeakMap;
export const attributeChangedCallback = (element, attributeName, oldValue, newValue) => {
if (
reactive &&
customElements.has(element) &&
element.attributeChangedCallback &&
element.constructor.observedAttributes.includes(attributeName)
) {
element.attributeChangedCallback(attributeName, oldValue, newValue);
}
};
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);
export 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];
}
}
};
const triggerDisconnected = createTrigger('disconnectedCallback', false);
export 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];
}
}
};
/**
* @implements globalThis.CustomElementRegistry
*/
export 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;
}
}