UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

492 lines (429 loc) 14.7 kB
/* ************************************************************************ 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. ************************************************************************ */ /** * Loads web fonts */ qx.Class.define("qx.bom.webfonts.WebFontLoader", { extend: qx.core.Object, construct(fontFamily) { super(); this.setFontFamily(fontFamily); this.__validators = {}; }, properties: { /** The font name that this font is known by */ fontFamily: { check: "String" }, /** The fontFaces which need to be defined */ fontFaces: { nullable: true, apply: "_applyFontFaces" }, /** CSS urls or paths which need to be loaded */ css: { nullable: true, check: "Array" }, /** * Characters that are used to test if the font has loaded properly. These * default to "WEei" in `qx.bom.webfont.Validator` and can be overridden * for certain cases like icon fonts that do not provide the predefined * characters. */ comparisonString: { check: "String", init: null, nullable: true }, /** * Version identifier that is appended to the URL to be loaded. Fonts * that are defined thru themes may be managed by the resource manager. * In this case updated fonts persist due to aggressive fontface caching * of some browsers. To get around this, set the `version` property to * the version of your font. It will be appended to the CSS URL and forces * the browser to re-validate. * * The version needs to be URL friendly, so only characters, numbers, * dash and dots are allowed here. */ version: { check(value) { return ( value === null || (typeof value === "string" && /^[a-zA-Z0-9.-]+$/.test(value)) ); }, init: null, nullable: true } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members: { __fontFacesQueue: null, __fontFacesCreatedPromise: null, _validators: null, getValidator(fontWeight, fontStyle) { fontWeight = fontWeight || "normal"; fontStyle = fontStyle || "normal"; let id = fontWeight + "::" + fontStyle; let validator = this.__validators[id]; if (!validator) { validator = this.__validators[id] = new qx.bom.webfonts.Validator( this.getFontFamily(), this.getComparisonString(), fontWeight, fontStyle ); validator.setTimeout(qx.bom.webfonts.WebFont.VALIDATION_TIMEOUT); validator.validate(); } return validator; }, /** * Called to load the font details into the browser */ async load() { (this.getCss() || []).forEach(url => { if (!url.match(/^https?:/)) { url = qx.util.ResourceManager.getInstance().toUri(url); } if (this.getVersion()) { url += url.indexOf("?") < 0 ? "?" : "&"; url += this.getVersion(); } qx.bom.webfonts.WebFontLoader.__loadStylesheet(url); }); let fontFaces = this.getFontFaces(); if (fontFaces) { fontFaces.forEach(fontFace => { if (fontFace.paths) { fontFace.paths = fontFace.paths.map(path => { if (!path.match(/^https?:/)) { path = qx.util.ResourceManager.getInstance().toUri(path); } if (this.getVersion()) { path += path.indexOf("?") < 0 ? "?" : "&"; path += this.getVersion(); } return path; }); } }); this.__fontFacesQueue = qx.lang.Array.clone(fontFaces); this.__fontFacesCreatedPromise = new qx.Promise(); } this.__dequeueFontFaces(); }, async promiseLoaded() { return await this.__fontFacesCreatedPromise; }, /** * Adds the font faces in __fontFacesQueue */ __dequeueFontFaces() { if (this.__fontFacesQueue == null) { return; } let fontFace = this.__fontFacesQueue.pop(); this.__addFontFace(fontFace); if (this.__fontFacesQueue.length == 0) { this.__fontFacesQueue = null; this.__fontFacesCreatedPromise.resolve(true); } if ( qx.core.Environment.get("engine.name") == "mshtml" && (parseInt(qx.core.Environment.get("engine.version")) < 9 || qx.core.Environment.get("browser.documentmode") < 9) ) { // old IEs need a break in between adding @font-face rules setTimeout(() => this.__dequeueFontFaces(), 100); } else { this.__dequeueFontFaces(); } }, /** * Adds a font face definition to the browser * * @param {*} fontFace - POJO of from the array in Manifest.json * @returns */ __addFontFace(fontFace) { let fontFamily = fontFace.fontFamily || this.getFontFamily(); let fontLookupKey = qx.bom.webfonts.WebFontLoader.createFontLookupKey( fontFamily, fontFace.fontWeight || "normal", fontFace.fontStyle || "normal" ); if (qx.bom.webfonts.WebFontLoader.__addedFontFaces[fontLookupKey]) { return; } if (!qx.bom.webfonts.WebFontLoader.__styleSheet) { let styleSheet = qx.bom.Stylesheet.createElement(); qx.bom.webfonts.WebFontLoader.__styleSheet = styleSheet; if (qx.core.Environment.get("qx.debug")) { styleSheet.ownerNode.setAttribute( "data-qx-created-by", qx.bom.webfonts.WebFontLoader.classname ); styleSheet.ownerNode.$$qxObject = this; } } let sourcesMap = {}; const MATCH_FORMAT = new RegExp( ".(" + qx.bom.webfonts.WebFontLoader.getPreferredFormats().join("|") + ")" ); /* * When compiling a `@font-face` rule, note that the first "src:" must never specify a format * and that EOT must go first if there is one */ let fontFaceSrcRules = []; for (let i = 0; i < fontFace.paths.length; i++) { let match = MATCH_FORMAT.exec(fontFace.paths[i]); if (!match) { continue; } let fontFormat = match[1]; let url = fontFace.paths[i]; if (this.getVersion()) { url += "?" + this.getVersion(); } fontFaceSrcRules.push({ url: url, format: fontFormat }); if (fontFormat == "eot") { fontFaceSrcRules.push({ url: url + "?#iefix'", format: "embedded-opentype" }); } } fontFaceSrcRules = fontFaceSrcRules.sort((a, b) => { a.fontFormat == "embedded-opentype" ? -1 : 0; }); let strSources = "src: "; for (let i = 0; i < fontFaceSrcRules.length; i++) { if (i > 0) { strSources += ", "; } strSources += "url('" + new URL(fontFaceSrcRules[i].url, document.baseURI).href + "')"; if (i > 0) { strSources += " format('" + fontFaceSrcRules[i].format + "')"; } } strSources += ";\n"; let rule = "font-family: " + fontFamily + ";\n"; rule += strSources + "\n"; rule += "font-style: " + (fontFace.fontStyle || "normal") + ";\n"; rule += "font-weight: " + (fontFace.fontWeight || "normal") + ";\n"; rule = "@font-face {\n" + rule + "}\n"; let styleSheet = qx.bom.webfonts.WebFontLoader.__styleSheet; try { if ( qx.core.Environment.get("browser.name") == "ie" && qx.core.Environment.get("browser.documentmode") < 9 ) { let cssText = qx.bom.webfonts.WebFontLoader.__fixCssText( styleSheet.cssText ); cssText += rule; styleSheet.cssText = cssText; } else { styleSheet.insertRule(rule, styleSheet.cssRules.length); } } catch (ex) { if (qx.core.Environment.get("qx.debug")) { this.warn("Error while adding @font-face rule:", ex.message); return; } } qx.bom.webfonts.WebFontLoader.__addedFontFaces[fontLookupKey] = true; }, // property apply _applyFontFaces(fontFaces, old) { var families = []; for (var i = 0, l = fontFaces.length; i < l; i++) { let fontFace = fontFaces[i]; var familyName = this._quoteFontFamily( fontFace.family || this.getFontFamily() ); if (families.indexOf(familyName) < 0) { families.push(familyName); } } }, /** * Makes sure font-family names containing spaces are properly quoted * * @param familyName {String} A font-family CSS value * @return {String} The quoted family name */ _quoteFontFamily(familyName) { return familyName.replace(/["']/g, ""); } }, 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", "woff2", "woff", "ttf", "svg"], /** * Timeout (in ms) to wait before deciding that a web font was not loaded. */ VALIDATION_TIMEOUT: 5000, /** @type{String[]} array of supported font formats, most preferred first */ __preferredFormats: null, /** */ __loadedStylesheets: {}, __addedFontFaces: {}, /** Loader instances indexed by font family name */ __loaders: {}, /** * Gets/creates a loader * * @param {String} name font family name * @param {Boolean?} create whether to create one if one does not exist (default to false) * @returns */ getLoader(name, create) { let loader = qx.bom.webfonts.WebFontLoader.__loaders[name]; if (!loader && create) { loader = qx.bom.webfonts.WebFontLoader.__loaders[name] = new qx.bom.webfonts.WebFontLoader(name); } return loader; }, /** * Adds a stylesheet, once per url * * @param {String} url */ __loadStylesheet(url) { if (qx.bom.webfonts.WebFontLoader.__loadedStylesheets[url]) { return; } qx.bom.Stylesheet.includeFile(url); qx.bom.webfonts.WebFontLoader.__loadedStylesheets[url] = true; }, /** * 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(familyName, fontWeight, fontStyle) { var lookupKey = familyName + "_" + (fontWeight ? fontWeight : "normal") + "_" + (fontStyle ? fontStyle : "normal"); return lookupKey; }, /** * 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(sources) { var formats = qx.bom.webfonts.WebFontLoader.FONT_FORMATS; var sourcesMap = {}; var reg = new RegExp(".(" + formats.join("|") + ")"); for (var i = 0, l = sources.length; i < l; i++) { var match = reg.exec(sources[i]); if (match) { var type = match[1]; sourcesMap[type] = sources[i]; } } return sourcesMap; }, /** * 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() { if (qx.bom.webfonts.WebFontLoader.__preferredFormats) { return qx.bom.webfonts.WebFontLoader.__preferredFormats; } 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 == "edge" && browserVersion >= 14) || (browser == "firefox" && browserVersion >= 69) || (browser == "chrome" && browserVersion >= 36) ) { preferredFormats.push("woff2"); } if ( (browser == "ie" && qx.core.Environment.get("browser.documentmode") >= 9) || (browser == "edge" && browserVersion >= 12) || (browser == "firefox" && browserVersion >= 3.6) || (browser == "chrome" && browserVersion >= 6) ) { preferredFormats.push("woff"); } if ( (browser == "edge" && browserVersion >= 12) || (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 (qx.bom.webfonts.WebFontLoader.__preferredFormats = preferredFormats); }, /** * 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(cssText) { return cssText .replace("'eot)", "'eot')") .replace("('embedded-opentype)", "('embedded-opentype')"); } } });