ce-v0
Version:
Custom Elements V0 API
766 lines (702 loc) • 22.7 kB
JavaScript
(function(window){'use strict';
/**
* Copyright (c) 2017, Andrea Giammarchi, @WebReflection
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
var
document = window.document,
Object = window.Object,
// V0 polyfill entry
REGISTER_ELEMENT = 'registerElement',
// IE < 11 only + old WebKit for attributes + feature detection
EXPANDO_UID = '__' + REGISTER_ELEMENT + (window.Math.random() * 10e4 >> 0),
// shortcuts and costants
ADD_EVENT_LISTENER = 'addEventListener',
ATTACHED = 'attached',
CALLBACK = 'Callback',
DETACHED = 'detached',
ATTRIBUTE_CHANGED_CALLBACK = 'attributeChanged' + CALLBACK,
ATTACHED_CALLBACK = ATTACHED + CALLBACK,
CONNECTED_CALLBACK = 'connected' + CALLBACK,
DISCONNECTED_CALLBACK = 'disconnected' + CALLBACK,
CREATED_CALLBACK = 'created' + CALLBACK,
DETACHED_CALLBACK = DETACHED + CALLBACK,
ADDITION = 'ADDITION',
MODIFICATION = 'MODIFICATION',
REMOVAL = 'REMOVAL',
DOM_ATTR_MODIFIED = 'DOMAttrModified',
DOM_CONTENT_LOADED = 'DOMContentLoaded',
DOM_SUBTREE_MODIFIED = 'DOMSubtreeModified',
PREFIX_TAG = '<',
PREFIX_IS = '=',
// registered types and their prototypes
types = [],
protos = [],
// to query subnodes
query = '',
// html shortcut used to feature detect
documentElement = document.documentElement,
// ES5 inline helpers || basic patches
indexOf = types.indexOf || function (v) {
for(var i = this.length; i-- && this[i] !== v;){}
return i;
},
// other helpers / shortcuts
OP = Object.prototype,
hOP = OP.hasOwnProperty,
iPO = OP.isPrototypeOf,
defineProperty = Object.defineProperty,
gOPD = Object.getOwnPropertyDescriptor,
gOPN = Object.getOwnPropertyNames,
gPO = Object.getPrototypeOf,
sPO = Object.setPrototypeOf,
// jshint proto: true
hasProto = !!Object.__proto__,
// used to create unique instances
create = Object.create || function Bridge(proto) {
// silly broken polyfill probably ever used but short enough to work
return proto ? ((Bridge.prototype = proto), new Bridge()) : this;
},
// will set the prototype if possible
// or copy over all properties
setPrototype = sPO || (
hasProto ?
function (o, p) {
o.__proto__ = p;
return o;
} : (
(gOPN && gOPD) ?
(function(){
function setProperties(o, p) {
for (var
key,
names = gOPN(p),
i = 0, length = names.length;
i < length; i++
) {
key = names[i];
if (!hOP.call(o, key)) {
defineProperty(o, key, gOPD(p, key));
}
}
}
return function (o, p) {
do {
setProperties(o, p);
} while ((p = gPO(p)) && !iPO.call(p, o));
return o;
};
}()) :
function (o, p) {
for (var key in p) {
o[key] = p[key];
}
return o;
}
)),
// DOM shortcuts and helpers, if any
MutationObserver = window.MutationObserver ||
window.WebKitMutationObserver,
HTMLElementPrototype = (
window.HTMLElement ||
window.Element ||
window.Node
).prototype,
IE8 = !iPO.call(HTMLElementPrototype, documentElement),
isValidNode = IE8 ?
function (node) {
return node.nodeType === 1;
} :
function (node) {
return iPO.call(HTMLElementPrototype, node);
},
targets = IE8 && [],
attachShadow = HTMLElementPrototype.attachShadow,
dispatchEvent = HTMLElementPrototype.dispatchEvent,
getAttribute = HTMLElementPrototype.getAttribute,
hasAttribute = HTMLElementPrototype.hasAttribute,
removeAttribute = HTMLElementPrototype.removeAttribute,
setAttribute = HTMLElementPrototype.setAttribute,
// replaced later on
createElement = document.createElement,
// shared observer for all attributes
attributesObserver = MutationObserver && {
attributes: true,
characterData: true,
attributeOldValue: true
},
// useful to detect only if there's no MutationObserver
DOMAttrModified = MutationObserver || function() {
doesNotSupportDOMAttrModified = false;
documentElement.removeEventListener(
DOM_ATTR_MODIFIED,
DOMAttrModified
);
},
// will both be used to make DOMNodeInserted asynchronous
asapQueue,
asapTimer = 0,
setListener = true,
justSetup = false,
doesNotSupportDOMAttrModified = true,
dropDomContentLoaded = true,
// needed for the innerHTML helper
notFromInnerHTMLHelper = true,
// optionally defined later on
onSubtreeModified,
callDOMAttrModified,
getAttributesMirror,
observer,
observe,
// based on setting prototype capability
// will check proto or the expando attribute
// in order to setup the node once
patchIfNotAlready,
patch,
// customElements ready browsers
customElements = window.customElements,
noop, construct, nativeDefine
;
// only if needed
if (REGISTER_ELEMENT in document) return;
// otherwise if V1 is available, use it
else if (customElements) {
noop = function () {};
construct = Reflect.construct;
nativeDefine = customElements.define.bind(customElements);
document.registerElement = function registerElement(name, info) {
var
proto = info.prototype,
Constructor = gPO(proto).constructor,
created = proto[CREATED_CALLBACK],
attributeChanged = proto[ATTRIBUTE_CHANGED_CALLBACK],
attached = proto[ATTACHED_CALLBACK],
detached = proto[DETACHED_CALLBACK],
observe = attributeChanged ?
function (node) {
mo.observe(node, {
attributes: true,
attributeOldValue: true
});
return node;
} :
Object,
define = function (name, value) {
defineProperty(CustomElementV0.prototype, name, {
configurable: true,
writable: true,
value: value
});
},
mo = attributeChanged && new MutationObserver(function (mutations) {
for (var i = 0, length = mutations.length; i < length; i++) {
notifyAttributeChanged(mutations[i]);
}
})
;
function CustomElementV0() {
return construct(
Constructor,
arguments,
CustomElementV0
)[CREATED_CALLBACK]();
}
CustomElementV0.prototype = create(proto);
define(CREATED_CALLBACK, created ?
function () {
return created.call(observe(this)), this;
} :
function () {
return observe(this);
}
);
if (attributeChanged) define(ATTRIBUTE_CHANGED_CALLBACK, attributeChanged);
if (attached) define(CONNECTED_CALLBACK, attached);
if (detached) define(DISCONNECTED_CALLBACK, detached);
nativeDefine(name, CustomElementV0);
return CustomElementV0;
};
}
// the rest is from the old polyfill for every old mobile/desktop browser
else {
if (sPO || hasProto) {
patchIfNotAlready = function (node, proto) {
if (!iPO.call(proto, node)) {
setupNode(node, proto);
}
};
patch = setupNode;
} else {
patchIfNotAlready = function (node, proto) {
if (!node[EXPANDO_UID]) {
node[EXPANDO_UID] = Object(true);
setupNode(node, proto);
}
};
patch = patchIfNotAlready;
}
if (IE8) {
doesNotSupportDOMAttrModified = false;
(function (){
var
descriptor = gOPD(HTMLElementPrototype, ADD_EVENT_LISTENER),
addEventListener = descriptor.value,
patchedRemoveAttribute = function (name) {
var e = new CustomEvent(DOM_ATTR_MODIFIED, {bubbles: true});
e.attrName = name;
e.prevValue = getAttribute.call(this, name);
e.newValue = null;
e[REMOVAL] = e.attrChange = 2;
removeAttribute.call(this, name);
dispatchEvent.call(this, e);
},
patchedSetAttribute = function (name, value) {
var
had = hasAttribute.call(this, name),
old = had && getAttribute.call(this, name),
e = new CustomEvent(DOM_ATTR_MODIFIED, {bubbles: true})
;
setAttribute.call(this, name, value);
e.attrName = name;
e.prevValue = had ? old : null;
e.newValue = value;
if (had) {
e[MODIFICATION] = e.attrChange = 1;
} else {
e[ADDITION] = e.attrChange = 0;
}
dispatchEvent.call(this, e);
},
onPropertyChange = function (e) {
// jshint eqnull:true
var
node = e.currentTarget,
superSecret = node[EXPANDO_UID],
propertyName = e.propertyName,
event
;
if (superSecret.hasOwnProperty(propertyName)) {
superSecret = superSecret[propertyName];
event = new CustomEvent(DOM_ATTR_MODIFIED, {bubbles: true});
event.attrName = superSecret.name;
event.prevValue = superSecret.value || null;
event.newValue = (superSecret.value = node[propertyName] || null);
if (event.prevValue == null) {
event[ADDITION] = event.attrChange = 0;
} else {
event[MODIFICATION] = event.attrChange = 1;
}
dispatchEvent.call(node, event);
}
}
;
descriptor.value = function (type, handler, capture) {
if (
type === DOM_ATTR_MODIFIED &&
this[ATTRIBUTE_CHANGED_CALLBACK] &&
this.setAttribute !== patchedSetAttribute
) {
this[EXPANDO_UID] = {
className: {
name: 'class',
value: this.className
}
};
this.setAttribute = patchedSetAttribute;
this.removeAttribute = patchedRemoveAttribute;
addEventListener.call(this, 'propertychange', onPropertyChange);
}
addEventListener.call(this, type, handler, capture);
};
defineProperty(HTMLElementPrototype, ADD_EVENT_LISTENER, descriptor);
}());
} else if (!MutationObserver) {
documentElement[ADD_EVENT_LISTENER](DOM_ATTR_MODIFIED, DOMAttrModified);
documentElement.setAttribute(EXPANDO_UID, 1);
documentElement.removeAttribute(EXPANDO_UID);
if (doesNotSupportDOMAttrModified) {
onSubtreeModified = function (e) {
var
node = this,
oldAttributes,
newAttributes,
key
;
if (node === e.target) {
oldAttributes = node[EXPANDO_UID];
node[EXPANDO_UID] = (newAttributes = getAttributesMirror(node));
for (key in newAttributes) {
if (!(key in oldAttributes)) {
// attribute was added
return callDOMAttrModified(
0,
node,
key,
oldAttributes[key],
newAttributes[key],
ADDITION
);
} else if (newAttributes[key] !== oldAttributes[key]) {
// attribute was changed
return callDOMAttrModified(
1,
node,
key,
oldAttributes[key],
newAttributes[key],
MODIFICATION
);
}
}
// checking if it has been removed
for (key in oldAttributes) {
if (!(key in newAttributes)) {
// attribute removed
return callDOMAttrModified(
2,
node,
key,
oldAttributes[key],
newAttributes[key],
REMOVAL
);
}
}
}
};
callDOMAttrModified = function (
attrChange,
currentTarget,
attrName,
prevValue,
newValue,
action
) {
var e = {
attrChange: attrChange,
currentTarget: currentTarget,
attrName: attrName,
prevValue: prevValue,
newValue: newValue
};
e[action] = attrChange;
onDOMAttrModified(e);
};
getAttributesMirror = function (node) {
for (var
attr, name,
result = {},
attributes = node.attributes,
i = 0, length = attributes.length;
i < length; i++
) {
attr = attributes[i];
name = attr.name;
if (name !== 'setAttribute') {
result[name] = attr.value;
}
}
return result;
};
}
}
// set as enumerable, writable and configurable
document[REGISTER_ELEMENT] = function registerElement(type, options) {
upperType = type.toUpperCase();
if (setListener) {
// only first time document.registerElement is used
// we need to set this listener
// setting it by default might slow down for no reason
setListener = false;
if (MutationObserver) {
observer = (function(attached, detached){
function checkEmAll(list, callback) {
for (var i = 0, length = list.length; i < length; callback(list[i++])){}
}
return new MutationObserver(function (records) {
for (var
current,
i = 0, length = records.length; i < length; i++
) {
current = records[i];
if (current.type === 'childList') {
checkEmAll(current.addedNodes, attached);
checkEmAll(current.removedNodes, detached);
} else {
notifyAttributeChanged(current);
}
}
});
}(executeAction(ATTACHED), executeAction(DETACHED)));
observe = function (node) {
observer.observe(
node,
{
childList: true,
subtree: true
}
);
return node;
};
observe(document);
if (attachShadow) {
HTMLElementPrototype.attachShadow = function () {
return observe(attachShadow.apply(this, arguments));
};
}
} else {
asapQueue = [];
document[ADD_EVENT_LISTENER]('DOMNodeInserted', onDOMNode(ATTACHED));
document[ADD_EVENT_LISTENER]('DOMNodeRemoved', onDOMNode(DETACHED));
}
document[ADD_EVENT_LISTENER](DOM_CONTENT_LOADED, onReadyStateChange);
document[ADD_EVENT_LISTENER]('readystatechange', onReadyStateChange);
patchCloneNode(HTMLElementPrototype);
patchCloneNode(document.createDocumentFragment().constructor.prototype);
}
if (justSetup) return (justSetup = false);
if (-2 < (
indexOf.call(types, PREFIX_IS + upperType) +
indexOf.call(types, PREFIX_TAG + upperType)
)) {
throwTypeError(type);
}
var
constructor = function () {
return document.createElement(nodeName);
},
opt = options || OP,
nodeName = upperType,
upperType,
i
;
i = types.push(PREFIX_TAG + upperType) - 1;
query = query.concat(
query.length ? ',' : '',
nodeName
);
constructor.prototype = (
protos[i] = hOP.call(opt, 'prototype') ?
opt.prototype :
create(HTMLElementPrototype)
);
if (query.length) loopAndVerify(
document.querySelectorAll(query),
ATTACHED
);
return constructor;
};
document.createElement = function (localName) {
var
node = createElement.apply(document, arguments),
name = '' + localName,
i = indexOf.call(
types,
PREFIX_TAG + name.toUpperCase()
),
setup = -1 < i
;
notFromInnerHTMLHelper = !document.createElement.innerHTMLHelper;
if (setup) patch(node, protos[i]);
return node;
};
}
function notifyAttributeChanged(mutation) {
var
node = mutation.target,
name = mutation.attributeName,
oldValue = mutation.oldValue,
newValue
;
if (notFromInnerHTMLHelper &&
node[ATTRIBUTE_CHANGED_CALLBACK] &&
name !== 'style') {
newValue = getAttribute.call(node, name);
if (newValue !== oldValue) {
node[ATTRIBUTE_CHANGED_CALLBACK](
name,
oldValue,
newValue
);
}
}
}
function ASAP() {
var queue = asapQueue.splice(0, asapQueue.length);
asapTimer = 0;
while (queue.length) {
queue.shift().call(
null, queue.shift()
);
}
}
function loopAndVerify(list, action) {
for (var i = 0, length = list.length; i < length; i++) {
verifyAndSetupAndAction(list[i], action);
}
}
function loopAndSetup(list) {
for (var i = 0, length = list.length, node; i < length; i++) {
node = list[i];
patch(node, protos[getTypeIndex(node)]);
}
}
function executeAction(action) {
return function (node) {
if (isValidNode(node)) {
verifyAndSetupAndAction(node, action);
if (query.length) loopAndVerify(
node.querySelectorAll(query),
action
);
}
};
}
function getTypeIndex(target) {
return indexOf.call(
types,
PREFIX_TAG + target.nodeName.toUpperCase()
);
}
function onDOMAttrModified(e) {
var
node = e.currentTarget,
attrChange = e.attrChange,
attrName = e.attrName,
target = e.target,
addition = e[ADDITION] || 2,
removal = e[REMOVAL] || 3
;
if (notFromInnerHTMLHelper &&
(!target || target === node) &&
node[ATTRIBUTE_CHANGED_CALLBACK] &&
attrName !== 'style' && (
e.prevValue !== e.newValue ||
// IE9, IE10, and Opera 12 gotcha
e.newValue === '' && (
attrChange === addition ||
attrChange === removal
)
)) {
node[ATTRIBUTE_CHANGED_CALLBACK](
attrName,
attrChange === addition ? null : e.prevValue,
attrChange === removal ? null : e.newValue
);
}
}
function onDOMNode(action) {
var executor = executeAction(action);
return function (e) {
asapQueue.push(executor, e.target);
if (asapTimer) clearTimeout(asapTimer);
asapTimer = setTimeout(ASAP, 1);
};
}
function onReadyStateChange(e) {
if (dropDomContentLoaded) {
dropDomContentLoaded = false;
e.currentTarget.removeEventListener(DOM_CONTENT_LOADED, onReadyStateChange);
}
if (query.length) loopAndVerify(
(e.target || document).querySelectorAll(query),
e.detail === DETACHED ? DETACHED : ATTACHED
);
if (IE8) purge();
}
function patchCloneNode(proto) {
var cloneNode = proto.cloneNode;
proto.cloneNode = function (deep) {
var
node = cloneNode.call(this, !!deep),
i = getTypeIndex(node)
;
if (-1 < i) patch(node, protos[i]);
if (deep && query.length) loopAndSetup(node.querySelectorAll(query));
return node;
};
}
function patchedSetAttribute(name, value) {
// jshint validthis:true
var self = this;
setAttribute.call(self, name, value);
onSubtreeModified.call(self, {target: self});
}
function setupNode(node, proto) {
setPrototype(node, proto);
if (observer) {
observer.observe(node, attributesObserver);
} else {
if (doesNotSupportDOMAttrModified) {
node.setAttribute = patchedSetAttribute;
node[EXPANDO_UID] = getAttributesMirror(node);
node[ADD_EVENT_LISTENER](DOM_SUBTREE_MODIFIED, onSubtreeModified);
}
node[ADD_EVENT_LISTENER](DOM_ATTR_MODIFIED, onDOMAttrModified);
}
if (node[CREATED_CALLBACK] && notFromInnerHTMLHelper) {
node.created = true;
node[CREATED_CALLBACK]();
node.created = false;
}
}
function purge() {
for (var
node,
i = 0,
length = targets.length;
i < length; i++
) {
node = targets[i];
if (!documentElement.contains(node)) {
length--;
targets.splice(i--, 1);
verifyAndSetupAndAction(node, DETACHED);
}
}
}
function throwTypeError(type) {
throw new Error('A ' + type + ' type is already registered');
}
function verifyAndSetupAndAction(node, action) {
var
fn,
i = getTypeIndex(node),
counterAction
;
if (-1 < i) {
patchIfNotAlready(node, protos[i]);
i = 0;
if (action === ATTACHED && !node[ATTACHED]) {
node[DETACHED] = false;
node[ATTACHED] = true;
counterAction = 'connected';
i = 1;
if (IE8 && indexOf.call(targets, node) < 0) {
targets.push(node);
}
} else if (action === DETACHED && !node[DETACHED]) {
node[ATTACHED] = false;
node[DETACHED] = true;
counterAction = 'disconnected';
i = 1;
}
if (i && (fn = (
node[action + CALLBACK] ||
node[counterAction + CALLBACK]
))) fn.call(node);
}
}
}(window));