├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── exports.js ├── externs-commonjs.js ├── externs.js ├── fontfaceobserver.js ├── fontfaceobserver.standalone.js ├── package-lock.json ├── package.json ├── src ├── descriptors.js ├── observer.js └── ruler.js ├── test ├── assets │ ├── index.html │ ├── late.css │ ├── sourcesanspro-regular.eot │ ├── sourcesanspro-regular.ttf │ ├── sourcesanspro-regular.woff │ ├── subset.eot │ ├── subset.ttf │ └── subset.woff ├── browser-test.html ├── deps.js ├── index.html ├── observer-test.js └── ruler-test.js └── vendor └── google └── base.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | before_install: 6 | - npm install -g grunt-cli 7 | 8 | node_js: 9 | - "4" 10 | 11 | matrix: 12 | fast_finish: true 13 | 14 | cache: 15 | directories: 16 | - node_modules 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('google-closure-compiler').grunt(grunt, { 3 | max_parallel_compilations: require('os').cpus().length 4 | }); 5 | 6 | var compilerOptions = { 7 | compilation_level: 'ADVANCED_OPTIMIZATIONS', 8 | warning_level: 'VERBOSE', 9 | summary_detail_level: 3, 10 | language_in: 'ECMASCRIPT5_STRICT', 11 | output_wrapper: '(function(){%output%}());', 12 | use_types_for_optimization: true, 13 | externs: ['externs-commonjs.js'] 14 | }; 15 | 16 | var src = [ 17 | 'vendor/google/base.js', 18 | 'node_modules/closure-dom/src/dom.js', 19 | 'src/descriptors.js', 20 | 'src/ruler.js', 21 | 'src/observer.js', 22 | 'exports.js' 23 | ]; 24 | 25 | grunt.initConfig({ 26 | pkg: grunt.file.readJSON('package.json'), 27 | clean: { 28 | options: { 29 | force: true 30 | }, 31 | build: ['build'] 32 | }, 33 | exec: { 34 | test: './node_modules/.bin/mocha-headless-chrome -f test/index.html', 35 | deps: 'calcdeps -i src -i exports.js -p src -p ./vendor/google/base.js -p node_modules/closure-dom/src/dom.js -o deps > test/deps.js' 36 | }, 37 | jshint: { 38 | all: ['src/**/*.js'], 39 | options: { 40 | // ... better written as dot notation 41 | '-W069': true, 42 | 43 | // type definitions 44 | '-W030': true, 45 | 46 | // Don't make functions within loops 47 | '-W083': true, 48 | 49 | // Wrap the /regexp/ literal in parens to disambiguate the slash operator 50 | '-W092': true 51 | } 52 | }, 53 | 'closure-compiler': { 54 | dist: { 55 | files: { 56 | 'fontfaceobserver.js': src 57 | }, 58 | options: compilerOptions 59 | }, 60 | compile: { 61 | files: { 62 | 'build/fontfaceobserver.js': src 63 | }, 64 | options: compilerOptions 65 | }, 66 | debug: { 67 | files: { 68 | 'build/fontfaceobserver.debug.js': src 69 | }, 70 | options: Object.assign({}, compilerOptions, { 71 | debug: true, 72 | formatting: ['PRETTY_PRINT', 'PRINT_INPUT_DELIMITER'] 73 | }) 74 | } 75 | }, 76 | concat: { 77 | options: { 78 | banner: '/* Font Face Observer v<%= pkg.version %> - © Bram Stein. License: BSD-3-Clause */' 79 | }, 80 | dist_promises: { 81 | src: ['node_modules/promis/promise.js', 'build/fontfaceobserver.js'], 82 | dest: 'fontfaceobserver.js' 83 | }, 84 | dist: { 85 | src: ['build/fontfaceobserver.js'], 86 | dest: 'fontfaceobserver.standalone.js' 87 | } 88 | } 89 | }); 90 | 91 | grunt.loadNpmTasks('grunt-contrib-clean'); 92 | grunt.loadNpmTasks('grunt-contrib-jshint'); 93 | grunt.loadNpmTasks('grunt-contrib-concat'); 94 | grunt.loadNpmTasks('grunt-exec'); 95 | 96 | grunt.registerTask('compile', ['closure-compiler:compile']); 97 | grunt.registerTask('debug', ['closure-compiler:debug']); 98 | grunt.registerTask('default', ['compile']); 99 | grunt.registerTask('test', ['jshint', 'exec:test']); 100 | grunt.registerTask('dist', ['clean', 'closure-compiler:compile', 'concat:dist', 'concat:dist_promises']); 101 | }; 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 - Bram Stein 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Font Face Observer [![Build Status](https://travis-ci.org/bramstein/fontfaceobserver.png?branch=master)](https://travis-ci.org/bramstein/fontfaceobserver) 2 | 3 | Font Face Observer is a small `@font-face` loader and monitor (3.5KB minified and 1.3KB gzipped) compatible with any webfont service. It will monitor when a webfont is loaded and notify you. It does not limit you in any way in where, when, or how you load your webfonts. Unlike the [Web Font Loader](https://github.com/typekit/webfontloader) Font Face Observer uses scroll events to detect font loads efficiently and with minimum overhead. 4 | 5 | ## How to use 6 | 7 | Include your `@font-face` rules as usual. Fonts can be supplied by either a font service such as [Google Fonts](http://www.google.com/fonts), [Typekit](http://typekit.com), and [Webtype](http://webtype.com) or be self-hosted. You can set up monitoring for a single font family at a time: 8 | 9 | ```js 10 | var font = new FontFaceObserver('My Family', { 11 | weight: 400 12 | }); 13 | 14 | font.load().then(function () { 15 | console.log('Font is available'); 16 | }, function () { 17 | console.log('Font is not available'); 18 | }); 19 | ``` 20 | 21 | The `FontFaceObserver` constructor takes two arguments: the font-family name (required) and an object describing the variation (optional). The object can contain `weight`, `style`, and `stretch` properties. If a property is not present it will default to `normal`. To start loading the font, call the `load` method. It'll immediately return a new Promise that resolves when the font is loaded and rejected when the font fails to load. 22 | 23 | If your font doesn't contain at least the latin "BESbwy" characters you must pass a custom test string to the `load` method. 24 | 25 | ```js 26 | var font = new FontFaceObserver('My Family'); 27 | 28 | font.load('中国').then(function () { 29 | console.log('Font is available'); 30 | }, function () { 31 | console.log('Font is not available'); 32 | }); 33 | ``` 34 | 35 | The default timeout for giving up on font loading is 3 seconds. You can increase or decrease this by passing a number of milliseconds as the second parameter to the `load` method. 36 | 37 | ```js 38 | var font = new FontFaceObserver('My Family'); 39 | 40 | font.load(null, 5000).then(function () { 41 | console.log('Font is available'); 42 | }, function () { 43 | console.log('Font is not available after waiting 5 seconds'); 44 | }); 45 | ``` 46 | 47 | Multiple fonts can be loaded by creating a `FontFaceObserver` instance for each. 48 | 49 | ```js 50 | var fontA = new FontFaceObserver('Family A'); 51 | var fontB = new FontFaceObserver('Family B'); 52 | 53 | fontA.load().then(function () { 54 | console.log('Family A is available'); 55 | }); 56 | 57 | fontB.load().then(function () { 58 | console.log('Family B is available'); 59 | }); 60 | ``` 61 | 62 | You may also load both at the same time, rather than loading each individually. 63 | 64 | ```js 65 | var fontA = new FontFaceObserver('Family A'); 66 | var fontB = new FontFaceObserver('Family B'); 67 | 68 | Promise.all([fontA.load(), fontB.load()]).then(function () { 69 | console.log('Family A & B have loaded'); 70 | }); 71 | ``` 72 | 73 | If you are working with a large number of fonts, you may decide to create `FontFaceObserver` instances dynamically: 74 | 75 | ```js 76 | // An example collection of font data with additional metadata, 77 | // in this case “color.” 78 | var exampleFontData = { 79 | 'Family A': { weight: 400, color: 'red' }, 80 | 'Family B': { weight: 400, color: 'orange' }, 81 | 'Family C': { weight: 900, color: 'yellow' }, 82 | // Etc. 83 | }; 84 | 85 | var observers = []; 86 | 87 | // Make one observer for each font, 88 | // by iterating over the data we already have 89 | Object.keys(exampleFontData).forEach(function(family) { 90 | var data = exampleFontData[family]; 91 | var obs = new FontFaceObserver(family, data); 92 | observers.push(obs.load()); 93 | }); 94 | 95 | Promise.all(observers) 96 | .then(function(fonts) { 97 | fonts.forEach(function(font) { 98 | console.log(font.family + ' ' + font.weight + ' ' + 'loaded'); 99 | 100 | // Map the result of the Promise back to our existing data, 101 | // to get the other properties we need. 102 | console.log(exampleFontData[font.family].color); 103 | }); 104 | }) 105 | .catch(function(err) { 106 | console.warn('Some critical font are not available:', err); 107 | }); 108 | ``` 109 | 110 | The following example emulates FOUT with Font Face Observer for `My Family`. 111 | 112 | ```js 113 | var font = new FontFaceObserver('My Family'); 114 | 115 | font.load().then(function () { 116 | document.documentElement.className += " fonts-loaded"; 117 | }); 118 | ``` 119 | 120 | ```css 121 | .fonts-loaded { 122 | body { 123 | font-family: My Family, sans-serif; 124 | } 125 | } 126 | ``` 127 | 128 | ## Installation 129 | 130 | If you're using npm you can install Font Face Observer as a dependency: 131 | 132 | ```shell 133 | $ npm install fontfaceobserver 134 | ``` 135 | 136 | You can then require `fontfaceobserver` as a CommonJS (Browserify) module: 137 | 138 | ```js 139 | var FontFaceObserver = require('fontfaceobserver'); 140 | 141 | var font = new FontFaceObserver('My Family'); 142 | 143 | font.load().then(function () { 144 | console.log('My Family has loaded'); 145 | }); 146 | ``` 147 | 148 | If you're not using npm, grab `fontfaceobserver.js` or `fontfaceobserver.standalone.js` (see below) and include it in your project. It'll export a global `FontFaceObserver` that you can use to create new instances. 149 | 150 | Font Face Observer uses Promises in its API, so for [browsers that do not support promises](http://caniuse.com/#search=promise) you'll need to include a polyfill. If you use your own Promise polyfill you just need to include `fontfaceobserver.standalone.js` in your project. If you do not have an existing Promise polyfill you should use `fontfaceobserver.js` which includes a small Promise polyfill. Using the Promise polyfill adds roughly 1.4KB (500 bytes gzipped) to the file size. 151 | 152 | ## Browser support 153 | 154 | FontFaceObserver has been tested and works on the following browsers: 155 | 156 | * Chrome (desktop & Android) 157 | * Firefox 158 | * Opera 159 | * Safari (desktop & iOS) 160 | * IE8+ 161 | * Android WebKit 162 | 163 | ## License 164 | 165 | Font Face Observer is licensed under the BSD License. Copyright 2014-2017 Bram Stein. All rights reserved. 166 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontfaceobserver", 3 | "description": "Fast and simple web font loading.", 4 | "main": "fontfaceobserver.standalone.js", 5 | "authors": [ 6 | "Bram Stein (http://www.bramstein.com/)" 7 | ], 8 | "license": "BSD-3-Clause", 9 | "keywords": [ 10 | "fontloader", 11 | "fonts", 12 | "font", 13 | "font-face", 14 | "web", 15 | "font", 16 | "font", 17 | "load", 18 | "font", 19 | "events" 20 | ], 21 | "homepage": "https://github.com/bramstein/fontfaceobserver" 22 | } 23 | -------------------------------------------------------------------------------- /exports.js: -------------------------------------------------------------------------------- 1 | goog.require('fontface.Observer'); 2 | 3 | if (typeof module === 'object') { 4 | module.exports = fontface.Observer; 5 | } else { 6 | window['FontFaceObserver'] = fontface.Observer; 7 | window['FontFaceObserver']['prototype']['load'] = fontface.Observer.prototype.load; 8 | } 9 | -------------------------------------------------------------------------------- /externs-commonjs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @type {Object} 4 | */ 5 | var module = {}; 6 | 7 | /** 8 | * @type {Object} 9 | */ 10 | module.exports = {}; 11 | -------------------------------------------------------------------------------- /externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constructor 3 | * 4 | * @param {string} family 5 | * @param {Object} descriptors 6 | */ 7 | var FontFaceObserver = function (family, descriptors) {}; 8 | 9 | /** 10 | * @param {string=} opt_text 11 | * @param {number=} opt_timeout 12 | * 13 | * @return {Promise.} 14 | */ 15 | FontFaceObserver.prototype.load = function (opt_text, opt_timeout) {}; 16 | -------------------------------------------------------------------------------- /fontfaceobserver.js: -------------------------------------------------------------------------------- 1 | /* Font Face Observer v2.3.0 - © Bram Stein. License: BSD-3-Clause */(function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}} 2 | function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})}; 3 | function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;kparseInt(a[1],10)):F=!1);return F}function M(a){null===H&&(H=!!a.document.fonts);return H} 8 | function N(a,c){var b=a.style,g=a.weight;if(null===G){var e=document.createElement("div");try{e.style.font="condensed 100px sans-serif"}catch(q){}G=""!==e.style.font}return[b,g,G?a.stretch:"","100px",c].join(" ")} 9 | D.prototype.load=function(a,c){var b=this,g=a||"BESbswy",e=0,q=c||3E3,J=(new Date).getTime();return new Promise(function(K,L){if(M(b.context)&&!I(b.context)){var O=new Promise(function(r,t){function h(){(new Date).getTime()-J>=q?t(Error(""+q+"ms timeout exceeded")):b.context.document.fonts.load(N(b,'"'+b.family+'"'),g).then(function(n){1<=n.length?r():setTimeout(h,25)},t)}h()}),P=new Promise(function(r,t){e=setTimeout(function(){t(Error(""+q+"ms timeout exceeded"))},q)});Promise.race([P,O]).then(function(){clearTimeout(e); 10 | K(b)},L)}else u(function(){function r(){var d;if(d=-1!=k&&-1!=l||-1!=k&&-1!=m||-1!=l&&-1!=m)(d=k!=l&&k!=m&&l!=m)||(null===E&&(d=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),E=!!d&&(536>parseInt(d[1],10)||536===parseInt(d[1],10)&&11>=parseInt(d[2],10))),d=E&&(k==y&&l==y&&m==y||k==z&&l==z&&m==z||k==A&&l==A&&m==A)),d=!d;d&&(null!==f.parentNode&&f.parentNode.removeChild(f),clearTimeout(e),K(b))}function t(){if((new Date).getTime()-J>=q)null!==f.parentNode&&f.parentNode.removeChild(f), 11 | L(Error(""+q+"ms timeout exceeded"));else{var d=b.context.document.hidden;if(!0===d||void 0===d)k=h.g.offsetWidth,l=n.g.offsetWidth,m=v.g.offsetWidth,r();e=setTimeout(t,50)}}var h=new w(g),n=new w(g),v=new w(g),k=-1,l=-1,m=-1,y=-1,z=-1,A=-1,f=document.createElement("div");f.dir="ltr";x(h,N(b,"sans-serif"));x(n,N(b,"serif"));x(v,N(b,"monospace"));f.appendChild(h.g);f.appendChild(n.g);f.appendChild(v.g);b.context.document.body.appendChild(f);y=h.g.offsetWidth;z=n.g.offsetWidth;A=v.g.offsetWidth;t(); 12 | C(h,function(d){k=d;r()});x(h,N(b,'"'+b.family+'",sans-serif'));C(n,function(d){l=d;r()});x(n,N(b,'"'+b.family+'",serif'));C(v,function(d){m=d;r()});x(v,N(b,'"'+b.family+'",monospace'))})})};"object"===typeof module?module.exports=D:(window.FontFaceObserver=D,window.FontFaceObserver.prototype.load=D.prototype.load);}()); 13 | -------------------------------------------------------------------------------- /fontfaceobserver.standalone.js: -------------------------------------------------------------------------------- 1 | /* Font Face Observer v2.3.0 - © Bram Stein. License: BSD-3-Clause */(function(){function p(a,c){document.addEventListener?a.addEventListener("scroll",c,!1):a.attachEvent("scroll",c)}function u(a){document.body?a():document.addEventListener?document.addEventListener("DOMContentLoaded",function b(){document.removeEventListener("DOMContentLoaded",b);a()}):document.attachEvent("onreadystatechange",function g(){if("interactive"==document.readyState||"complete"==document.readyState)document.detachEvent("onreadystatechange",g),a()})};function w(a){this.g=document.createElement("div");this.g.setAttribute("aria-hidden","true");this.g.appendChild(document.createTextNode(a));this.h=document.createElement("span");this.i=document.createElement("span");this.m=document.createElement("span");this.j=document.createElement("span");this.l=-1;this.h.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.i.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;"; 2 | this.j.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.m.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;";this.h.appendChild(this.m);this.i.appendChild(this.j);this.g.appendChild(this.h);this.g.appendChild(this.i)} 3 | function x(a,c){a.g.style.cssText="max-width:none;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;white-space:nowrap;font-synthesis:none;font:"+c+";"}function B(a){var c=a.g.offsetWidth,b=c+100;a.j.style.width=b+"px";a.i.scrollLeft=b;a.h.scrollLeft=a.h.scrollWidth+100;return a.l!==c?(a.l=c,!0):!1}function C(a,c){function b(){var e=g;B(e)&&null!==e.g.parentNode&&c(e.l)}var g=a;p(a.h,b);p(a.i,b);B(a)};function D(a,c,b){c=c||{};b=b||window;this.family=a;this.style=c.style||"normal";this.weight=c.weight||"normal";this.stretch=c.stretch||"normal";this.context=b}var E=null,F=null,G=null,H=null;function I(a){null===F&&(M(a)&&/Apple/.test(window.navigator.vendor)?(a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(window.navigator.userAgent),F=!!a&&603>parseInt(a[1],10)):F=!1);return F}function M(a){null===H&&(H=!!a.document.fonts);return H} 4 | function N(a,c){var b=a.style,g=a.weight;if(null===G){var e=document.createElement("div");try{e.style.font="condensed 100px sans-serif"}catch(q){}G=""!==e.style.font}return[b,g,G?a.stretch:"","100px",c].join(" ")} 5 | D.prototype.load=function(a,c){var b=this,g=a||"BESbswy",e=0,q=c||3E3,J=(new Date).getTime();return new Promise(function(K,L){if(M(b.context)&&!I(b.context)){var O=new Promise(function(r,t){function h(){(new Date).getTime()-J>=q?t(Error(""+q+"ms timeout exceeded")):b.context.document.fonts.load(N(b,'"'+b.family+'"'),g).then(function(n){1<=n.length?r():setTimeout(h,25)},t)}h()}),P=new Promise(function(r,t){e=setTimeout(function(){t(Error(""+q+"ms timeout exceeded"))},q)});Promise.race([P,O]).then(function(){clearTimeout(e); 6 | K(b)},L)}else u(function(){function r(){var d;if(d=-1!=k&&-1!=l||-1!=k&&-1!=m||-1!=l&&-1!=m)(d=k!=l&&k!=m&&l!=m)||(null===E&&(d=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),E=!!d&&(536>parseInt(d[1],10)||536===parseInt(d[1],10)&&11>=parseInt(d[2],10))),d=E&&(k==y&&l==y&&m==y||k==z&&l==z&&m==z||k==A&&l==A&&m==A)),d=!d;d&&(null!==f.parentNode&&f.parentNode.removeChild(f),clearTimeout(e),K(b))}function t(){if((new Date).getTime()-J>=q)null!==f.parentNode&&f.parentNode.removeChild(f), 7 | L(Error(""+q+"ms timeout exceeded"));else{var d=b.context.document.hidden;if(!0===d||void 0===d)k=h.g.offsetWidth,l=n.g.offsetWidth,m=v.g.offsetWidth,r();e=setTimeout(t,50)}}var h=new w(g),n=new w(g),v=new w(g),k=-1,l=-1,m=-1,y=-1,z=-1,A=-1,f=document.createElement("div");f.dir="ltr";x(h,N(b,"sans-serif"));x(n,N(b,"serif"));x(v,N(b,"monospace"));f.appendChild(h.g);f.appendChild(n.g);f.appendChild(v.g);b.context.document.body.appendChild(f);y=h.g.offsetWidth;z=n.g.offsetWidth;A=v.g.offsetWidth;t(); 8 | C(h,function(d){k=d;r()});x(h,N(b,'"'+b.family+'",sans-serif'));C(n,function(d){l=d;r()});x(n,N(b,'"'+b.family+'",serif'));C(v,function(d){m=d;r()});x(v,N(b,'"'+b.family+'",monospace'))})})};"object"===typeof module?module.exports=D:(window.FontFaceObserver=D,window.FontFaceObserver.prototype.load=D.prototype.load);}()); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontfaceobserver", 3 | "version": "2.3.0", 4 | "description": "Detect if web fonts are available", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/bramstein/fontfaceobserver.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/bramstein/fontfaceobserver/issues" 14 | }, 15 | "homepage": "https://fontfaceobserver.com/", 16 | "main": "fontfaceobserver.standalone.js", 17 | "keywords": [ 18 | "fontloader", 19 | "fonts", 20 | "font", 21 | "font-face", 22 | "web font", 23 | "font load", 24 | "font events" 25 | ], 26 | "files": [ 27 | "fontfaceobserver.js", 28 | "fontfaceobserver.standalone.js", 29 | "src/*.js", 30 | "externs.js" 31 | ], 32 | "author": "Bram Stein (http://www.bramstein.com/)", 33 | "license": "BSD-2-Clause", 34 | "devDependencies": { 35 | "closure-dom": "=0.2.6", 36 | "google-closure-compiler": "=v20220502", 37 | "grunt": "^1.0.3", 38 | "grunt-contrib-clean": "^2.0.1", 39 | "grunt-contrib-concat": "^1.0.1", 40 | "grunt-contrib-jshint": "^3.2.0", 41 | "grunt-exec": "~1.0.0", 42 | "mocha": "^10.0.0", 43 | "mocha-headless-chrome": "^4.0.0", 44 | "promis": "=1.1.4", 45 | "sinon": "^14.0.0", 46 | "unexpected": "^13.0.0" 47 | }, 48 | "scripts": { 49 | "preversion": "npm test", 50 | "version": "grunt dist && git add fontfaceobserver.js && git add fontfaceobserver.standalone.js", 51 | "postversion": "git push && git push --tags && rm -rf build && npm publish", 52 | "test": "grunt test" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/descriptors.js: -------------------------------------------------------------------------------- 1 | goog.provide('fontface.Descriptors'); 2 | 3 | /** 4 | * @typedef {{ 5 | * style: (string|undefined), 6 | * weight: (string|undefined), 7 | * stretch: (string|undefined) 8 | * }} 9 | */ 10 | fontface.Descriptors; 11 | -------------------------------------------------------------------------------- /src/observer.js: -------------------------------------------------------------------------------- 1 | goog.provide('fontface.Observer'); 2 | 3 | goog.require('fontface.Ruler'); 4 | goog.require('dom'); 5 | 6 | goog.scope(function () { 7 | var Ruler = fontface.Ruler; 8 | 9 | /** 10 | * @constructor 11 | * 12 | * @param {string} family 13 | * @param {fontface.Descriptors=} opt_descriptors 14 | * @param {Window=} opt_context 15 | */ 16 | fontface.Observer = function (family, opt_descriptors, opt_context) { 17 | var descriptors = opt_descriptors || {}; 18 | var context = opt_context || window; 19 | 20 | /** 21 | * @type {string} 22 | */ 23 | this['family'] = family; 24 | 25 | /** 26 | * @type {string} 27 | */ 28 | this['style'] = descriptors.style || 'normal'; 29 | 30 | /** 31 | * @type {string} 32 | */ 33 | this['weight'] = descriptors.weight || 'normal'; 34 | 35 | /** 36 | * @type {string} 37 | */ 38 | this['stretch'] = descriptors.stretch || 'normal'; 39 | 40 | /** 41 | * @type {Window} 42 | */ 43 | this['context'] = context; 44 | }; 45 | 46 | var Observer = fontface.Observer; 47 | 48 | /** 49 | * @type {null|boolean} 50 | */ 51 | Observer.HAS_WEBKIT_FALLBACK_BUG = null; 52 | 53 | /** 54 | * @type {null|boolean} 55 | */ 56 | Observer.HAS_SAFARI_10_BUG = null; 57 | 58 | /** 59 | * @type {null|boolean} 60 | */ 61 | Observer.SUPPORTS_STRETCH = null; 62 | 63 | /** 64 | * @type {null|boolean} 65 | */ 66 | Observer.SUPPORTS_NATIVE_FONT_LOADING = null; 67 | 68 | /** 69 | * @type {number} 70 | */ 71 | Observer.DEFAULT_TIMEOUT = 3000; 72 | 73 | /** 74 | * @return {string} 75 | */ 76 | Observer.getUserAgent = function () { 77 | return window.navigator.userAgent; 78 | }; 79 | 80 | /** 81 | * @return {string} 82 | */ 83 | Observer.getNavigatorVendor = function () { 84 | return window.navigator.vendor; 85 | }; 86 | 87 | /** 88 | * Returns true if this browser is WebKit and it has the fallback bug 89 | * which is present in WebKit 536.11 and earlier. 90 | * 91 | * @return {boolean} 92 | */ 93 | Observer.hasWebKitFallbackBug = function () { 94 | if (Observer.HAS_WEBKIT_FALLBACK_BUG === null) { 95 | var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(Observer.getUserAgent()); 96 | 97 | Observer.HAS_WEBKIT_FALLBACK_BUG = !!match && 98 | (parseInt(match[1], 10) < 536 || 99 | (parseInt(match[1], 10) === 536 && 100 | parseInt(match[2], 10) <= 11)); 101 | } 102 | return Observer.HAS_WEBKIT_FALLBACK_BUG; 103 | }; 104 | 105 | /** 106 | * Returns true if the browser has the Safari 10 bugs. The 107 | * native font load API in Safari 10 has two bugs that cause 108 | * the document.fonts.load and FontFace.prototype.load methods 109 | * to return promises that don't reliably get settled. 110 | * 111 | * The bugs are described in more detail here: 112 | * - https://bugs.webkit.org/show_bug.cgi?id=165037 113 | * - https://bugs.webkit.org/show_bug.cgi?id=164902 114 | * 115 | * If the browser is made by Apple, and has native font 116 | * loading support, it is potentially affected. But the API 117 | * was fixed around AppleWebKit version 603, so any newer 118 | * versions that that does not contain the bug. 119 | * 120 | * @return {boolean} 121 | */ 122 | Observer.hasSafari10Bug = function (context) { 123 | if (Observer.HAS_SAFARI_10_BUG === null) { 124 | if (Observer.supportsNativeFontLoading(context) && /Apple/.test(Observer.getNavigatorVendor())) { 125 | var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(Observer.getUserAgent()); 126 | 127 | Observer.HAS_SAFARI_10_BUG = !!match && parseInt(match[1], 10) < 603; 128 | } else { 129 | Observer.HAS_SAFARI_10_BUG = false; 130 | } 131 | } 132 | return Observer.HAS_SAFARI_10_BUG; 133 | }; 134 | 135 | /** 136 | * Returns true if the browser supports the native font loading 137 | * API. 138 | * 139 | * @return {boolean} 140 | */ 141 | Observer.supportsNativeFontLoading = function (context) { 142 | if (Observer.SUPPORTS_NATIVE_FONT_LOADING === null) { 143 | Observer.SUPPORTS_NATIVE_FONT_LOADING = !!context.document['fonts']; 144 | } 145 | return Observer.SUPPORTS_NATIVE_FONT_LOADING; 146 | }; 147 | 148 | /** 149 | * Returns true if the browser supports font-style in the font 150 | * short-hand syntax. 151 | * 152 | * @return {boolean} 153 | */ 154 | Observer.supportStretch = function () { 155 | if (Observer.SUPPORTS_STRETCH === null) { 156 | var div = dom.createElement('div'); 157 | 158 | try { 159 | div.style.font = 'condensed 100px sans-serif'; 160 | } catch (e) {} 161 | Observer.SUPPORTS_STRETCH = (div.style.font !== ''); 162 | } 163 | 164 | return Observer.SUPPORTS_STRETCH; 165 | }; 166 | 167 | /** 168 | * @private 169 | * 170 | * @param {string} family 171 | * @return {string} 172 | */ 173 | Observer.prototype.getStyle = function (family) { 174 | return [this['style'], this['weight'], Observer.supportStretch() ? this['stretch'] : '', '100px', family].join(' '); 175 | }; 176 | 177 | /** 178 | * Returns the current time in milliseconds 179 | * 180 | * @return {number} 181 | */ 182 | Observer.prototype.getTime = function () { 183 | return new Date().getTime(); 184 | }; 185 | 186 | /** 187 | * @param {string=} text Optional test string to use for detecting if a font is available. 188 | * @param {number=} timeout Optional timeout for giving up on font load detection and rejecting the promise (defaults to 3 seconds). 189 | * @return {Promise.} 190 | */ 191 | Observer.prototype.load = function (text, timeout) { 192 | var that = this; 193 | var testString = text || 'BESbswy'; 194 | var timeoutId = 0; 195 | var timeoutValue = timeout || Observer.DEFAULT_TIMEOUT; 196 | var start = that.getTime(); 197 | 198 | return new Promise(function (resolve, reject) { 199 | if (Observer.supportsNativeFontLoading(that['context']) && !Observer.hasSafari10Bug(that['context'])) { 200 | var loader = new Promise(function (resolve, reject) { 201 | var check = function () { 202 | var now = that.getTime(); 203 | 204 | if (now - start >= timeoutValue) { 205 | reject(new Error('' + timeoutValue + 'ms timeout exceeded')); 206 | } else { 207 | that['context'].document.fonts.load(that.getStyle('"' + that['family'] + '"'), testString).then(function (fonts) { 208 | if (fonts.length >= 1) { 209 | resolve(); 210 | } else { 211 | setTimeout(check, 25); 212 | } 213 | }, reject); 214 | } 215 | }; 216 | check(); 217 | }); 218 | 219 | var timer = new Promise(function (resolve, reject) { 220 | timeoutId = setTimeout( 221 | function() { reject(new Error('' + timeoutValue + 'ms timeout exceeded')); }, 222 | timeoutValue 223 | ); 224 | }); 225 | 226 | Promise.race([timer, loader]).then(function () { 227 | clearTimeout(timeoutId); 228 | resolve(that); 229 | }, reject); 230 | } else { 231 | dom.waitForBody(function () { 232 | var rulerA = new Ruler(testString); 233 | var rulerB = new Ruler(testString); 234 | var rulerC = new Ruler(testString); 235 | 236 | var widthA = -1; 237 | var widthB = -1; 238 | var widthC = -1; 239 | 240 | var fallbackWidthA = -1; 241 | var fallbackWidthB = -1; 242 | var fallbackWidthC = -1; 243 | 244 | var container = dom.createElement('div'); 245 | 246 | /** 247 | * @private 248 | */ 249 | function removeContainer() { 250 | if (container.parentNode !== null) { 251 | dom.remove(container.parentNode, container); 252 | } 253 | } 254 | 255 | /** 256 | * @private 257 | * 258 | * If metric compatible fonts are detected, one of the widths will be -1. This is 259 | * because a metric compatible font won't trigger a scroll event. We work around 260 | * this by considering a font loaded if at least two of the widths are the same. 261 | * Because we have three widths, this still prevents false positives. 262 | * 263 | * Cases: 264 | * 1) Font loads: both a, b and c are called and have the same value. 265 | * 2) Font fails to load: resize callback is never called and timeout happens. 266 | * 3) WebKit bug: both a, b and c are called and have the same value, but the 267 | * values are equal to one of the last resort fonts, we ignore this and 268 | * continue waiting until we get new values (or a timeout). 269 | */ 270 | function check() { 271 | if ((widthA != -1 && widthB != -1) || (widthA != -1 && widthC != -1) || (widthB != -1 && widthC != -1)) { 272 | if (widthA == widthB || widthA == widthC || widthB == widthC) { 273 | // All values are the same, so the browser has most likely loaded the web font 274 | 275 | if (Observer.hasWebKitFallbackBug()) { 276 | // Except if the browser has the WebKit fallback bug, in which case we check to see if all 277 | // values are set to one of the last resort fonts. 278 | 279 | if (((widthA == fallbackWidthA && widthB == fallbackWidthA && widthC == fallbackWidthA) || 280 | (widthA == fallbackWidthB && widthB == fallbackWidthB && widthC == fallbackWidthB) || 281 | (widthA == fallbackWidthC && widthB == fallbackWidthC && widthC == fallbackWidthC))) { 282 | // The width we got matches some of the known last resort fonts, so let's assume we're dealing with the last resort font. 283 | return; 284 | } 285 | } 286 | removeContainer(); 287 | clearTimeout(timeoutId); 288 | resolve(that); 289 | } 290 | } 291 | } 292 | 293 | // This ensures the scroll direction is correct. 294 | container.dir = 'ltr'; 295 | 296 | rulerA.setFont(that.getStyle('sans-serif')); 297 | rulerB.setFont(that.getStyle('serif')); 298 | rulerC.setFont(that.getStyle('monospace')); 299 | 300 | dom.append(container, rulerA.getElement()); 301 | dom.append(container, rulerB.getElement()); 302 | dom.append(container, rulerC.getElement()); 303 | 304 | dom.append(that['context'].document.body, container); 305 | 306 | fallbackWidthA = rulerA.getWidth(); 307 | fallbackWidthB = rulerB.getWidth(); 308 | fallbackWidthC = rulerC.getWidth(); 309 | 310 | function checkForTimeout() { 311 | var now = that.getTime(); 312 | 313 | if (now - start >= timeoutValue) { 314 | removeContainer(); 315 | reject(new Error('' + timeoutValue + 'ms timeout exceeded')); 316 | } else { 317 | var hidden = that['context'].document['hidden']; 318 | if (hidden === true || hidden === undefined) { 319 | widthA = rulerA.getWidth(); 320 | widthB = rulerB.getWidth(); 321 | widthC = rulerC.getWidth(); 322 | check(); 323 | } 324 | timeoutId = setTimeout(checkForTimeout, 50); 325 | } 326 | } 327 | 328 | checkForTimeout(); 329 | 330 | 331 | rulerA.onResize(function (width) { 332 | widthA = width; 333 | check(); 334 | }); 335 | 336 | rulerA.setFont(that.getStyle('"' + that['family'] + '",sans-serif')); 337 | 338 | rulerB.onResize(function (width) { 339 | widthB = width; 340 | check(); 341 | }); 342 | 343 | rulerB.setFont(that.getStyle('"' + that['family'] + '",serif')); 344 | 345 | rulerC.onResize(function (width) { 346 | widthC = width; 347 | check(); 348 | }); 349 | 350 | rulerC.setFont(that.getStyle('"' + that['family'] + '",monospace')); 351 | }); 352 | } 353 | }); 354 | }; 355 | }); 356 | -------------------------------------------------------------------------------- /src/ruler.js: -------------------------------------------------------------------------------- 1 | goog.provide('fontface.Ruler'); 2 | 3 | goog.require('dom'); 4 | 5 | goog.scope(function () { 6 | /** 7 | * @constructor 8 | * @param {string} text 9 | */ 10 | fontface.Ruler = function (text) { 11 | var style = 'max-width:none;' + 12 | 'display:inline-block;' + 13 | 'position:absolute;' + 14 | 'height:100%;' + 15 | 'width:100%;' + 16 | 'overflow:scroll;' + 17 | 'font-size:16px;'; 18 | 19 | this.element = dom.createElement('div'); 20 | this.element.setAttribute('aria-hidden', 'true'); 21 | 22 | dom.append(this.element, dom.createText(text)); 23 | 24 | this.collapsible = dom.createElement('span'); 25 | this.expandable = dom.createElement('span'); 26 | this.collapsibleInner = dom.createElement('span'); 27 | this.expandableInner = dom.createElement('span'); 28 | 29 | this.lastOffsetWidth = -1; 30 | 31 | dom.style(this.collapsible, style); 32 | dom.style(this.expandable, style); 33 | dom.style(this.expandableInner, style); 34 | dom.style(this.collapsibleInner, 'display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;'); 35 | 36 | dom.append(this.collapsible, this.collapsibleInner); 37 | dom.append(this.expandable, this.expandableInner); 38 | 39 | dom.append(this.element, this.collapsible); 40 | dom.append(this.element, this.expandable); 41 | }; 42 | 43 | var Ruler = fontface.Ruler; 44 | 45 | /** 46 | * @return {Element} 47 | */ 48 | Ruler.prototype.getElement = function () { 49 | return this.element; 50 | }; 51 | 52 | /** 53 | * @param {string} font 54 | */ 55 | Ruler.prototype.setFont = function (font) { 56 | dom.style(this.element, 'max-width:none;' + 57 | 'min-width:20px;' + 58 | 'min-height:20px;' + 59 | 'display:inline-block;' + 60 | 'overflow:hidden;' + 61 | 'position:absolute;' + 62 | 'width:auto;' + 63 | 'margin:0;' + 64 | 'padding:0;' + 65 | 'top:-999px;' + 66 | 'white-space:nowrap;' + 67 | 'font-synthesis:none;' + 68 | 'font:' + font + ';'); 69 | }; 70 | 71 | /** 72 | * @return {number} 73 | */ 74 | Ruler.prototype.getWidth = function () { 75 | return this.element.offsetWidth; 76 | }; 77 | 78 | /** 79 | * @param {string} width 80 | */ 81 | Ruler.prototype.setWidth = function (width) { 82 | this.element.style.width = width + 'px'; 83 | }; 84 | 85 | /** 86 | * @private 87 | * 88 | * @return {boolean} 89 | */ 90 | Ruler.prototype.reset = function () { 91 | var offsetWidth = this.getWidth(), 92 | width = offsetWidth + 100; 93 | 94 | this.expandableInner.style.width = width + 'px'; 95 | this.expandable.scrollLeft = width; 96 | this.collapsible.scrollLeft = this.collapsible.scrollWidth + 100; 97 | 98 | if (this.lastOffsetWidth !== offsetWidth) { 99 | this.lastOffsetWidth = offsetWidth; 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | }; 105 | 106 | /** 107 | * @private 108 | * @param {function(number)} callback 109 | */ 110 | Ruler.prototype.onScroll = function (callback) { 111 | if (this.reset() && this.element.parentNode !== null) { 112 | callback(this.lastOffsetWidth); 113 | } 114 | }; 115 | 116 | /** 117 | * @param {function(number)} callback 118 | */ 119 | Ruler.prototype.onResize = function (callback) { 120 | var that = this; 121 | 122 | function onScroll() { 123 | that.onScroll(callback); 124 | } 125 | 126 | dom.addListener(this.collapsible, 'scroll', onScroll); 127 | dom.addListener(this.expandable, 'scroll', onScroll); 128 | this.reset(); 129 | }; 130 | }); 131 | -------------------------------------------------------------------------------- /test/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/assets/late.css: -------------------------------------------------------------------------------- 1 | @font-face { font-family: observer-test9; src: url(sourcesanspro-regular.woff) format('woff'), url(sourcesanspro-regular.ttf) format('truetype'); } 2 | -------------------------------------------------------------------------------- /test/assets/sourcesanspro-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/sourcesanspro-regular.eot -------------------------------------------------------------------------------- /test/assets/sourcesanspro-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/sourcesanspro-regular.ttf -------------------------------------------------------------------------------- /test/assets/sourcesanspro-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/sourcesanspro-regular.woff -------------------------------------------------------------------------------- /test/assets/subset.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/subset.eot -------------------------------------------------------------------------------- /test/assets/subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/subset.ttf -------------------------------------------------------------------------------- /test/assets/subset.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontfaceobserver/46553ba71b2a1bcf33a3f16a84dfc88d7dbe616b/test/assets/subset.woff -------------------------------------------------------------------------------- /test/browser-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Browser Test 5 | 6 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/deps.js: -------------------------------------------------------------------------------- 1 | // This file was autogenerated by calcdeps.js 2 | goog.addDependency('../../node_modules/closure-dom/src/dom.js', ['dom'], []); 3 | goog.addDependency('../../src/descriptors.js', ['fontface.Descriptors'], []); 4 | goog.addDependency('../../src/observer.js', ['fontface.Observer'], ['fontface.Ruler','dom']); 5 | goog.addDependency('../../src/ruler.js', ['fontface.Ruler'], ['dom']); 6 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FontFaceObserver 6 | 7 | 8 | 73 | 74 | 75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /test/observer-test.js: -------------------------------------------------------------------------------- 1 | describe('Observer', function () { 2 | var Observer = fontface.Observer, 3 | Ruler = fontface.Ruler; 4 | 5 | describe('#constructor', function () { 6 | it('creates a new instance with the correct signature', function () { 7 | var observer = new Observer('my family', {}); 8 | expect(observer, 'not to be', null); 9 | expect(observer.load, 'to be a function'); 10 | }); 11 | 12 | it('parses descriptors', function () { 13 | var observer = new Observer('my family', { 14 | weight: 'bold' 15 | }); 16 | 17 | expect(observer.family, 'to equal', 'my family'); 18 | expect(observer.weight, 'to equal', 'bold'); 19 | }); 20 | 21 | it('defaults descriptors that are not given', function () { 22 | var observer = new Observer('my family', { 23 | weight: 'bold' 24 | }); 25 | 26 | expect(observer.style, 'to equal', 'normal'); 27 | }); 28 | 29 | it('defaults context to current window', function () { 30 | var observer = new Observer('my family', {}); 31 | 32 | expect(observer.context, 'to equal', window); 33 | }); 34 | }); 35 | 36 | describe('#getStyle', function () { 37 | it('creates the correct default style', function () { 38 | var observer = new Observer('my family', {}); 39 | 40 | if (Observer.supportStretch()) { 41 | expect(observer.getStyle('sans-serif'), 'to equal', 'normal normal normal 100px sans-serif'); 42 | } else { 43 | expect(observer.getStyle('sans-serif'), 'to equal', 'normal normal 100px sans-serif'); 44 | } 45 | }); 46 | 47 | it('passes through all descriptors', function () { 48 | var observer = new Observer('my family', { 49 | style: 'italic', 50 | weight: 'bold', 51 | stretch: 'condensed' 52 | }); 53 | 54 | if (Observer.supportStretch()) { 55 | expect(observer.getStyle('sans-serif'), 'to equal', 'italic bold condensed 100px sans-serif'); 56 | } else { 57 | expect(observer.getStyle('sans-serif'), 'to equal', 'italic bold 100px sans-serif'); 58 | } 59 | }); 60 | }); 61 | 62 | describe('#load', function () { 63 | this.timeout(5000); 64 | 65 | it('finds a font and resolve the promise', function (done) { 66 | var observer = new Observer('observer-test1', {}), 67 | ruler = new Ruler('hello'); 68 | 69 | document.body.appendChild(ruler.getElement()); 70 | 71 | ruler.setFont('monospace', ''); 72 | var beforeWidth = ruler.getWidth(); 73 | 74 | ruler.setFont('100px observer-test1, monospace'); 75 | observer.load(null, 5000).then(function () { 76 | var activeWidth = ruler.getWidth(); 77 | 78 | expect(activeWidth, 'not to equal', beforeWidth); 79 | 80 | setTimeout(function () { 81 | var afterWidth = ruler.getWidth(); 82 | 83 | expect(afterWidth, 'to equal', activeWidth); 84 | expect(afterWidth, 'not to equal', beforeWidth); 85 | document.body.removeChild(ruler.getElement()); 86 | done(); 87 | }, 0); 88 | }, function () { 89 | done(new Error('Timeout')); 90 | }); 91 | }); 92 | 93 | it('finds a font and resolve the promise in an iframe (context)', function (done) { 94 | var iframe = document.createElement('iframe'); 95 | iframe.style.position = 'fixed'; 96 | iframe.style.left = '-9999px'; 97 | document.body.appendChild(iframe); 98 | 99 | // Can't just load a local HTML file, because under file:// we wouldn't 100 | // be able to interact with it due to same-origin policy 101 | var style = iframe.contentWindow.document.createElement('style'); 102 | style.textContent = 103 | "@font-face {" + 104 | " font-family: observer-test1;" + 105 | " src: url(assets/sourcesanspro-regular.woff) format('woff')," + 106 | " url(assets/sourcesanspro-regular.ttf) format('truetype');" + 107 | "}"; 108 | iframe.contentWindow.document.head.appendChild(style); 109 | 110 | var observer = new Observer('observer-test1', {}, iframe.contentWindow), 111 | ruler = new Ruler('hello'); 112 | 113 | iframe.contentWindow.document.body.appendChild(ruler.getElement()); 114 | 115 | ruler.setFont('monospace', ''); 116 | var beforeWidth = ruler.getWidth(); 117 | 118 | ruler.setFont('100px observer-test1, monospace'); 119 | observer.load(null, 5000).then(function () { 120 | var activeWidth = ruler.getWidth(); 121 | 122 | expect(activeWidth, 'not to equal', beforeWidth); 123 | 124 | setTimeout(function () { 125 | var afterWidth = ruler.getWidth(); 126 | 127 | expect(afterWidth, 'to equal', activeWidth); 128 | expect(afterWidth, 'not to equal', beforeWidth); 129 | document.body.removeChild(iframe); 130 | done(); 131 | }, 0); 132 | }, function () { 133 | document.body.removeChild(iframe); 134 | done(new Error('Timeout')); 135 | }); 136 | }); 137 | 138 | it('finds a font and resolves the promise even though the @font-face rule is not in the CSSOM yet', function (done) { 139 | var observer = new Observer('observer-test9', {}), 140 | ruler = new Ruler('hello'); 141 | 142 | document.body.appendChild(ruler.getElement()); 143 | 144 | ruler.setFont('monospace', ''); 145 | var beforeWidth = ruler.getWidth(); 146 | 147 | ruler.setFont('100px observer-test9, monospace'); 148 | observer.load(null, 10000).then(function () { 149 | var activeWidth = ruler.getWidth(); 150 | 151 | expect(activeWidth, 'not to equal', beforeWidth); 152 | 153 | setTimeout(function () { 154 | var afterWidth = ruler.getWidth(); 155 | 156 | expect(afterWidth, 'to equal', activeWidth); 157 | expect(afterWidth, 'not to equal', beforeWidth); 158 | document.body.removeChild(ruler.getElement()); 159 | done(); 160 | }, 0); 161 | }, function () { 162 | done(new Error('Timeout')); 163 | }); 164 | 165 | // We don't use a style element here because IE9/10 have issues with 166 | // dynamically inserted @font-face rules. 167 | var link = document.createElement('link'); 168 | 169 | link.rel = 'stylesheet'; 170 | link.href = 'assets/late.css'; 171 | 172 | document.head.appendChild(link); 173 | }); 174 | 175 | it('finds a font and resolve the promise even when the page is RTL', function (done) { 176 | var observer = new Observer('observer-test8', {}), 177 | ruler = new Ruler('hello'); 178 | 179 | document.body.dir = 'rtl'; 180 | document.body.appendChild(ruler.getElement()); 181 | 182 | ruler.setFont('monospace', ''); 183 | var beforeWidth = ruler.getWidth(); 184 | 185 | ruler.setFont('100px observer-test1, monospace'); 186 | observer.load(null, 5000).then(function () { 187 | var activeWidth = ruler.getWidth(); 188 | 189 | expect(activeWidth, 'not to equal', beforeWidth); 190 | 191 | setTimeout(function () { 192 | var afterWidth = ruler.getWidth(); 193 | 194 | expect(afterWidth, 'to equal', activeWidth); 195 | expect(afterWidth, 'not to equal', beforeWidth); 196 | document.body.removeChild(ruler.getElement()); 197 | document.body.dir = 'ltr'; 198 | done(); 199 | }, 0); 200 | }, function () { 201 | done(new Error('Timeout')); 202 | }); 203 | }); 204 | 205 | 206 | it('finds a font with spaces in the name and resolve the promise', function (done) { 207 | var observer = new Observer('Trebuchet W01 Regular', {}), 208 | ruler = new Ruler('hello'); 209 | 210 | document.body.appendChild(ruler.getElement()); 211 | 212 | ruler.setFont('100px monospace'); 213 | var beforeWidth = ruler.getWidth(); 214 | 215 | ruler.setFont('100px "Trebuchet W01 Regular", monospace'); 216 | observer.load(null, 5000).then(function () { 217 | var activeWidth = ruler.getWidth(); 218 | 219 | expect(activeWidth, 'not to equal', beforeWidth); 220 | 221 | setTimeout(function () { 222 | var afterWidth = ruler.getWidth(); 223 | 224 | expect(afterWidth, 'to equal', activeWidth); 225 | expect(afterWidth, 'not to equal', beforeWidth); 226 | document.body.removeChild(ruler.getElement()); 227 | done(); 228 | }, 0); 229 | }, function () { 230 | done(new Error('Timeout')); 231 | }); 232 | }); 233 | 234 | it('loads a font with spaces and numbers in the name and resolve the promise', function (done) { 235 | var observer = new Observer('Neue Frutiger 1450 W04', {}), 236 | ruler = new Ruler('hello'); 237 | 238 | document.body.appendChild(ruler.getElement()); 239 | 240 | ruler.setFont('100px monospace'); 241 | var beforeWidth = ruler.getWidth(); 242 | 243 | ruler.setFont('100px "Neue Frutiger 1450 W04", monospace'); 244 | observer.load(null, 5000).then(function () { 245 | var activeWidth = ruler.getWidth(); 246 | 247 | expect(activeWidth, 'not to equal', beforeWidth); 248 | 249 | setTimeout(function () { 250 | var afterWidth = ruler.getWidth(); 251 | 252 | expect(afterWidth, 'to equal', activeWidth); 253 | expect(afterWidth, 'not to equal', beforeWidth); 254 | document.body.removeChild(ruler.getElement()); 255 | done(); 256 | }, 0); 257 | }, function () { 258 | done(new Error('Timeout')); 259 | }); 260 | }); 261 | 262 | it('fails to find a font and reject the promise', function (done) { 263 | var observer = new Observer('observer-test2', {}); 264 | 265 | observer.load(null, 50).then(function () { 266 | done(new Error('Should not resolve')); 267 | }, function (err) { 268 | try { 269 | done(); 270 | } catch(testFailure) { 271 | done(testFailure); 272 | } 273 | }); 274 | }); 275 | 276 | it('finds the font even if it is already loaded', function (done) { 277 | var observer = new Observer('observer-test3', {}); 278 | 279 | observer.load(null, 5000).then(function () { 280 | observer.load(null, 5000).then(function () { 281 | done(); 282 | }, function () { 283 | done(new Error('Second call failed')); 284 | }); 285 | }, function () { 286 | done(new Error('Timeout')); 287 | }); 288 | }); 289 | 290 | it('finds a font with a custom unicode range within ASCII', function (done) { 291 | var observer = new Observer('observer-test4', {}), 292 | ruler = new Ruler('\u0021'); 293 | 294 | ruler.setFont('monospace', ''); 295 | document.body.appendChild(ruler.getElement()); 296 | 297 | var beforeWidth = ruler.getWidth(); 298 | 299 | ruler.setFont('100px observer-test4,monospace'); 300 | 301 | observer.load('\u0021', 5000).then(function () { 302 | var activeWidth = ruler.getWidth(); 303 | 304 | expect(activeWidth, 'not to equal', beforeWidth); 305 | 306 | setTimeout(function () { 307 | var afterWidth = ruler.getWidth(); 308 | 309 | expect(afterWidth, 'to equal', activeWidth); 310 | expect(afterWidth, 'not to equal', beforeWidth); 311 | 312 | document.body.removeChild(ruler.getElement()); 313 | done(); 314 | }, 0); 315 | }, function () { 316 | done(new Error('Timeout')); 317 | }); 318 | }); 319 | 320 | it('finds a font with a custom unicode range outside ASCII (but within BMP)', function (done) { 321 | var observer = new Observer('observer-test5', {}), 322 | ruler = new Ruler('\u4e2d\u56fd'); 323 | 324 | ruler.setFont('100px monospace'); 325 | document.body.appendChild(ruler.getElement()); 326 | 327 | var beforeWidth = ruler.getWidth(); 328 | 329 | ruler.setFont('100px observer-test5,monospace'); 330 | 331 | observer.load('\u4e2d\u56fd', 5000).then(function () { 332 | var activeWidth = ruler.getWidth(); 333 | 334 | expect(activeWidth, 'not to equal', beforeWidth); 335 | 336 | setTimeout(function () { 337 | var afterWidth = ruler.getWidth(); 338 | 339 | expect(afterWidth, 'to equal', activeWidth); 340 | expect(afterWidth, 'not to equal', beforeWidth); 341 | 342 | document.body.removeChild(ruler.getElement()); 343 | 344 | done(); 345 | }, 0); 346 | }, function () { 347 | done(new Error('Timeout')); 348 | }); 349 | }); 350 | 351 | it('finds a font with a custom unicode range outside the BMP', function (done) { 352 | var observer = new Observer('observer-test6', {}), 353 | ruler = new Ruler('\udbff\udfff'); 354 | 355 | ruler.setFont('100px monospace'); 356 | document.body.appendChild(ruler.getElement()); 357 | 358 | var beforeWidth = ruler.getWidth(); 359 | 360 | ruler.setFont('100px observer-test6,monospace'); 361 | 362 | observer.load('\udbff\udfff', 5000).then(function () { 363 | var activeWidth = ruler.getWidth(); 364 | 365 | expect(activeWidth, 'not to equal', beforeWidth); 366 | 367 | setTimeout(function () { 368 | var afterWidth = ruler.getWidth(); 369 | 370 | expect(afterWidth, 'to equal', activeWidth); 371 | expect(afterWidth, 'not to equal', beforeWidth); 372 | 373 | document.body.removeChild(ruler.getElement()); 374 | 375 | done(); 376 | }, 0); 377 | }, function () { 378 | done(new Error('Timeout')); 379 | }); 380 | }); 381 | 382 | it('fails to find the font if it is available but does not contain the test string', function (done) { 383 | var observer = new Observer('observer-test7', {}); 384 | 385 | observer.load(null, 50).then(function () { 386 | done(new Error('Should not be called')); 387 | }, function () { 388 | done(); 389 | }); 390 | }); 391 | 392 | xit('finds a locally installed font', function (done) { 393 | var observer = new Observer('sans-serif', {}); 394 | 395 | observer.load(null, 50).then(function () { 396 | done(); 397 | }, function () { 398 | done(new Error('Did not detect local font')); 399 | }); 400 | }); 401 | 402 | xit('finds a locally installed font with the same metrics as the a fallback font (on OS X)', function (done) { 403 | var observer = new Observer('serif', {}); 404 | 405 | observer.load(null, 50).then(function () { 406 | done(); 407 | }, function () { 408 | done(new Error('Did not detect local font')); 409 | }); 410 | }); 411 | }); 412 | 413 | describe('hasSafari10Bug', function () { 414 | var getUserAgent = null; 415 | var getNavigatorVendor = null; 416 | var supportsNativeFontLoading = null; 417 | 418 | beforeEach(function () { 419 | Observer.HAS_SAFARI_10_BUG = null; 420 | 421 | getUserAgent = sinon.stub(Observer, 'getUserAgent'); 422 | getNavigatorVendor = sinon.stub(Observer, 'getNavigatorVendor'); 423 | supportsNativeFontLoading = sinon.stub(Observer, 'supportsNativeFontLoading'); 424 | }); 425 | 426 | afterEach(function () { 427 | getUserAgent.restore(); 428 | getNavigatorVendor.restore(); 429 | supportsNativeFontLoading.restore(); 430 | }); 431 | 432 | it('returns false when the user agent is not WebKit', function () { 433 | getUserAgent.returns('Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/15.0 Firefox/14.0'); 434 | getNavigatorVendor.returns('Google'); 435 | supportsNativeFontLoading.returns(true); 436 | 437 | expect(Observer.hasSafari10Bug(), 'to be false'); 438 | }); 439 | 440 | it('returns true if the browser is an affected version of Safari 10', function () { 441 | getUserAgent.returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14'); 442 | getNavigatorVendor.returns('Apple'); 443 | supportsNativeFontLoading.returns(true); 444 | 445 | expect(Observer.hasSafari10Bug(), 'to be true'); 446 | }); 447 | 448 | it('returns true if the browser is an WebView with an affected version of Safari 10', function () { 449 | getUserAgent.returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.2.14 (KHTML, like Gecko) FxiOS/6.1 Safari/602.2.14'); 450 | getNavigatorVendor.returns('Apple'); 451 | supportsNativeFontLoading.returns(true); 452 | 453 | expect(Observer.hasSafari10Bug(), 'to be true'); 454 | }); 455 | 456 | it('returns false in older versions of Safari', function () { 457 | getUserAgent.returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/9.3.2 Safari/537.75.14'); 458 | getNavigatorVendor.returns('Apple'); 459 | supportsNativeFontLoading.returns(false); 460 | 461 | expect(Observer.hasSafari10Bug(), 'to be false'); 462 | }); 463 | 464 | it('returns false in newer versions of Safari', function () { 465 | getUserAgent.returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/603.1.20 (KHTML, like Gecko) Version/10.1 Safari/603.1.20'); 466 | getNavigatorVendor.returns('Apple'); 467 | supportsNativeFontLoading.returns(true); 468 | 469 | expect(Observer.hasSafari10Bug(), 'to be false'); 470 | }); 471 | }); 472 | 473 | describe('hasWebKitFallbackBug', function () { 474 | var getUserAgent = null; 475 | 476 | beforeEach(function () { 477 | Observer.HAS_WEBKIT_FALLBACK_BUG = null; 478 | 479 | getUserAgent = sinon.stub(Observer, 'getUserAgent'); 480 | }); 481 | 482 | afterEach(function () { 483 | getUserAgent.restore(); 484 | }); 485 | 486 | it('returns false when the user agent is not WebKit', function () { 487 | getUserAgent.returns('Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/15.0 Firefox/14.0'); 488 | 489 | expect(Observer.hasWebKitFallbackBug(), 'to be false'); 490 | }); 491 | 492 | it('returns false when the user agent is WebKit but the bug is not present', function () { 493 | getUserAgent.returns('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.12 (KHTML, like Gecko) Chrome/20.0.814.2 Safari/536.12'); 494 | 495 | expect(Observer.hasWebKitFallbackBug(), 'to be false'); 496 | }); 497 | 498 | it('returns true when the user agent is WebKit and the bug is present', function () { 499 | getUserAgent.returns('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.814.2 Safari/536.11'); 500 | 501 | expect(Observer.hasWebKitFallbackBug(), 'to be true'); 502 | }); 503 | 504 | it('returns true when the user agent is WebKit and the bug is present in an old version', function () { 505 | getUserAgent.returns('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/20.0.814.2 Safari/535.19'); 506 | 507 | expect(Observer.hasWebKitFallbackBug(), 'to be true'); 508 | }); 509 | 510 | it('caches the results', function () { 511 | getUserAgent.returns('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.814.2 Safari/536.11'); 512 | 513 | expect(Observer.hasWebKitFallbackBug(), 'to be true'); 514 | 515 | getUserAgent.returns('Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/15.0 Firefox/14.0'); 516 | 517 | expect(Observer.hasWebKitFallbackBug(), 'to be true'); 518 | }); 519 | }); 520 | }); 521 | -------------------------------------------------------------------------------- /test/ruler-test.js: -------------------------------------------------------------------------------- 1 | describe('Ruler', function () { 2 | var Ruler = fontface.Ruler, 3 | ruler = null; 4 | 5 | beforeEach(function () { 6 | ruler = new Ruler('hello'); 7 | ruler.setFont('', ''); 8 | ruler.setWidth(100); 9 | document.body.appendChild(ruler.getElement()); 10 | }); 11 | 12 | afterEach(function () { 13 | document.body.removeChild(ruler.getElement()); 14 | ruler = null; 15 | }); 16 | 17 | describe('#constructor', function () { 18 | it('creates a new instance with the correct signature', function () { 19 | expect(ruler, 'not to be', null); 20 | expect(ruler.onResize, 'to be a function'); 21 | expect(ruler.setFont, 'to be a function'); 22 | }); 23 | }); 24 | 25 | describe('#onResize', function () { 26 | it('detects expansion', function (done) { 27 | ruler.onResize(function (width) { 28 | expect(width, 'to equal', 200); 29 | done(); 30 | }); 31 | 32 | ruler.setWidth(200); 33 | }); 34 | 35 | it('detects multiple expansions', function (done) { 36 | var first = true; 37 | 38 | ruler.onResize(function (width) { 39 | if (first) { 40 | expect(width, 'to equal', 200); 41 | ruler.setWidth(300); 42 | first = false; 43 | } else { 44 | expect(width, 'to equal', 300); 45 | done(); 46 | } 47 | }); 48 | 49 | ruler.setWidth(200); 50 | }); 51 | 52 | it('detects collapse', function (done) { 53 | ruler.onResize(function (width) { 54 | expect(width, 'to equal', 50); 55 | done(); 56 | }); 57 | 58 | ruler.setWidth(50); 59 | }); 60 | 61 | it('detects multiple collapses', function (done) { 62 | var first = true; 63 | 64 | ruler.onResize(function (width) { 65 | if (first) { 66 | expect(width, 'to equal', 70); 67 | ruler.setWidth(50); 68 | first = false; 69 | } else { 70 | expect(width, 'to equal', 50); 71 | done(); 72 | } 73 | }); 74 | 75 | ruler.setWidth(70); 76 | }); 77 | 78 | it('detects a collapse and an expansion', function (done) { 79 | var first = true; 80 | 81 | ruler.onResize(function (width) { 82 | if (first) { 83 | expect(width, 'to equal', 70); 84 | ruler.setWidth(100); 85 | first = false; 86 | } else { 87 | expect(width, 'to equal', 100); 88 | done(); 89 | } 90 | }); 91 | 92 | ruler.setWidth(70); 93 | }); 94 | 95 | it('detects an expansion and a collapse', function (done) { 96 | var first = true; 97 | 98 | ruler.onResize(function (width) { 99 | if (first) { 100 | expect(width, 'to equal', 200); 101 | ruler.setWidth(100); 102 | first = false; 103 | } else { 104 | expect(width, 'to equal', 100); 105 | done(); 106 | } 107 | }); 108 | 109 | ruler.setWidth(200); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /vendor/google/base.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 The Closure Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Bootstrap for the Google JS Library (Closure). 17 | * 18 | * In uncompiled mode base.js will write out Closure's deps file, unless the 19 | * global CLOSURE_NO_DEPS is set to true. This allows projects to 20 | * include their own deps file(s) from different locations. 21 | * 22 | * 23 | * @provideGoog 24 | */ 25 | 26 | 27 | /** 28 | * @define {boolean} Overridden to true by the compiler when --closure_pass 29 | * or --mark_as_compiled is specified. 30 | */ 31 | var COMPILED = false; 32 | 33 | 34 | /** 35 | * Base namespace for the Closure library. Checks to see goog is 36 | * already defined in the current scope before assigning to prevent 37 | * clobbering if base.js is loaded more than once. 38 | * 39 | * @const 40 | */ 41 | var goog = goog || {}; // Identifies this file as the Closure base. 42 | 43 | 44 | /** 45 | * Reference to the global context. In most cases this will be 'window'. 46 | */ 47 | goog.global = window; 48 | 49 | 50 | /** 51 | * @define {boolean} DEBUG is provided as a convenience so that debugging code 52 | * that should not be included in a production js_binary can be easily stripped 53 | * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most 54 | * toString() methods should be declared inside an "if (goog.DEBUG)" conditional 55 | * because they are generally used for debugging purposes and it is difficult 56 | * for the JSCompiler to statically determine whether they are used. 57 | */ 58 | goog.DEBUG = true; 59 | 60 | 61 | /** 62 | * @define {string} LOCALE defines the locale being used for compilation. It is 63 | * used to select locale specific data to be compiled in js binary. BUILD rule 64 | * can specify this value by "--define goog.LOCALE=" as JSCompiler 65 | * option. 66 | * 67 | * Take into account that the locale code format is important. You should use 68 | * the canonical Unicode format with hyphen as a delimiter. Language must be 69 | * lowercase, Language Script - Capitalized, Region - UPPERCASE. 70 | * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. 71 | * 72 | * See more info about locale codes here: 73 | * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers 74 | * 75 | * For language codes you should use values defined by ISO 693-1. See it here 76 | * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from 77 | * this rule: the Hebrew language. For legacy reasons the old code (iw) should 78 | * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. 79 | */ 80 | goog.LOCALE = 'en'; // default to en 81 | 82 | 83 | /** 84 | * @define {boolean} Whether this code is running on trusted sites. 85 | * 86 | * On untrusted sites, several native functions can be defined or overridden by 87 | * external libraries like Prototype, Datejs, and JQuery and setting this flag 88 | * to false forces closure to use its own implementations when possible. 89 | * 90 | * If your javascript can be loaded by a third party site and you are wary about 91 | * relying on non-standard implementations, specify 92 | * "--define goog.TRUSTED_SITE=false" to the JSCompiler. 93 | */ 94 | goog.TRUSTED_SITE = true; 95 | 96 | 97 | /** 98 | * Creates object stubs for a namespace. The presence of one or more 99 | * goog.provide() calls indicate that the file defines the given 100 | * objects/namespaces. Build tools also scan for provide/require statements 101 | * to discern dependencies, build dependency files (see deps.js), etc. 102 | * @see goog.require 103 | * @param {string} name Namespace provided by this file in the form 104 | * "goog.package.part". 105 | */ 106 | goog.provide = function(name) { 107 | if (!COMPILED) { 108 | // Ensure that the same namespace isn't provided twice. This is intended 109 | // to teach new developers that 'goog.provide' is effectively a variable 110 | // declaration. And when JSCompiler transforms goog.provide into a real 111 | // variable declaration, the compiled JS should work the same as the raw 112 | // JS--even when the raw JS uses goog.provide incorrectly. 113 | if (goog.isProvided_(name)) { 114 | throw Error('Namespace "' + name + '" already declared.'); 115 | } 116 | delete goog.implicitNamespaces_[name]; 117 | 118 | var namespace = name; 119 | while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { 120 | if (goog.getObjectByName(namespace)) { 121 | break; 122 | } 123 | goog.implicitNamespaces_[namespace] = true; 124 | } 125 | } 126 | 127 | goog.exportPath_(name); 128 | }; 129 | 130 | 131 | /** 132 | * Marks that the current file should only be used for testing, and never for 133 | * live code in production. 134 | * @param {string=} opt_message Optional message to add to the error that's 135 | * raised when used in production code. 136 | */ 137 | goog.setTestOnly = function(opt_message) { 138 | if (COMPILED && !goog.DEBUG) { 139 | opt_message = opt_message || ''; 140 | throw Error('Importing test-only code into non-debug environment' + 141 | opt_message ? ': ' + opt_message : '.'); 142 | } 143 | }; 144 | 145 | 146 | if (!COMPILED) { 147 | 148 | /** 149 | * Check if the given name has been goog.provided. This will return false for 150 | * names that are available only as implicit namespaces. 151 | * @param {string} name name of the object to look for. 152 | * @return {boolean} Whether the name has been provided. 153 | * @private 154 | */ 155 | goog.isProvided_ = function(name) { 156 | return !goog.implicitNamespaces_[name] && !!goog.getObjectByName(name); 157 | }; 158 | 159 | /** 160 | * Namespaces implicitly defined by goog.provide. For example, 161 | * goog.provide('goog.events.Event') implicitly declares 162 | * that 'goog' and 'goog.events' must be namespaces. 163 | * 164 | * @type {Object} 165 | * @private 166 | */ 167 | goog.implicitNamespaces_ = {}; 168 | } 169 | 170 | 171 | /** 172 | * Builds an object structure for the provided namespace path, 173 | * ensuring that names that already exist are not overwritten. For 174 | * example: 175 | * "a.b.c" -> a = {};a.b={};a.b.c={}; 176 | * Used by goog.provide and goog.exportSymbol. 177 | * @param {string} name name of the object that this file defines. 178 | * @param {*=} opt_object the object to expose at the end of the path. 179 | * @param {Object=} opt_objectToExportTo The object to add the path to; default 180 | * is |goog.global|. 181 | * @private 182 | */ 183 | goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { 184 | var parts = name.split('.'); 185 | var cur = opt_objectToExportTo || goog.global; 186 | 187 | // Internet Explorer exhibits strange behavior when throwing errors from 188 | // methods externed in this manner. See the testExportSymbolExceptions in 189 | // base_test.html for an example. 190 | if (!(parts[0] in cur) && cur.execScript) { 191 | cur.execScript('var ' + parts[0]); 192 | } 193 | 194 | // Certain browsers cannot parse code in the form for((a in b); c;); 195 | // This pattern is produced by the JSCompiler when it collapses the 196 | // statement above into the conditional loop below. To prevent this from 197 | // happening, use a for-loop and reserve the init logic as below. 198 | 199 | // Parentheses added to eliminate strict JS warning in Firefox. 200 | for (var part; parts.length && (part = parts.shift());) { 201 | if (!parts.length && goog.isDef(opt_object)) { 202 | // last part and we have an object; use it 203 | cur[part] = opt_object; 204 | } else if (cur[part]) { 205 | cur = cur[part]; 206 | } else { 207 | cur = cur[part] = {}; 208 | } 209 | } 210 | }; 211 | 212 | 213 | /** 214 | * Returns an object based on its fully qualified external name. If you are 215 | * using a compilation pass that renames property names beware that using this 216 | * function will not find renamed properties. 217 | * 218 | * @param {string} name The fully qualified name. 219 | * @param {Object=} opt_obj The object within which to look; default is 220 | * |goog.global|. 221 | * @return {?} The value (object or primitive) or, if not found, null. 222 | */ 223 | goog.getObjectByName = function(name, opt_obj) { 224 | var parts = name.split('.'); 225 | var cur = opt_obj || goog.global; 226 | for (var part; part = parts.shift(); ) { 227 | if (goog.isDefAndNotNull(cur[part])) { 228 | cur = cur[part]; 229 | } else { 230 | return null; 231 | } 232 | } 233 | return cur; 234 | }; 235 | 236 | 237 | /** 238 | * Globalizes a whole namespace, such as goog or goog.lang. 239 | * 240 | * @param {Object} obj The namespace to globalize. 241 | * @param {Object=} opt_global The object to add the properties to. 242 | * @deprecated Properties may be explicitly exported to the global scope, but 243 | * this should no longer be done in bulk. 244 | */ 245 | goog.globalize = function(obj, opt_global) { 246 | var global = opt_global || goog.global; 247 | for (var x in obj) { 248 | global[x] = obj[x]; 249 | } 250 | }; 251 | 252 | 253 | /** 254 | * Adds a dependency from a file to the files it requires. 255 | * @param {string} relPath The path to the js file. 256 | * @param {Array} provides An array of strings with the names of the objects 257 | * this file provides. 258 | * @param {Array} requires An array of strings with the names of the objects 259 | * this file requires. 260 | */ 261 | goog.addDependency = function(relPath, provides, requires) { 262 | if (!COMPILED) { 263 | var provide, require; 264 | var path = relPath.replace(/\\/g, '/'); 265 | var deps = goog.dependencies_; 266 | for (var i = 0; provide = provides[i]; i++) { 267 | deps.nameToPath[provide] = path; 268 | if (!(path in deps.pathToNames)) { 269 | deps.pathToNames[path] = {}; 270 | } 271 | deps.pathToNames[path][provide] = true; 272 | } 273 | for (var j = 0; require = requires[j]; j++) { 274 | if (!(path in deps.requires)) { 275 | deps.requires[path] = {}; 276 | } 277 | deps.requires[path][require] = true; 278 | } 279 | } 280 | }; 281 | 282 | 283 | 284 | 285 | // NOTE(nnaze): The debug DOM loader was included in base.js as an orignal 286 | // way to do "debug-mode" development. The dependency system can sometimes 287 | // be confusing, as can the debug DOM loader's asyncronous nature. 288 | // 289 | // With the DOM loader, a call to goog.require() is not blocking -- the 290 | // script will not load until some point after the current script. If a 291 | // namespace is needed at runtime, it needs to be defined in a previous 292 | // script, or loaded via require() with its registered dependencies. 293 | // User-defined namespaces may need their own deps file. See http://go/js_deps, 294 | // http://go/genjsdeps, or, externally, DepsWriter. 295 | // http://code.google.com/closure/library/docs/depswriter.html 296 | // 297 | // Because of legacy clients, the DOM loader can't be easily removed from 298 | // base.js. Work is being done to make it disableable or replaceable for 299 | // different environments (DOM-less JavaScript interpreters like Rhino or V8, 300 | // for example). See bootstrap/ for more information. 301 | 302 | 303 | /** 304 | * @define {boolean} Whether to enable the debug loader. 305 | * 306 | * If enabled, a call to goog.require() will attempt to load the namespace by 307 | * appending a script tag to the DOM (if the namespace has been registered). 308 | * 309 | * If disabled, goog.require() will simply assert that the namespace has been 310 | * provided (and depend on the fact that some outside tool correctly ordered 311 | * the script). 312 | */ 313 | goog.ENABLE_DEBUG_LOADER = true; 314 | 315 | 316 | /** 317 | * Implements a system for the dynamic resolution of dependencies 318 | * that works in parallel with the BUILD system. Note that all calls 319 | * to goog.require will be stripped by the JSCompiler when the 320 | * --closure_pass option is used. 321 | * @see goog.provide 322 | * @param {string} name Namespace to include (as was given in goog.provide()) 323 | * in the form "goog.package.part". 324 | */ 325 | goog.require = function(name) { 326 | 327 | // if the object already exists we do not need do do anything 328 | // TODO(arv): If we start to support require based on file name this has 329 | // to change 330 | // TODO(arv): If we allow goog.foo.* this has to change 331 | // TODO(arv): If we implement dynamic load after page load we should probably 332 | // not remove this code for the compiled output 333 | if (!COMPILED) { 334 | if (goog.isProvided_(name)) { 335 | return; 336 | } 337 | 338 | if (goog.ENABLE_DEBUG_LOADER) { 339 | var path = goog.getPathFromDeps_(name); 340 | if (path) { 341 | goog.included_[path] = true; 342 | goog.writeScripts_(); 343 | return; 344 | } 345 | } 346 | 347 | var errorMessage = 'goog.require could not find: ' + name; 348 | if (goog.global.console) { 349 | goog.global.console['error'](errorMessage); 350 | } 351 | 352 | 353 | throw Error(errorMessage); 354 | 355 | } 356 | }; 357 | 358 | 359 | /** 360 | * Path for included scripts 361 | * @type {string} 362 | */ 363 | goog.basePath = ''; 364 | 365 | 366 | /** 367 | * A hook for overriding the base path. 368 | * @type {string|undefined} 369 | */ 370 | goog.global.CLOSURE_BASE_PATH; 371 | 372 | 373 | /** 374 | * Whether to write out Closure's deps file. By default, 375 | * the deps are written. 376 | * @type {boolean|undefined} 377 | */ 378 | goog.global.CLOSURE_NO_DEPS; 379 | 380 | 381 | /** 382 | * A function to import a single script. This is meant to be overridden when 383 | * Closure is being run in non-HTML contexts, such as web workers. It's defined 384 | * in the global scope so that it can be set before base.js is loaded, which 385 | * allows deps.js to be imported properly. 386 | * 387 | * The function is passed the script source, which is a relative URI. It should 388 | * return true if the script was imported, false otherwise. 389 | */ 390 | goog.global.CLOSURE_IMPORT_SCRIPT; 391 | 392 | 393 | /** 394 | * Null function used for default values of callbacks, etc. 395 | * @return {void} Nothing. 396 | */ 397 | goog.nullFunction = function() {}; 398 | 399 | 400 | /** 401 | * The identity function. Returns its first argument. 402 | * 403 | * @param {*=} opt_returnValue The single value that will be returned. 404 | * @param {...*} var_args Optional trailing arguments. These are ignored. 405 | * @return {?} The first argument. We can't know the type -- just pass it along 406 | * without type. 407 | * @deprecated Use goog.functions.identity instead. 408 | */ 409 | goog.identityFunction = function(opt_returnValue, var_args) { 410 | return opt_returnValue; 411 | }; 412 | 413 | 414 | /** 415 | * When defining a class Foo with an abstract method bar(), you can do: 416 | * 417 | * Foo.prototype.bar = goog.abstractMethod 418 | * 419 | * Now if a subclass of Foo fails to override bar(), an error 420 | * will be thrown when bar() is invoked. 421 | * 422 | * Note: This does not take the name of the function to override as 423 | * an argument because that would make it more difficult to obfuscate 424 | * our JavaScript code. 425 | * 426 | * @type {!Function} 427 | * @throws {Error} when invoked to indicate the method should be 428 | * overridden. 429 | */ 430 | goog.abstractMethod = function() { 431 | throw Error('unimplemented abstract method'); 432 | }; 433 | 434 | 435 | /** 436 | * Adds a {@code getInstance} static method that always return the same instance 437 | * object. 438 | * @param {!Function} ctor The constructor for the class to add the static 439 | * method to. 440 | */ 441 | goog.addSingletonGetter = function(ctor) { 442 | ctor.getInstance = function() { 443 | if (ctor.instance_) { 444 | return ctor.instance_; 445 | } 446 | if (goog.DEBUG) { 447 | // NOTE: JSCompiler can't optimize away Array#push. 448 | goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor; 449 | } 450 | return ctor.instance_ = new ctor; 451 | }; 452 | }; 453 | 454 | 455 | /** 456 | * All singleton classes that have been instantiated, for testing. Don't read 457 | * it directly, use the {@code goog.testing.singleton} module. The compiler 458 | * removes this variable if unused. 459 | * @type {!Array.} 460 | * @private 461 | */ 462 | goog.instantiatedSingletons_ = []; 463 | 464 | 465 | if (!COMPILED && goog.ENABLE_DEBUG_LOADER) { 466 | /** 467 | * Object used to keep track of urls that have already been added. This 468 | * record allows the prevention of circular dependencies. 469 | * @type {Object} 470 | * @private 471 | */ 472 | goog.included_ = {}; 473 | 474 | 475 | /** 476 | * This object is used to keep track of dependencies and other data that is 477 | * used for loading scripts 478 | * @private 479 | * @type {Object} 480 | */ 481 | goog.dependencies_ = { 482 | pathToNames: {}, // 1 to many 483 | nameToPath: {}, // 1 to 1 484 | requires: {}, // 1 to many 485 | // used when resolving dependencies to prevent us from 486 | // visiting the file twice 487 | visited: {}, 488 | written: {} // used to keep track of script files we have written 489 | }; 490 | 491 | 492 | /** 493 | * Tries to detect whether is in the context of an HTML document. 494 | * @return {boolean} True if it looks like HTML document. 495 | * @private 496 | */ 497 | goog.inHtmlDocument_ = function() { 498 | var doc = goog.global.document; 499 | return typeof doc != 'undefined' && 500 | 'write' in doc; // XULDocument misses write. 501 | }; 502 | 503 | 504 | /** 505 | * Tries to detect the base path of the base.js script that bootstraps Closure 506 | * @private 507 | */ 508 | goog.findBasePath_ = function() { 509 | if (goog.global.CLOSURE_BASE_PATH) { 510 | goog.basePath = goog.global.CLOSURE_BASE_PATH; 511 | return; 512 | } else if (!goog.inHtmlDocument_()) { 513 | return; 514 | } 515 | var doc = goog.global.document; 516 | var scripts = doc.getElementsByTagName('script'); 517 | // Search backwards since the current script is in almost all cases the one 518 | // that has base.js. 519 | for (var i = scripts.length - 1; i >= 0; --i) { 520 | var src = scripts[i].src; 521 | var qmark = src.lastIndexOf('?'); 522 | var l = qmark == -1 ? src.length : qmark; 523 | if (src.substr(l - 7, 7) == 'base.js') { 524 | goog.basePath = src.substr(0, l - 7); 525 | return; 526 | } 527 | } 528 | }; 529 | 530 | 531 | /** 532 | * Imports a script if, and only if, that script hasn't already been imported. 533 | * (Must be called at execution time) 534 | * @param {string} src Script source. 535 | * @private 536 | */ 537 | goog.importScript_ = function(src) { 538 | var importScript = goog.global.CLOSURE_IMPORT_SCRIPT || 539 | goog.writeScriptTag_; 540 | if (!goog.dependencies_.written[src] && importScript(src)) { 541 | goog.dependencies_.written[src] = true; 542 | } 543 | }; 544 | 545 | 546 | /** 547 | * The default implementation of the import function. Writes a script tag to 548 | * import the script. 549 | * 550 | * @param {string} src The script source. 551 | * @return {boolean} True if the script was imported, false otherwise. 552 | * @private 553 | */ 554 | goog.writeScriptTag_ = function(src) { 555 | if (goog.inHtmlDocument_()) { 556 | var doc = goog.global.document; 557 | 558 | // If the user tries to require a new symbol after document load, 559 | // something has gone terribly wrong. Doing a document.write would 560 | // wipe out the page. 561 | if (doc.readyState == 'complete') { 562 | // Certain test frameworks load base.js multiple times, which tries 563 | // to write deps.js each time. If that happens, just fail silently. 564 | // These frameworks wipe the page between each load of base.js, so this 565 | // is OK. 566 | var isDeps = /\bdeps.js$/.test(src); 567 | if (isDeps) { 568 | return false; 569 | } else { 570 | throw Error('Cannot write "' + src + '" after document load'); 571 | } 572 | } 573 | 574 | doc.write( 575 | '