webfontloader
Version:
Web Font Loader gives you added control when using linked fonts via @font-face.
389 lines (338 loc) • 11 kB
JavaScript
goog.provide('webfont.DomHelper');
/**
* Handles common DOM manipulation tasks. The aim of this library is to cover
* the needs of typical font loading. Not more, not less.
* @param {Window} mainWindow The main window webfontloader.js is loaded in.
* @param {Window=} opt_loadWindow The window we'll load the font into. By
* default, the main window is used.
* @constructor
*/
webfont.DomHelper = function(mainWindow, opt_loadWindow) {
this.mainWindow_ = mainWindow;
this.loadWindow_ = opt_loadWindow || mainWindow;
/** @type {string} */
this.protocol_;
/** @type {Document} */
this.document_ = this.loadWindow_.document;
};
goog.scope(function () {
var DomHelper = webfont.DomHelper;
/**
* Creates an element.
* @param {string} elem The element type.
* @param {Object=} opt_attr A hash of attribute key/value pairs.
* @param {string=} opt_innerHtml Contents of the element.
* @return {Element} the new element.
*/
DomHelper.prototype.createElement = function(elem, opt_attr,
opt_innerHtml) {
var domElement = this.document_.createElement(elem);
if (opt_attr) {
for (var attr in opt_attr) {
// protect against native prototype augmentations
if (opt_attr.hasOwnProperty(attr)) {
if (attr == "style") {
this.setStyle(domElement, opt_attr[attr]);
} else {
domElement.setAttribute(attr, opt_attr[attr]);
}
}
}
}
if (opt_innerHtml) {
domElement.appendChild(this.document_.createTextNode(opt_innerHtml));
}
return domElement;
};
/**
* Inserts an element into the document. This is intended for unambiguous
* elements such as html, body, head.
* @param {string} tagName The element name.
* @param {Element} e The element to append.
* @return {boolean} True if the element was inserted.
*/
DomHelper.prototype.insertInto = function(tagName, e) {
var t = this.document_.getElementsByTagName(tagName)[0];
if (!t) { // opera allows documents without a head
t = document.documentElement;
}
if (t && t.lastChild) {
// This is safer than appendChild in IE. appendChild causes random
// JS errors in IE. Sometimes errors in other JS exectution, sometimes
// complete 'This page cannot be displayed' errors. For our purposes,
// it's equivalent because we don't need to insert at any specific
// location.
t.insertBefore(e, t.lastChild);
return true;
}
return false;
};
/**
* Calls a function when the body tag exists.
* @param {function()} callback The function to call.
*/
DomHelper.prototype.whenBodyExists = function(callback) {
var that = this;
var check = function() {
if (that.document_.body) {
callback();
} else {
setTimeout(check, 0);
}
}
check();
};
/**
* Removes an element from the DOM.
* @param {Element} node The element to remove.
* @return {boolean} True if the element was removed.
*/
DomHelper.prototype.removeElement = function(node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
return true;
}
return false;
};
/**
* @deprecated Use updateClassName().
*
* Appends a name to an element's class attribute.
* @param {Element} e The element.
* @param {string} name The class name to add.
*/
DomHelper.prototype.appendClassName = function(e, name) {
this.updateClassName(e, [name]);
};
/**
* @deprecated Use updateClassName().
*
* Removes a name to an element's class attribute.
* @param {Element} e The element.
* @param {string} name The class name to remove.
*/
DomHelper.prototype.removeClassName = function(e, name) {
this.updateClassName(e, null, [name]);
};
/**
* Updates an element's class attribute in a single change. This
* allows multiple updates in a single class name change so there
* is no chance for a browser to relayout in between changes.
*
* @param {Element} e The element.
* @param {Array.<string>=} opt_add List of class names to add.
* @param {Array.<string>=} opt_remove List of class names to remove.
*/
DomHelper.prototype.updateClassName = function (e, opt_add, opt_remove) {
var add = opt_add || [],
remove = opt_remove || [];
var classes = e.className.split(/\s+/);
for (var i = 0; i < add.length; i += 1) {
var found = false;
for (var j = 0; j < classes.length; j += 1) {
if (add[i] === classes[j]) {
found = true;
break;
}
}
if (!found) {
classes.push(add[i]);
}
}
var remainingClasses = [];
for (var i = 0; i < classes.length; i += 1) {
var found = false;
for (var j = 0; j < remove.length; j += 1) {
if (classes[i] === remove[j]) {
found = true;
break;
}
}
if (!found) {
remainingClasses.push(classes[i]);
}
}
e.className = remainingClasses.join(' ')
.replace(/\s+/g, ' ')
.replace(/^\s+|\s+$/, '');
};
/**
* Returns true if an element has a given class name and false otherwise.
* @param {Element} e The element.
* @param {string} name The class name to check for.
* @return {boolean} Whether or not the element has this class name.
*/
DomHelper.prototype.hasClassName = function(e, name) {
var classes = e.className.split(/\s+/);
for (var i = 0, len = classes.length; i < len; i++) {
if (classes[i] == name) {
return true;
}
}
return false;
};
/**
* Sets the style attribute on an element.
* @param {Element} e The element.
* @param {string} styleString The style string.
*/
DomHelper.prototype.setStyle = function(e, styleString) {
e.style.cssText = styleString;
};
/**
* @return {Window} The main window webfontloader.js is loaded in (for config).
*/
DomHelper.prototype.getMainWindow = function() {
return this.mainWindow_;
};
/**
* @return {Window} The window that we're loading the font(s) into.
*/
DomHelper.prototype.getLoadWindow = function() {
return this.loadWindow_;
};
/**
* @return {string} The protocol (http: or https:) to request resources in.
*/
DomHelper.prototype.getProtocol = function() {
if (typeof this.protocol_ === 'string') {
return this.protocol_;
} else {
var protocol = this.loadWindow_.location.protocol;
// For empty iframes, fallback to main window's protocol.
if (protocol == 'about:') {
protocol = this.mainWindow_.location.protocol;
}
return protocol == 'https:' ? 'https:' : 'http:';
}
};
/**
* Explicitly set the protocol instead of automatic detection.
*
* @param {string} protocol
*/
DomHelper.prototype.setProtocol = function (protocol) {
if (/^http(s)?:$/.test(protocol)) {
this.protocol_ = protocol;
}
};
/**
* Returns the secure status of the current document.
* @return {boolean} true if the current document is served securely.
*/
DomHelper.prototype.isHttps = function() {
return this.getProtocol() === 'https:';
};
/**
* Returns the hostname of the current document.
* @return {string} hostname.
*/
DomHelper.prototype.getHostName = function() {
return this.getLoadWindow().location.hostname || this.getMainWindow().location.hostname;
};
/**
* Creates a style element.
* @param {string} css Contents of the style element.
* @return {Element} a DOM element.
*/
DomHelper.prototype.createStyle = function(css) {
var e = this.createElement('style');
e.setAttribute('type', 'text/css');
if (e.styleSheet) { // IE
e.styleSheet.cssText = css;
} else {
e.appendChild(document.createTextNode(css));
}
return e;
};
/**
* Loads an external stylesheet.
*
* @param {string} href the URL of the stylesheet
* @param {function(Error)=} opt_callback Called when the stylesheet has loaded or failed to
* load. Note that the callback is *NOT* guaranteed to be called in all browsers. The first
* argument to the callback is an error object that is falsy when there are no errors and
* truthy when there are.
* @return {Element} The link element
*/
DomHelper.prototype.loadStylesheet = function (href, opt_callback) {
var link = this.createElement('link', {
'rel': 'stylesheet',
'href': href,
'media': 'only x'
});
var sheets = this.document_.styleSheets;
var done = false;
link.onload = function () {
if (!done) {
done = true;
if (opt_callback) {
opt_callback(null);
}
}
};
link.onerror = function () {
if (!done) {
done = true;
if (opt_callback) {
opt_callback(new Error('Stylesheet failed to load'));
}
}
};
function onAvailable(callback) {
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].href && sheets[i].href.indexOf(href) !== -1) {
return callback();
}
}
setTimeout(function () {
onAvailable(callback);
}, 0);
}
this.insertInto('head', link);
onAvailable(function () {
link.media = "all";
});
return link;
};
/**
* Loads an external script file.
* @param {string} src URL of the script.
* @param {function(Error)=} opt_callback callback when the script has loaded. The first argument to
* the callback is an error object that is falsy when there are no errors and truthy when there are.
* @param {number=} opt_timeout The number of milliseconds after which the callback will be called
* with a timeout error. Defaults to 5 seconds.
* @return {Element} The script element
*/
DomHelper.prototype.loadScript = function(src, opt_callback, opt_timeout) {
var head = this.document_.getElementsByTagName('head')[0];
if (head) {
var script = this.createElement('script', {
'src': src
});
var done = false;
script.onload = script.onreadystatechange = function() {
if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {
done = true;
if (opt_callback) {
opt_callback(null);
}
script.onload = script.onreadystatechange = null;
// Avoid a bizarre issue with unclosed <base> tag in IE6 - http://blog.dotsmart.net/2008/04/
if (script.parentNode.tagName == 'HEAD') head.removeChild(script);
}
};
head.appendChild(script);
window.setTimeout(function () {
if (!done) {
done = true;
if (opt_callback) {
opt_callback(new Error('Script load timeout'));
}
}
}, opt_timeout || 5000);
return script;
}
return null;
};
});