@webcomponents/custom-elements
Version:
HTML Custom Elements Polyfill
244 lines (215 loc) • 8.89 kB
JavaScript
import Native from './Native.js';
import CustomElementInternals from '../CustomElementInternals.js';
import CEState from '../CustomElementState.js';
import * as Utilities from '../Utilities.js';
import PatchParentNode from './Interface/ParentNode.js';
import PatchChildNode from './Interface/ChildNode.js';
/**
* @param {!CustomElementInternals} internals
*/
export default function(internals) {
if (Native.Element_attachShadow) {
Utilities.setPropertyUnchecked(Element.prototype, 'attachShadow',
/**
* @this {Element}
* @param {!{mode: string}} init
* @return {ShadowRoot}
*/
function(init) {
const shadowRoot = Native.Element_attachShadow.call(this, init);
this.__CE_shadowRoot = shadowRoot;
return shadowRoot;
});
}
function patch_innerHTML(destination, baseDescriptor) {
Object.defineProperty(destination, 'innerHTML', {
enumerable: baseDescriptor.enumerable,
configurable: true,
get: baseDescriptor.get,
set: /** @this {Element} */ function(htmlString) {
const isConnected = Utilities.isConnected(this);
// NOTE: In IE11, when using the native `innerHTML` setter, all nodes
// that were previously descendants of the context element have all of
// their children removed as part of the set - the entire subtree is
// 'disassembled'. This work around walks the subtree *before* using the
// native setter.
/** @type {!Array<!Element>|undefined} */
let removedElements = undefined;
if (isConnected) {
removedElements = [];
Utilities.walkDeepDescendantElements(this, element => {
if (element !== this) {
removedElements.push(element);
}
});
}
baseDescriptor.set.call(this, htmlString);
if (removedElements) {
for (let i = 0; i < removedElements.length; i++) {
const element = removedElements[i];
if (element.__CE_state === CEState.custom) {
internals.disconnectedCallback(element);
}
}
}
// Only create custom elements if this element's owner document is
// associated with the registry.
if (!this.ownerDocument.__CE_hasRegistry) {
internals.patchTree(this);
} else {
internals.patchAndUpgradeTree(this);
}
return htmlString;
},
});
}
if (Native.Element_innerHTML && Native.Element_innerHTML.get) {
patch_innerHTML(Element.prototype, Native.Element_innerHTML);
} else if (Native.HTMLElement_innerHTML && Native.HTMLElement_innerHTML.get) {
patch_innerHTML(HTMLElement.prototype, Native.HTMLElement_innerHTML);
} else {
internals.addPatch(function(element) {
patch_innerHTML(element, {
enumerable: true,
configurable: true,
// Implements getting `innerHTML` by performing an unpatched `cloneNode`
// of the element and returning the resulting element's `innerHTML`.
// TODO: Is this too expensive?
get: /** @this {Element} */ function() {
return Native.Node_cloneNode.call(this, true).innerHTML;
},
// Implements setting `innerHTML` by creating an unpatched element,
// setting `innerHTML` of that element and replacing the target
// element's children with those of the unpatched element.
set: /** @this {Element} */ function(assignedValue) {
// NOTE: re-route to `content` for `template` elements.
// We need to do this because `template.appendChild` does not
// route into `template.content`.
const isTemplate = (this.localName === 'template');
/** @type {!Node} */
const content = isTemplate ? (/** @type {!HTMLTemplateElement} */
(this)).content : this;
/** @type {!Node} */
const rawElement = Native.Document_createElement.call(document,
this.localName);
rawElement.innerHTML = assignedValue;
while (content.childNodes.length > 0) {
Native.Node_removeChild.call(content, content.childNodes[0]);
}
const container = isTemplate ? rawElement.content : rawElement;
while (container.childNodes.length > 0) {
Native.Node_appendChild.call(content, container.childNodes[0]);
}
},
});
});
}
Utilities.setPropertyUnchecked(Element.prototype, 'setAttribute',
/**
* @this {Element}
* @param {string} name
* @param {string} newValue
*/
function(name, newValue) {
// Fast path for non-custom elements.
if (this.__CE_state !== CEState.custom) {
return Native.Element_setAttribute.call(this, name, newValue);
}
const oldValue = Native.Element_getAttribute.call(this, name);
Native.Element_setAttribute.call(this, name, newValue);
newValue = Native.Element_getAttribute.call(this, name);
internals.attributeChangedCallback(this, name, oldValue, newValue, null);
});
Utilities.setPropertyUnchecked(Element.prototype, 'setAttributeNS',
/**
* @this {Element}
* @param {?string} namespace
* @param {string} name
* @param {string} newValue
*/
function(namespace, name, newValue) {
// Fast path for non-custom elements.
if (this.__CE_state !== CEState.custom) {
return Native.Element_setAttributeNS.call(this, namespace, name, newValue);
}
const oldValue = Native.Element_getAttributeNS.call(this, namespace, name);
Native.Element_setAttributeNS.call(this, namespace, name, newValue);
newValue = Native.Element_getAttributeNS.call(this, namespace, name);
internals.attributeChangedCallback(this, name, oldValue, newValue, namespace);
});
Utilities.setPropertyUnchecked(Element.prototype, 'removeAttribute',
/**
* @this {Element}
* @param {string} name
*/
function(name) {
// Fast path for non-custom elements.
if (this.__CE_state !== CEState.custom) {
return Native.Element_removeAttribute.call(this, name);
}
const oldValue = Native.Element_getAttribute.call(this, name);
Native.Element_removeAttribute.call(this, name);
if (oldValue !== null) {
internals.attributeChangedCallback(this, name, oldValue, null, null);
}
});
Utilities.setPropertyUnchecked(Element.prototype, 'removeAttributeNS',
/**
* @this {Element}
* @param {?string} namespace
* @param {string} name
*/
function(namespace, name) {
// Fast path for non-custom elements.
if (this.__CE_state !== CEState.custom) {
return Native.Element_removeAttributeNS.call(this, namespace, name);
}
const oldValue = Native.Element_getAttributeNS.call(this, namespace, name);
Native.Element_removeAttributeNS.call(this, namespace, name);
// In older browsers, `Element#getAttributeNS` may return the empty string
// instead of null if the attribute does not exist. For details, see;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNS#Notes
const newValue = Native.Element_getAttributeNS.call(this, namespace, name);
if (oldValue !== newValue) {
internals.attributeChangedCallback(this, name, oldValue, newValue, namespace);
}
});
function patch_insertAdjacentElement(destination, baseMethod) {
Utilities.setPropertyUnchecked(destination, 'insertAdjacentElement',
/**
* @this {Element}
* @param {string} where
* @param {!Element} element
* @return {?Element}
*/
function(where, element) {
const wasConnected = Utilities.isConnected(element);
const insertedElement = /** @type {!Element} */
(baseMethod.call(this, where, element));
if (wasConnected) {
internals.disconnectTree(element);
}
if (Utilities.isConnected(insertedElement)) {
internals.connectTree(element);
}
return insertedElement;
});
}
if (Native.HTMLElement_insertAdjacentElement) {
patch_insertAdjacentElement(HTMLElement.prototype, Native.HTMLElement_insertAdjacentElement);
} else if (Native.Element_insertAdjacentElement) {
patch_insertAdjacentElement(Element.prototype, Native.Element_insertAdjacentElement);
} else {
console.warn('Custom Elements: `Element#insertAdjacentElement` was not patched.');
}
PatchParentNode(internals, Element.prototype, {
prepend: Native.Element_prepend,
append: Native.Element_append,
});
PatchChildNode(internals, Element.prototype, {
before: Native.Element_before,
after: Native.Element_after,
replaceWith: Native.Element_replaceWith,
remove: Native.Element_remove,
});
};