joicomponents
Version:
Design patterns to build native web components
202 lines (184 loc) • 7.05 kB
JavaScript
/**
* @license
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
(function () {
'use strict';
/**
* Basic flow of the loader process:
*
* Step 1. Find out which if any web comp polyfills are needed
* Step 2a. If a polyfill is needed, load it either
* sync:
* async:
* Step 2b. If no polyfill is needed, and the document has completed loading,
* fire the ready event.
* Step 2c. If no polyfill is needed, but the document has not completed loading,
* which might occur due to some strange stuff,
* then
*
* There are 4 flows the loader can take when booting up
*
* - Synchronous script, no polyfills needed
* - wait for `DOMContentLoaded`
* - run callbacks passed to `waitFor`
* - fire WCR event
*
* - Synchronous script, polyfills needed
* - document.write the polyfill bundle
* - wait on the `load` event of the bundle to batch Custom Element upgrades
* - wait for `DOMContentLoaded`
* - run callbacks passed to `waitFor`
* - fire WCR event
*
* - Asynchronous script, no polyfills needed
* - fire WCR event, as there could not be any callbacks passed to `waitFor`
*
* - Asynchronous script, polyfills needed
* - Append the polyfill bundle script
* - wait for `load` event of the bundle
* - batch Custom Element Upgrades
* - run callbacks pass to `waitFor`
* - fire WCR event
*/
var polyfillsLoaded = false;
var whenLoadedFns = [];
var allowUpgrades = false;
var flushFn;
function pathForScript(scriptName, polyfillName) {
var script = document.querySelector('script[src*="' + scriptName + '"]');
return script.src.replace(scriptName, polyfillName);
}
function loadScriptSynch(url) {
var newScript = document.createElement('script');
newScript.src = url;
// make sure custom elements are batched whenever parser gets to the injected script
newScript.setAttribute('onload', 'window.WebComponents._batchCustomElements()');
document.write(newScript.outerHTML);
document.addEventListener('DOMContentLoaded', ready);
}
function loadScriptAsync(url) {
var newScript = document.createElement('script');
newScript.src = url;
newScript.addEventListener('load', function () {
asyncReady();
});
newScript.addEventListener('error', function () {
throw new Error('Could not load polyfill bundle' + url);
});
document.head.appendChild(newScript);
}
function loadScript(url) {
document.readyState === 'loading' ? loadScriptSynch(url) : loadScriptAsync(url);
}
function fireEvent() {
window.WebComponents.ready = true;
document.dispatchEvent(new CustomEvent('WebComponentsReady', {bubbles: true}));
}
function batchCustomElements() {
if (window.customElements && customElements.polyfillWrapFlushCallback) {
customElements.polyfillWrapFlushCallback(function (flushCallback) {
flushFn = flushCallback;
if (allowUpgrades) {
flushFn();
}
});
}
}
function asyncReady() {
batchCustomElements();
ready();
}
function ready() {
// bootstrap <template> elements before custom elements
if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) {
HTMLTemplateElement.bootstrap(window.document);
}
polyfillsLoaded = true;
runWhenLoadedFns().then(fireEvent);
}
function runWhenLoadedFns() {
allowUpgrades = false;
var done = function () {
allowUpgrades = true;
whenLoadedFns.length = 0;
flushFn && flushFn();
};
return Promise.all(whenLoadedFns.map(function (fn) {
return fn instanceof Function ? fn() : fn;
})).then(function () {
done();
}).catch(function (err) {
console.error(err);
});
}
//todo what is the difference between ready and polyfillsLoaded
//todo should we have a polyfillsLoaded? that starts false and ends true when it is loaded?
//todo when polyfillsLoaded is set to true, the runWhenLoadedFns(); should run,
//todo and then the
window.WebComponents = window.WebComponents || {
ready: false,
_batchCustomElements: batchCustomElements,
waitFor: function (waitFn) {
if (!waitFn) {
return;
}
whenLoadedFns.push(waitFn);
if (polyfillsLoaded) {
runWhenLoadedFns();
}
}
};
// Feature detect which polyfill needs to be imported.
var sd =
'attachShadow' in Element.prototype &&
'getRootNode' in Element.prototype &&
!(window.ShadyDOM && window.ShadyDOM.force);
var ce = window.customElements && !window.customElements.forcePolyfill;
var te = (function () {
// no real <template> because no `content` property (IE and older browsers)
var t = document.createElement('template');
if (!('content' in t)) {
return true;
}
// broken doc fragment (older Edge)
if (!(t.content.cloneNode() instanceof DocumentFragment)) {
return true;
}
// broken <template> cloning (Edge up to at least version 17)
var t2 = document.createElement('template');
t2.content.appendChild(document.createElement('div'));
t.content.appendChild(t2);
var clone = t.cloneNode(true);
return (clone.content.childNodes.length === 0 ||
clone.content.firstChild.content.childNodes.length === 0);
})();
var es5 = !window.Promise || !Array.from || !window.URL || !window.Symbol || te;
if (!es5 && sd && ce && te) {
//todo instead of using an internal property in addition to
polyfillsLoaded = true; //this is set in ready()?
if (document.readyState === 'complete') {
fireEvent();
} else {
// this script may come between DCL and load, so listen for both, and cancel load listener if DCL fires
window.addEventListener('load', ready);
window.addEventListener('DOMContentLoaded', function () {
window.removeEventListener('load', ready);
ready();
})
}
} else if (es5) { // NOTE: any browser that does not have template or ES6 features must load the full suite of polyfills.
loadScript(pathForScript('webcomponents-loader.js', 'bundles/webcomponents-sd-ce-pf.js'));
} else if (!sd && !ce) {
loadScript(pathForScript('webcomponents-loader.js', 'bundles/webcomponents-sd-ce.js'));
} else if (!sd) {
loadScript(pathForScript('webcomponents-loader.js', 'bundles/webcomponents-sd.js'));
} else if (!ce) {
loadScript(pathForScript('webcomponents-loader.js', 'bundles/webcomponents-ce.js'));
}
})();