@qooxdoo/framework
Version:
The JS Framework for Coders
572 lines (496 loc) • 18.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2011 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
************************************************************************ */
/**
* Manages font-face definitions, making sure that each rule is only applied
* once. It supports adding fonts of the same family but with different style
* and weight. For instance, the following declaration uses 4 different source
* files and combine them in a single font family.
*
* <pre class='javascript'>
* sources: [
* {
* family: "Sansation",
* source: [
* "fonts/Sansation-Regular.ttf"
* ]
* },
* {
* family: "Sansation",
* fontWeight: "bold",
* source: [
* "fonts/Sansation-Bold.ttf",
* ]
* },
* {
* family: "Sansation",
* fontStyle: "italic",
* source: [
* "fonts/Sansation-Italic.ttf",
* ]
* },
* {
* family: "Sansation",
* fontWeight: "bold",
* fontStyle: "italic",
* source: [
* "fonts/Sansation-BoldItalic.ttf",
* ]
* }
* ]
* </pre>
*
* This class does not need to be disposed, except when you want to abort the loading
* and validation process.
*/
qx.Class.define("qx.bom.webfonts.Manager", {
extend : qx.core.Object,
type : "singleton",
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
construct : function()
{
this.base(arguments);
this.__createdStyles = [];
this.__validators = {};
this.__queue = [];
this.__preferredFormats = this.getPreferredFormats();
},
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/**
* List of known font definition formats (i.e. file extensions). Used to
* identify the type of each font file configured for a web font.
*/
FONT_FORMATS : ["eot", "woff", "ttf", "svg"],
/**
* Timeout (in ms) to wait before deciding that a web font was not loaded.
*/
VALIDATION_TIMEOUT : 5000
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__createdStyles : null,
__styleSheet : null,
__validators : null,
__preferredFormats : null,
__queue : null,
__queueInterval : null,
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Adds the necessary font-face rule for a web font to the document. Also
* creates a web font Validator ({@link qx.bom.webfonts.Validator}) that
* checks if the webFont was applied correctly.
*
* @param familyName {String} Name of the web font
* @param sourcesList {Object} List of source URLs along with their style
* (e.g. fontStyle: "italic") and weight (e.g. fontWeight: "bold").
* For maximum compatibility, this should include EOT, WOFF and TTF versions
* of the font.
* @param callback {Function?} Optional event listener callback that will be
* executed once the validator has determined whether the webFont was
* applied correctly.
* See {@link qx.bom.webfonts.Validator#changeStatus}
* @param context {Object?} Optional context for the callback function
*/
require : function(familyName, sourcesList, callback, context)
{
var sourceUrls = sourcesList.source;
var comparisonString = sourcesList.comparisonString;
var version = sourcesList.version;
var fontWeight = sourcesList.fontWeight;
var fontStyle = sourcesList.fontStyle;
var sources = [];
for (var i=0,l=sourceUrls.length; i<l; i++) {
var split = sourceUrls[i].split("#");
var src = qx.util.ResourceManager.getInstance().toUri(split[0]);
if (split.length > 1) {
src = src + "#" + split[1];
}
sources.push(src);
}
// old IEs need a break in between adding @font-face rules
if (qx.core.Environment.get("engine.name") == "mshtml" && (
parseInt(qx.core.Environment.get("engine.version")) < 9 ||
qx.core.Environment.get("browser.documentmode") < 9)) {
if (!this.__queueInterval) {
this.__queueInterval = new qx.event.Timer(100);
this.__queueInterval.addListener("interval", this.__flushQueue, this);
}
if (!this.__queueInterval.isEnabled()) {
this.__queueInterval.start();
}
this.__queue.push([familyName, sources, fontWeight, fontStyle, comparisonString, version, callback, context]);
} else {
this.__require(familyName, sources, fontWeight, fontStyle, comparisonString, version, callback, context);
}
},
/**
* Removes a font's font-face definition from the style sheet. This means
* the font will no longer be available and any elements using it will
* fall back to the their regular font-families.
*
* @param familyName {String} font-family name
* @param fontWeight {String} the font-weight.
* @param fontStyle {String} the font-style.
*/
remove : function(familyName, fontWeight, fontStyle) {
var fontLookupKey = this.__createFontLookupKey(familyName, fontWeight, fontStyle);
var index = null;
for (var i=0,l=this.__createdStyles.length; i<l; i++) {
if (this.__createdStyles[i] == fontLookupKey) {
index = i;
this.__removeRule(familyName, fontWeight, fontStyle);
break;
}
}
if (index !== null) {
qx.lang.Array.removeAt(this.__createdStyles, index);
}
if (familyName in this.__validators) {
this.__validators[familyName].dispose();
delete this.__validators[familyName];
}
},
/**
* Returns the preferred font format(s) for the currently used browser. Some
* browsers support multiple formats, e.g. WOFF and TTF or WOFF and EOT. In
* those cases, WOFF is considered the preferred format.
*
* @return {String[]} List of supported font formats ordered by preference
* or empty Array if none could be determined
*/
getPreferredFormats : function()
{
var preferredFormats = [];
var browser = qx.core.Environment.get("browser.name");
var browserVersion = qx.core.Environment.get("browser.version");
var os = qx.core.Environment.get("os.name");
var osVersion = qx.core.Environment.get("os.version");
if ((browser == "ie" && qx.core.Environment.get("browser.documentmode") >= 9) ||
(browser == "firefox" && browserVersion >= 3.6) ||
(browser == "chrome" && browserVersion >= 6)) {
preferredFormats.push("woff");
}
if ((browser == "opera" && browserVersion >= 10) ||
(browser == "safari" && browserVersion >= 3.1) ||
(browser == "firefox" && browserVersion >= 3.5) ||
(browser == "chrome" && browserVersion >= 4) ||
(browser == "mobile safari" && os == "ios" && osVersion >= 4.2)) {
preferredFormats.push("ttf");
}
if (browser == "ie" && browserVersion >= 4) {
preferredFormats.push("eot");
}
if (browser == "mobileSafari" && os == "ios" && osVersion >= 4.1) {
preferredFormats.push("svg");
}
return preferredFormats;
},
/**
* Removes the styleSheet element used for all web font definitions from the
* document. This means all web fonts declared by the manager will no longer
* be available and elements using them will fall back to their regular
* font-families
*/
removeStyleSheet : function()
{
this.__createdStyles = [];
if (this.__styleSheet) {
qx.bom.Stylesheet.removeSheet(this.__styleSheet);
}
this.__styleSheet = null;
},
/*
---------------------------------------------------------------------------
PRIVATE API
---------------------------------------------------------------------------
*/
/**
* Creates a lookup key to index the created fonts.
* @param familyName {String} font-family name
* @param fontWeight {String} the font-weight.
* @param fontStyle {String} the font-style.
* @return {string} the font lookup key
*/
__createFontLookupKey: function (familyName, fontWeight, fontStyle) {
var lookupKey = familyName + "_" + (fontWeight ? fontWeight : "normal") + "_" + (fontStyle ? fontStyle : "normal");
return lookupKey;
},
/**
* Does the actual work of adding stylesheet rules and triggering font
* validation
*
* @param familyName {String} Name of the web font
* @param sources {String[]} List of source URLs. For maximum compatibility,
* this should include EOT, WOFF and TTF versions of the font.
* @param fontWeight {String} the web font should be registered using a
* fontWeight font weight.
* @param fontStyle {String} the web font should be registered using an
* fontStyle font style.
* @param comparisonString {String} String to check whether the font has loaded or not
* @param version {String?} Optional version that is appended to the font URL to be able to override caching
* @param callback {Function?} Optional event listener callback that will be
* executed once the validator has determined whether the webFont was
* applied correctly.
* @param context {Object?} Optional context for the callback function
*/
__require : function(familyName, sources, fontWeight, fontStyle, comparisonString, version, callback, context)
{
var fontLookupKey = this.__createFontLookupKey(familyName, fontWeight, fontStyle);
if (!this.__createdStyles.includes(fontLookupKey)) {
var sourcesMap = this.__getSourcesMap(sources);
var rule = this.__getRule(familyName, fontWeight, fontStyle, sourcesMap, version);
if (!rule) {
throw new Error("Couldn't create @font-face rule for WebFont " + familyName + "!");
}
if (!this.__styleSheet) {
this.__styleSheet = qx.bom.Stylesheet.createElement();
}
try {
this.__addRule(rule);
}
catch(ex) {
if (qx.core.Environment.get("qx.debug")) {
this.warn("Error while adding @font-face rule:", ex.message);
return;
}
}
this.__createdStyles.push(fontLookupKey);
}
if (!this.__validators[familyName]) {
this.__validators[familyName] = new qx.bom.webfonts.Validator(familyName, comparisonString);
this.__validators[familyName].setTimeout(qx.bom.webfonts.Manager.VALIDATION_TIMEOUT);
this.__validators[familyName].addListenerOnce("changeStatus", this.__onFontChangeStatus, this);
}
if (callback) {
var cbContext = context || window;
this.__validators[familyName].addListenerOnce("changeStatus", callback, cbContext);
}
this.__validators[familyName].validate();
},
/**
* Processes the next item in the queue
*/
__flushQueue : function()
{
if (this.__queue.length == 0) {
this.__queueInterval.stop();
return;
}
var next = this.__queue.shift();
this.__require.apply(this, next);
},
/**
* Removes the font-face declaration if a font could not be validated
*
* @param ev {qx.event.type.Data} qx.bom.webfonts.Validator#changeStatus
*/
__onFontChangeStatus : function(ev)
{
var result = ev.getData();
if (result.valid === false) {
qx.event.Timer.once(function() {
this.remove(result.family);
}, this, 250);
}
},
/**
* Uses a naive regExp match to determine the format of each defined source
* file for a webFont. Returns a map with the format names as keys and the
* corresponding source URLs as values.
*
* @param sources {String[]} Array of source URLs
* @return {Map} Map of formats and URLs
*/
__getSourcesMap : function(sources)
{
var formats = qx.bom.webfonts.Manager.FONT_FORMATS;
var sourcesMap = {};
for (var i=0, l=sources.length; i<l; i++) {
var type = null;
for (var x=0; x < formats.length; x++) {
var reg = new RegExp("\.(" + formats[x] + ")");
var match = reg.exec(sources[i]);
if (match) {
type = match[1];
}
}
if (type) {
sourcesMap[type] = sources[i];
}
}
return sourcesMap;
},
/**
* Assembles the body of a font-face rule for a single webFont.
*
* @param familyName {String} Font-family name
* @param fontWeight {String} the web font should be registered using a
* fontWeight font weight.
* @param fontStyle {String} the web font should be registered using an
* fontStyle font style.
* @param sourcesMap {Map} Map of font formats and sources
* @param version {String?} Optional version to be appended to the URL
* @return {String} The computed CSS rule
*/
__getRule : function(familyName, fontWeight, fontStyle, sourcesMap, version)
{
var rules = [];
var formatList = this.__preferredFormats.length > 0
? this.__preferredFormats : qx.bom.webfonts.Manager.FONT_FORMATS;
for (var i=0,l=formatList.length; i<l; i++) {
var format = formatList[i];
if (sourcesMap[format]) {
rules.push(this.__getSourceForFormat(format, sourcesMap[format], version));
}
}
var rule = "src: " + rules.join(",\n") + ";";
rule = "font-family: " + familyName + ";\n" + rule;
rule = rule + "\nfont-style: " + (fontStyle ? fontStyle : "normal") + ";";
rule = rule + "\nfont-weight: " + (fontWeight ? fontWeight : "normal") + ";";
return rule;
},
/**
* Returns the full src value for a given font URL depending on the type
* @param format {String} The font format, one of eot, woff, ttf, svg
* @param url {String} The font file's URL
* @param version {String?} Optional version to be appended to the URL
* @return {String} The src directive
*/
__getSourceForFormat : function(format, url, version)
{
if (version) {
url += "?" + version;
}
switch(format) {
case "eot": return "url('" + url + "');" +
"src: url('" + url + "?#iefix') format('embedded-opentype')";
case "woff":
return "url('" + url + "') format('woff')";
case "ttf":
return "url('" + url + "') format('truetype')";
case "svg":
return "url('" + url + "') format('svg')";
default:
return null;
}
},
/**
* Adds a font-face rule to the document
*
* @param rule {String} The body of the CSS rule
*/
__addRule : function(rule)
{
var completeRule = "@font-face {" + rule + "}\n";
if (qx.core.Environment.get("browser.name") == "ie" &&
qx.core.Environment.get("browser.documentmode") < 9) {
var cssText = this.__fixCssText(this.__styleSheet.cssText);
cssText += completeRule;
this.__styleSheet.cssText = cssText;
}
else {
this.__styleSheet.insertRule(completeRule, this.__styleSheet.cssRules.length);
}
},
/**
* Removes the font-face declaration for the given font-family from the
* stylesheet
*
* @param familyName {String} The font-family name
* @param fontWeight {String} fontWeight font-weight.
* @param fontStyle {String} fontStyle font-style.
*/
__removeRule : function(familyName, fontWeight, fontStyle)
{
// In IE and edge even if the rule was added with font-style first
// and font-weight second, it is not guaranteed that the attributes
// remain in that order. Therefore we check for both version,
// style first, weight second and weight first, style second.
// Without this fix the rule isn't found and removed reliable.
var regtext =
"@font-face.*?" + familyName +
"(.*font-style: *" + (fontStyle ? fontStyle : "normal") +
".*font-weight: *" + (fontWeight ? fontWeight : "normal")+")|" +
"(.*font-weight: *" + (fontWeight ? fontWeight : "normal") +
".*font-style: *" + (fontStyle ? fontStyle : "normal")+")"
;
var reg = new RegExp(regtext, "m");
for (var i=0,l=document.styleSheets.length; i<l; i++) {
var sheet = document.styleSheets[i];
if (sheet.cssText) {
var cssText = sheet.cssText.replace(/\n/g, "").replace(/\r/g, "");
cssText = this.__fixCssText(cssText);
if (reg.exec(cssText)) {
cssText = cssText.replace(reg, "");
}
sheet.cssText = cssText;
}
else if (sheet.cssRules) {
for (var j=0,m=sheet.cssRules.length; j<m; j++) {
var cssText = sheet.cssRules[j].cssText.replace(/\n/g, "").replace(/\r/g, "");
if (reg.exec(cssText)) {
this.__styleSheet.deleteRule(j);
return;
}
}
}
}
},
/**
* IE 6 and 7 omit the trailing quote after the format name when
* querying cssText. This needs to be fixed before cssText is replaced
* or all rules will be invalid and no web fonts will work any more.
*
* @param cssText {String} CSS text
* @return {String} Fixed CSS text
*/
__fixCssText : function(cssText)
{
return cssText.replace("'eot)", "'eot')")
.replace("('embedded-opentype)", "('embedded-opentype')");
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
if (this.__queueInterval) {
this.__queueInterval.stop();
this.__queueInterval.dispose();
}
delete this.__createdStyles;
this.removeStyleSheet();
for (var prop in this.__validators) {
this.__validators[prop].dispose();
}
qx.bom.webfonts.Validator.removeDefaultHelperElements();
}
});