@webreflection/custom-elements-builtin
Version:
A better custom-elements-builtin polyfill, Safari only
174 lines (155 loc) • 4.86 kB
JavaScript
import attributesObserver from '@webreflection/custom-elements-attributes';
import {expando} from '@webreflection/custom-elements-upgrade';
import qsaObserver from 'qsa-observer';
const {
customElements, document,
Element, MutationObserver, Object, Promise,
Map, Set, WeakMap, Reflect
} = self;
const {createElement} = document;
const {define, get, upgrade} = customElements;
const {construct} = Reflect || {construct(HTMLElement) {
return HTMLElement.call(this);
}};
const {defineProperty, getOwnPropertyNames, setPrototypeOf} = Object;
const shadowRoots = new WeakMap;
const shadows = new Set;
const classes = new Map;
const defined = new Map;
const prototypes = new Map;
const registry = new Map;
const shadowed = [];
const query = [];
const getCE = is => registry.get(is) || get.call(customElements, is);
const handle = (element, connected, selector) => {
const proto = prototypes.get(selector);
if (connected && !proto.isPrototypeOf(element)) {
const redefine = expando(element);
override = setPrototypeOf(element, proto);
try { new proto.constructor; }
finally {
override = null;
redefine();
}
}
const method = `${connected ? '' : 'dis'}connectedCallback`;
if (method in proto)
element[method]();
};
const {parse} = qsaObserver({query, handle});
const {parse: parseShadowed} = qsaObserver({
query: shadowed,
handle(element, connected) {
if (shadowRoots.has(element)) {
if (connected)
shadows.add(element);
else
shadows.delete(element);
if (query.length)
parseShadow.call(query, element);
}
}
});
// qsaObserver also patches attachShadow
// be sure this runs *after* that
const {attachShadow} = Element.prototype;
if (attachShadow)
Element.prototype.attachShadow = function (init) {
const root = attachShadow.call(this, init);
shadowRoots.set(this, root);
return root;
};
const whenDefined = name => {
if (!defined.has(name)) {
let _, $ = new Promise($ => { _ = $; });
defined.set(name, {$, _});
}
return defined.get(name).$;
};
const augment = attributesObserver(whenDefined, MutationObserver);
let override = null;
getOwnPropertyNames(self)
.filter(k => /^HTML.*Element$/.test(k))
.forEach(k => {
const HTMLElement = self[k];
function HTMLBuiltIn() {
const {constructor} = this;
if (!classes.has(constructor))
throw new TypeError('Illegal constructor');
const {is, tag} = classes.get(constructor);
if (is) {
if (override)
return augment(override, is);
const element = createElement.call(document, tag);
element.setAttribute('is', is);
return augment(setPrototypeOf(element, constructor.prototype), is);
}
else
return construct.call(this, HTMLElement, [], constructor);
}
setPrototypeOf(HTMLBuiltIn, HTMLElement);
defineProperty(
HTMLBuiltIn.prototype = HTMLElement.prototype,
'constructor',
{value: HTMLBuiltIn}
);
defineProperty(self, k, {value: HTMLBuiltIn});
});
document.createElement = function (name, options) {
const is = options && options.is;
if (is) {
const Class = registry.get(is);
if (Class && classes.get(Class).tag === name)
return new Class;
}
const element = createElement.call(document, name);
if (is)
element.setAttribute('is', is);
return element;
};
customElements.get = getCE;
customElements.whenDefined = whenDefined;
customElements.upgrade = function (element) {
const is = element.getAttribute('is');
if (is) {
const constructor = registry.get(is);
if (constructor) {
augment(setPrototypeOf(element, constructor.prototype), is);
// apparently unnecessary because this is handled by qsa observer
// if (element.isConnected && element.connectedCallback)
// element.connectedCallback();
return;
}
}
upgrade.call(customElements, element);
};
customElements.define = function (is, Class, options) {
if (getCE(is))
throw new Error(`'${is}' has already been defined as a custom element`);
let selector;
const tag = options && options.extends;
classes.set(Class, tag ? {is, tag} : {is: '', tag: is});
if (tag) {
selector = `${tag}[is="${is}"]`;
prototypes.set(selector, Class.prototype);
registry.set(is, Class);
query.push(selector);
}
else {
define.apply(customElements, arguments);
shadowed.push(selector = is);
}
whenDefined(is).then(() => {
if (tag) {
parse(document.querySelectorAll(selector));
shadows.forEach(parseShadow, [selector]);
}
else
parseShadowed(document.querySelectorAll(selector));
});
defined.get(is)._(Class);
};
function parseShadow(element) {
const root = shadowRoots.get(element);
parse(root.querySelectorAll(this), element.isConnected);
}