dominject
Version:
Inject scripts and styles into the DOM with duplicate prevention and completion callback support
179 lines (157 loc) • 4.69 kB
JavaScript
// Prepare
const ONE_MINUTE = 60 * 1000;
/* :: declare class DOMInjectElement extends HTMLElement {
ondominject: Array<function>;
domInjectDone: bool;
domInjectTimer: any;
} */
/**
* Get the object type string
* @param {Object} opts - an object of the following properties, requires at least type and url
* @param {string} opts.type - either 'script' or 'style'
* @param {string} opts.url - the url of the resource to inject
* @param {Object} [opts.attrs] - an object of attribute names to values for the injected element
* @param {number} [opts.timeout=ONE_MINUTE] - the timeout duration in milliseconds, can be fasley for none
* @param {function} [opts.next] - the completion callback, accepts a potential Error as the first argument, and the dom element as the second
* @returns {string}
*/
export default function dominject(opts /* :Object */) {
// Extract
const {
type,
url,
attrs = {},
timeout = ONE_MINUTE
} = opts;
// Ensure that errors won't go unhandled if the next callback doesn't exist
function next(err, el) {
if (opts.next) {
opts.next(err, el);
} else if (err) {
throw err;
}
}
// Check if we already have an existing element
// If so, our job was el.domInjectDone earlier
let el /* : HTMLElement */ = document.getElementById(url);
if (el) {
if (el.domInjectDone) {
next(null, el);
} else {
el.ondominject.push(next);
}
return el;
}
// Otherwise create the element
// Finish up
function finish(err) {
// Check
if (el.domInjectDone) return;
// Reset
el.domInjectDone = true;
el.onload = el.onreadystatechange = null;
if (el.domInjectTimer != null) {
clearTimeout(el.domInjectTimer);
el.domInjectTimer = null;
}
// Remove the element if we error'd
if (err && el && parent) {
el.parentNode.removeChild(el);
el = null;
}
// Complete (with ensured err as null)
/* eslint no-cond-assign:0 */
let handler;
while (handler = el.ondominject.shift()) {
handler(err || null, el);
}
}
// Handle on Load
function onLoad() {
// Check
if (el.domInjectDone || !this.readyState /* browsers/events that do not support ready state */ || this.readyState === 'complete' /* mdn */ || this.readyState === 'loaded' /* IE */) {
finish();
}
}
// Handle on Error
function onError() {
// Check
if (!el.domInjectDone) {
// Error
const err = new Error(`The ${url} failed to be injected`);
finish(err);
}
}
// Handle on Timeout
function onTimeout() {
// Check
if (!el.domInjectDone) {
// Error
const err = new Error(`The url ${url} took too long to be injected and timed out`);
finish(err);
}
}
// Handle
switch (type) {
case 'script':
{
// Create
el = document.createElement('script');
el.ondominject = [next];
el.domInjectDone = false;
// Attributes
if (attrs.defer == null) attrs.defer = true;
if (attrs.src == null) attrs.src = url;
if (attrs.id == null) attrs.id = url;
for (const key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key]);
}
}
// Events
el.onload = el.onreadystatechange = onLoad;
el.onerror = onError;
// Attach
document.body.appendChild(el);
break;
}
case 'style':
{
// Create
el = document.createElement('link');
el.ondominject = [next];
el.domInjectDone = false;
// Attributes
if (attrs.rel == null) attrs.rel = 'stylesheet';
if (attrs.href == null) attrs.href = url;
if (attrs.id == null) attrs.id = url;
for (const key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key]);
}
}
// Events
el.onload = el.onreadystatechange = onLoad;
el.onerror = onError;
// Attach
document.head.appendChild(el);
// Fallback for older browsers
// http://www.backalleycoder.com/2011/03/20/link-tag-css-stylesheet-load-event/
const img = document.createElement('img');
img.onerror = onLoad;
img.src = url;
break;
}
// Something else
default:
{
// Error
const err = new Error(`The url ${url} has an unknown inject type of ${type}`);
return next(err);
}
}
// Timeout if applicable
if (timeout) el.domInjectTimer = setTimeout(onTimeout, timeout);
// Return the element
return el;
}