├── .gitignore ├── test ├── fontloader-test.js ├── assets │ ├── subset.ttf │ ├── subset.woff │ ├── sourcesanspro-regular.ttf │ └── sourcesanspro-regular.woff ├── helper.js ├── index.html ├── deps.js ├── fontface-test.js └── fontfaceset-test.js ├── src ├── binarydata.js ├── fontfacesource.js ├── fontfacesetloadstatus.js ├── fontfaceloadstatus.js ├── fontfacedescriptors.js ├── fontformat.js ├── fontfaceset.js └── fontface.js ├── polyfill.js ├── .travis.yml ├── package.json ├── vendor ├── bramstein │ └── promise.min.js ├── google │ └── base.js └── sunesimonsen │ └── unexpected.js ├── Gruntfile.js ├── browsers.json ├── README.md └── fontloader.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/fontloader-test.js: -------------------------------------------------------------------------------- 1 | describe('fontloader', function () { 2 | 3 | }); 4 | -------------------------------------------------------------------------------- /test/assets/subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontloader/HEAD/test/assets/subset.ttf -------------------------------------------------------------------------------- /test/assets/subset.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontloader/HEAD/test/assets/subset.woff -------------------------------------------------------------------------------- /test/assets/sourcesanspro-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontloader/HEAD/test/assets/sourcesanspro-regular.ttf -------------------------------------------------------------------------------- /test/assets/sourcesanspro-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramstein/fontloader/HEAD/test/assets/sourcesanspro-regular.woff -------------------------------------------------------------------------------- /src/binarydata.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.BinaryData'); 2 | 3 | /** 4 | * @typedef {ArrayBuffer|ArrayBufferView} 5 | */ 6 | fl.BinaryData; 7 | -------------------------------------------------------------------------------- /src/fontfacesource.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFaceSource'); 2 | 3 | /** 4 | * @typedef {{ 5 | * url: string, 6 | * format: string 7 | * }} 8 | */ 9 | fl.FontFaceSource; 10 | -------------------------------------------------------------------------------- /src/fontfacesetloadstatus.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFaceSetLoadStatus'); 2 | 3 | /** 4 | * @enum {string} 5 | */ 6 | fl.FontFaceSetLoadStatus = { 7 | LOADED: 'loaded', 8 | LOADING: 'loading' 9 | }; 10 | -------------------------------------------------------------------------------- /src/fontfaceloadstatus.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFaceLoadStatus'); 2 | 3 | /** 4 | * @enum {string} 5 | */ 6 | fl.FontFaceLoadStatus = { 7 | UNLOADED: "unloaded", 8 | LOADING: "loading", 9 | LOADED: "loaded", 10 | ERROR: "error" 11 | }; 12 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | window.loadStylesheet = function (href) { 2 | var link = document.createElement('link'); 3 | 4 | link.rel = 'stylesheet'; 5 | link.href = href; 6 | 7 | document.getElementsByTagName('head')[0].appendChild(link); 8 | 9 | return link; 10 | }; 11 | -------------------------------------------------------------------------------- /polyfill.js: -------------------------------------------------------------------------------- 1 | goog.provide('fontloader'); 2 | 3 | goog.require('fl.FontFace'); 4 | goog.require('fl.FontFaceSet'); 5 | 6 | if (!window['FontFace']) { 7 | window['FontFace'] = fl.FontFace; 8 | window['FontFace']['prototype']['load'] = fl.FontFace.prototype.load; 9 | 10 | document['fonts'] = new fl.FontFaceSet(); 11 | } 12 | -------------------------------------------------------------------------------- /src/fontfacedescriptors.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFaceDescriptors'); 2 | 3 | /** 4 | * @typedef {{ 5 | * style: string, 6 | * weight: string, 7 | * stretch: string, 8 | * display: string, 9 | * unicodeRange: string, 10 | * variant: string, 11 | * featureSettings: string 12 | * }} 13 | */ 14 | fl.FontFaceDescriptors; 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - npm install -g grunt-cli 3 | - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 4 | - tar -xjf phantomjs-2.0.0-ubuntu-12.04.tar.bz2 5 | - sudo mv /usr/local/phantomjs/bin/phantomjs /usr/local/phantomjs/bin/phantomjs1 6 | - sudo mv phantomjs /usr/local/phantomjs/bin/phantomjs 7 | language: node_js 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontloader", 3 | "version": "1.2.7", 4 | "description": "A polyfill for the FontLoader interface", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "repository": "https://github.com/bramstein/fontloader.git", 9 | "keywords": [ 10 | "fontloader", 11 | "fonts", 12 | "font", 13 | "font-face" 14 | ], 15 | "author": "Bram Stein (http://www.bramstein.com/)", 16 | "license": "Apache 2.0", 17 | "devDependencies": { 18 | "mocha": "=2.2.5", 19 | "sinon": "=1.14.1", 20 | "grunt": "=0.4.5", 21 | "grunt-contrib-clean": "=0.6.0", 22 | "grunt-contrib-jshint": "=0.11.2", 23 | "google-closure-compiler": "^20160911.0.0", 24 | "grunt-exec": "~0.4.6", 25 | "extend": "^2.0.1", 26 | "unexpected": "^7.5.0", 27 | "mocha-phantomjs-core": "^0.2.0", 28 | "grunt-contrib-concat": "^0.5.1" 29 | }, 30 | "scripts": { 31 | "test": "grunt test" 32 | }, 33 | "dependencies": { 34 | "promis": "=1.1.4", 35 | "closure-fetch": "=0.4.2", 36 | "fontfaceobserver": "=2.0.5", 37 | "closure-dom": "=0.2.6", 38 | "cssvalue": "=0.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FontLoader 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /vendor/bramstein/promise.min.js: -------------------------------------------------------------------------------- 1 | (function(){'use strict';function f(a){this.a=k;this.b=void 0;this.d=[];var b=this;try{a(function(a){l(b,a)},function(a){m(b,a)})}catch(d){m(b,d)}}var k=2;function n(a){return new f(function(b,d){d(a)})}function p(a){return new f(function(b){b(a)})} 2 | function l(a,b){if(a.a===k){if(b===a)throw new TypeError("Promise resolved with itself.");try{var d=b&&b.then,c=!1;if(null!==b&&"object"===typeof b&&"function"===typeof d){d.call(b,function(b){c||l(a,b);c=!0},function(b){c||m(a,b);c=!0});return}}catch(e){c||m(a,e);return}a.a=0;a.b=b;q(a)}}function m(a,b){if(a.a===k){if(b===a)throw new TypeError("Promise rejected with itself.");a.a=1;a.b=b;q(a)}} 3 | function q(a){setTimeout(function(){if(a.a!==k)for(;a.d.length;){var b=a.d.shift(),d=b[0],c=b[1],e=b[2],b=b[3];try{0===a.a?"function"===typeof d?e(d.call(void 0,a.b)):e(a.b):1===a.a&&("function"===typeof c?e(c.call(void 0,a.b)):b(a.b))}catch(g){b(g)}}},0)}f.prototype.e=function(a){return this.c(void 0,a)};f.prototype.c=function(a,b){var d=this;return new f(function(c,e){d.d.push([a,b,c,e]);q(d)})}; 4 | function r(a){return new f(function(b,d){function c(c){return function(d){g[c]=d;e+=1;e===a.length&&b(g)}}var e=0,g=[];0===a.length&&b(g);for(var h=0;h test/deps.js' 29 | }, 30 | jshint: { 31 | all: ['src/**/*.js'], 32 | options: { 33 | // ... better written as dot notation 34 | "-W069": true, 35 | 36 | // type definitions 37 | "-W030": true, 38 | 39 | // Don't make functions within loops 40 | "-W083": true, 41 | 42 | // Wrap the /regexp/ literal in parens to disambiguate the slash operator 43 | "-W092": true 44 | } 45 | }, 46 | 'closure-compiler': { 47 | compile: { 48 | files: { 49 | "build/fontloader.js": src 50 | }, 51 | options: extend({}, compilerOptions) 52 | }, 53 | debug: { 54 | files: { 55 | "build/fontloader.debug.js": src 56 | }, 57 | options: extend({}, compilerOptions, { 58 | debug: true, 59 | formatting: ['PRETTY_PRINT', 'PRINT_INPUT_DELIMITER'] 60 | }) 61 | } 62 | }, 63 | concat: { 64 | dist: { 65 | src: ['node_modules/promis/promise.js', 'build/fontloader.js'], 66 | dest: 'fontloader.js' 67 | } 68 | } 69 | }); 70 | 71 | grunt.loadNpmTasks('grunt-contrib-clean'); 72 | grunt.loadNpmTasks('grunt-contrib-jshint'); 73 | grunt.loadNpmTasks('grunt-exec'); 74 | grunt.loadNpmTasks('grunt-contrib-concat'); 75 | 76 | grunt.registerTask('compile', ['closure-compiler:compile']); 77 | grunt.registerTask('debug', ['closure-compiler:debug']); 78 | grunt.registerTask('default', ['compile']); 79 | grunt.registerTask('test', ['exec:test']); 80 | grunt.registerTask('deps', ['exec:deps']); 81 | grunt.registerTask('dist', ['closure-compiler:compile', 'concat:dist']); 82 | }; 83 | -------------------------------------------------------------------------------- /test/deps.js: -------------------------------------------------------------------------------- 1 | // This file was autogenerated by calcdeps.js 2 | goog.addDependency("../../polyfill.js", ["fontloader"], ["fl.FontFace","fl.FontFaceSet"]); 3 | goog.addDependency("../../src/binarydata.js", ["fl.BinaryData"], []); 4 | goog.addDependency("../../src/fontface.js", ["fl.FontFace"], ["fl.FontFaceLoadStatus","fl.FontFormat","cssvalue.Src","lang.Promise","net.fetch","dom"]); 5 | goog.addDependency("../../src/fontfacedescriptors.js", ["fl.FontFaceDescriptors"], []); 6 | goog.addDependency("../../src/fontfaceloadstatus.js", ["fl.FontFaceLoadStatus"], []); 7 | goog.addDependency("../../src/fontfaceset.js", ["fl.FontFaceSet"], ["fl.FontFaceSetLoadStatus","cssvalue.Font","lang.Promise"]); 8 | goog.addDependency("../../src/fontfacesetloadstatus.js", ["fl.FontFaceSetLoadStatus"], []); 9 | goog.addDependency("../../src/fontfacesource.js", ["fl.FontFaceSource"], []); 10 | goog.addDependency("../../src/fontformat.js", ["fl.FontFormat"], ["fontface.Observer"]); 11 | goog.addDependency("../../node_modules/cssvalue/src/featuresettings.js", ["cssvalue.FeatureSettings"], []); 12 | goog.addDependency("../../node_modules/cssvalue/src/font.js", ["cssvalue.Font"], []); 13 | goog.addDependency("../../node_modules/cssvalue/src/src.js", ["cssvalue.Src"], []); 14 | goog.addDependency("../../node_modules/cssvalue/src/unicoderange.js", ["cssvalue.UnicodeRange"], []); 15 | goog.addDependency("../../node_modules/promis/src/async.js", ["lang.async"], []); 16 | goog.addDependency("../../node_modules/promis/src/promise.js", ["lang.Promise"], ["lang.async"]); 17 | goog.addDependency("../../node_modules/closure-fetch/src/body.js", ["net.Body"], []); 18 | goog.addDependency("../../node_modules/closure-fetch/src/bodyinit.js", ["net.BodyInit"], []); 19 | goog.addDependency("../../node_modules/closure-fetch/src/fetch.js", ["net.fetch"], ["net.Body","net.Response","lang.Promise"]); 20 | goog.addDependency("../../node_modules/closure-fetch/src/headersinit.js", ["net.HeadersInit"], []); 21 | goog.addDependency("../../node_modules/closure-fetch/src/requestinit.js", ["net.RequestInit"], []); 22 | goog.addDependency("../../node_modules/closure-fetch/src/response.js", ["net.Response"], ["lang.Promise"]); 23 | goog.addDependency("../../node_modules/closure-fetch/src/responseinit.js", ["net.ResponseInit"], []); 24 | goog.addDependency("../../node_modules/closure-dom/src/dom.js", ["dom"], []); 25 | goog.addDependency("../../node_modules/fontfaceobserver/src/css.js", ["fontface.Css"], []); 26 | goog.addDependency("../../node_modules/fontfaceobserver/src/descriptors.js", ["fontface.Descriptors"], []); 27 | goog.addDependency("../../node_modules/fontfaceobserver/src/observer.js", ["fontface.Observer"], ["fontface.Ruler","dom","lang.Promise"]); 28 | goog.addDependency("../../node_modules/fontfaceobserver/src/ruler.js", ["fontface.Ruler"], ["dom"]); 29 | -------------------------------------------------------------------------------- /browsers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "os_version": "XP", 4 | "os": "Windows", 5 | "browser_version": "11.1", 6 | "browser": "opera" 7 | }, 8 | { 9 | "os_version": "XP", 10 | "os": "Windows", 11 | "browser_version": "3.6", 12 | "browser": "firefox" 13 | }, 14 | { 15 | "os_version": "XP", 16 | "os": "Windows", 17 | "browser_version": "20.0", 18 | "browser": "firefox" 19 | }, 20 | { 21 | "os_version": "XP", 22 | "os": "Windows", 23 | "browser_version": "6.0", 24 | "browser": "ie" 25 | }, 26 | { 27 | "os_version": "XP", 28 | "os": "Windows", 29 | "browser_version": "7.0", 30 | "browser": "ie" 31 | }, 32 | { 33 | "os_version": "XP", 34 | "os": "Windows", 35 | "browser_version": "8.0", 36 | "browser": "ie" 37 | }, 38 | { 39 | "os_version": "8", 40 | "os": "Windows", 41 | "browser_version": "10.0 Desktop", 42 | "browser": "ie" 43 | }, 44 | { 45 | "os_version": "7", 46 | "os": "Windows", 47 | "browser_version": "9.0", 48 | "browser": "ie" 49 | }, 50 | { 51 | "os_version": "Mountain Lion", 52 | "os": "OS X", 53 | "browser_version": "6.0", 54 | "browser": "safari" 55 | }, 56 | { 57 | "os_version": "Mountain Lion", 58 | "os": "OS X", 59 | "browser_version": "25.0", 60 | "browser": "chrome" 61 | }, 62 | { 63 | "os_version": "Snow Leopard", 64 | "os": "OS X", 65 | "browser_version": "5.0", 66 | "browser": "safari" 67 | }, 68 | { 69 | "os_version": "5.0", 70 | "device": "iPad 2 (5.0)", 71 | "os": "ios", 72 | "browser": "Mobile Safari" 73 | }, 74 | { 75 | "os_version": "5.1", 76 | "device": "iPad 3rd", 77 | "os": "ios", 78 | "browser": "Mobile Safari" 79 | }, 80 | { 81 | "os_version": "6.0", 82 | "device": "iPhone 5", 83 | "os": "ios", 84 | "browser": "Mobile Safari" 85 | }, 86 | { 87 | "os_version": "4.3.2", 88 | "device": "iPad 2", 89 | "os": "ios", 90 | "browser": "Mobile Safari" 91 | }, 92 | { 93 | "os_version": "2.3", 94 | "device": "Samsung Galaxy S II", 95 | "os": "android", 96 | "browser": "Android Browser" 97 | }, 98 | { 99 | "os_version": "2.2", 100 | "device": "Samsung Galaxy S", 101 | "os": "android", 102 | "browser": "Android Browser" 103 | }, 104 | { 105 | "os_version": "4.2", 106 | "device": "LG Nexus 4", 107 | "os": "android", 108 | "browser": "Android Browser" 109 | }, 110 | { 111 | "os_version": "4.0", 112 | "device": "Samsung Galaxy Nexus", 113 | "os": "android", 114 | "browser": "Android Browser" 115 | }, 116 | { 117 | "os_version": "4.1", 118 | "device": "Samsung Galaxy S III", 119 | "os": "android", 120 | "browser": "Android Browser" 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /src/fontformat.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFormat'); 2 | 3 | goog.require('fontface.Observer'); 4 | 5 | goog.scope(function () { 6 | fl.FontFormat = {}; 7 | 8 | var FontFormat = fl.FontFormat; 9 | 10 | /** 11 | * Width is 3072 (3em). 12 | * 13 | * @const 14 | * @type {string} 15 | */ 16 | FontFormat.WOFF2 = 'd09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAciuMTaGVo8IaqBbcKPeB3CyAAIO4unr9nb72QE3p00iGQQIZcAAcAMEJOztBx7zdWVWn//BAPW1l0BN429cPrCPE75MA637gPs0DjavNxzHtWeXXErKIV3AF9TbHqCTOATL2BgjeIH30lQwSAonU1LabV8Iz12wDvgd/obV5QVxXDKvUhW1QfWNrS6HzEQJaP4tBA=='; 17 | 18 | /** 19 | * Width is 2048 (2em). 20 | * 21 | * @const 22 | * @type {string} 23 | */ 24 | FontFormat.WOFF = 'd09GRgABAAAAAAHgAAoAAAAAAggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABUAAAABcAAABOBIQEIWNtYXAAAAFwAAAAJgAAACwADABzZ2x5ZgAAAaAAAAAUAAAAFAwBPQJoZWFkAAAA9AAAAC0AAAA2CHEB92hoZWEAAAEkAAAAFgAAACQMAQgDaG10eAAAAWgAAAAIAAAACAgAAABsb2NhAAABmAAAAAYAAAAGAAoAAG1heHAAAAE8AAAAEwAAACAABAACbmFtZQAAAbQAAAAeAAAAIAAjCF5wb3N0AAAB1AAAAAwAAAAgAAMAAHgBY2BkYABhb81vuvH8Nl8ZmFgYQOBCWvVrMP3VURxEczBAxBmYQAQAAFIIBgAAAHgBY2BkYGBhAAEOKAkUQQVMAAJKABkAAHgBY2BkYGBgAkIgjQ0AAAC+AAcAeAFjAIEUBkYGcoECgwILmAEiASBRAK4AAAAAAAgAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoQgvsP//xDy/0EwnwEATX4GfAAAAAAAAAAKAAAAAQAAAAAIAAQAAAEAADEBCAAEAHgBY2BgYGKQY2BmYGThZGAEshmgbCYw2wEABjMAigAAeAFjYGbACwAAfQAE'; 25 | 26 | /** 27 | * @type {string} 28 | */ 29 | FontFormat.TEST_FONT_FAMILY = '_fff_'; 30 | 31 | /** 32 | * @type {Promise.>} 33 | */ 34 | FontFormat.SUPPORTED_FORMATS = null; 35 | 36 | /** 37 | * @return {Promise.>} 38 | */ 39 | FontFormat.detect = function () { 40 | if (!FontFormat.SUPPORTED_FORMATS) { 41 | if (/MSIE|Trident/.test(navigator.userAgent)) { 42 | return Promise.resolve(['woff', 'opentype', 'truetype']); 43 | } 44 | 45 | var style = document.createElement('style'), 46 | head = document.getElementsByTagName('head')[0]; 47 | 48 | style.appendChild(document.createTextNode( 49 | '@font-face{' + 50 | 'font-family:"' + FontFormat.TEST_FONT_FAMILY + '";' + 51 | 'src:' + 52 | 'url(data:font/woff2;base64,' + FontFormat.WOFF2 +') format("woff2"),' + 53 | 'url(data:application/font-woff;base64,' + FontFormat.WOFF + ') format("woff")' + 54 | '}' 55 | )); 56 | 57 | head.appendChild(style); 58 | 59 | // TODO: Since we have the font data hardcoded in the JS we can 60 | // use the forced-relayout trick here, which removes the need 61 | // to insert test spans into the document. 62 | FontFormat.SUPPORTED_FORMATS = new fontface.Observer(FontFormat.TEST_FONT_FAMILY, {}).load('@', 5000).then(function () { 63 | var ruler = new fontface.Ruler('@'), 64 | formats = ['opentype', 'truetype']; 65 | 66 | ruler.setFont(FontFormat.TEST_FONT_FAMILY); 67 | 68 | document.body.appendChild(ruler.getElement()); 69 | 70 | var width = ruler.getWidth(); 71 | 72 | if (width >= 200) { 73 | formats.unshift('woff'); 74 | } 75 | 76 | if (width == 300) { 77 | formats.unshift('woff2'); 78 | } 79 | 80 | head.removeChild(style); 81 | document.body.removeChild(ruler.getElement()); 82 | 83 | return formats; 84 | }, function () { 85 | return ['opentype', 'truetype']; 86 | }); 87 | } 88 | return FontFormat.SUPPORTED_FORMATS; 89 | }; 90 | }); 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | This library is deprecated and no longer maintained. The CSS font loading API is near impossible to implement without access to the browser's internal font loading behaviour. The most useful aspects of the API (font events) has been implemented in [Font Face Observer](https://github.com/bramstein/fontfaceobserver), so please use that instead. 4 | 5 | ## FontLoader Polyfill [![Build Status](https://travis-ci.org/bramstein/fontloader.png?branch=master)](https://travis-ci.org/bramstein/fontloader) 6 | 7 | This polyfill implements the [W3C Font Load Events Module Level 3 specification](http://www.w3.org/TR/css-font-loading/). It detects when fonts have loaded and provides callbacks for each font load event. You can use the fontloader polyfill to prevent the Flash Of Unstyled Text (FOUT) or execute JavaScript code when fonts have loaded (for example to perform layout or show a user interface element.) 8 | 9 | ## API 10 | 11 | 12 | ## Installation 13 | 14 | If you using npm: 15 | 16 | $ npm install fontloader 17 | 18 | Otherwise copy the [fontloader.js](fontloader.js) file to your project and include it. 19 | 20 | ## Limitations 21 | 22 | The following are limitations due to the fact that this is a JavaScript library without access to a browser's internal state. 23 | 24 | #### Metric compatible fonts 25 | 26 | Metric compatible fonts are fonts that are designed to have identical metrics to another font so they can be used as a substitute without affecting page layout. When a web font is metric compatible with one of the system fonts (the fonts that are used for `serif`, `sans-serif` and `monospace`) the fontloader can not detect when the web font has loaded. 27 | 28 | #### Slow loading fonts 29 | 30 | The library has a default timeout of 3 seconds after which it considers a font "load" failed. Unlike the native API this library is not capable of cancelling in-progress font loads so it may happen that the font still loads after the timeout. This is a rare case and usually indicative of a problem with either the font or the host it is loaded from. 31 | 32 | ## Browser Support 33 | 34 | The following browsers are supported: 35 | 36 | * IE9+ 37 | * Chrome 38 | * Firefox 39 | * Safari 40 | * Android 41 | * iOS 42 | * Opera 43 | 44 | Other browser may work, but are not extensively tested. Please [open an issue](https://github.com/bramstein/fontloader/issues) if you think a browser should be supported but isn't. Tests are run automatically on all supported browsers using [BrowserStack](http://www.browserstack.com) and [browserstack-test](https://github.com/bramstein/browserstack-test). 45 | 46 | ## Copyright and License 47 | 48 | Portions of this project are derived from the [Web Font Loader](https://github.com/typekit/webfontloader): 49 | 50 | Web Font Loader Copyright (c) 2010 Adobe Systems Incorporated, Google Incorporated. 51 | 52 | The FontLoader polyfill is therefore also licenced under the Apache 2.0 License: 53 | 54 | FontLoader Polyfill Copyright (c) 2013-2014 Bram Stein 55 | 56 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 57 | 58 | http://www.apache.org/licenses/LICENSE-2.0 59 | 60 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 61 | -------------------------------------------------------------------------------- /src/fontfaceset.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFaceSet'); 2 | 3 | goog.require('fl.FontFaceSetLoadStatus'); 4 | 5 | goog.require('cssvalue.Font'); 6 | 7 | goog.scope(function () { 8 | var FontFaceSetLoadStatus = fl.FontFaceSetLoadStatus, 9 | Font = cssvalue.Font; 10 | 11 | /** 12 | * @constructor 13 | */ 14 | fl.FontFaceSet = function () { 15 | 16 | /** 17 | * @type {Array.} 18 | */ 19 | this.fonts = []; 20 | 21 | /** 22 | * @type {!fl.FontFaceSetLoadStatus} 23 | */ 24 | this.loadStatus = FontFaceSetLoadStatus.LOADED; 25 | 26 | Object.defineProperties(this, { 27 | 'status': { 28 | get: function () { 29 | return this.loadStatus; 30 | } 31 | }, 32 | 'size': { 33 | get: function () { 34 | return this.fonts.length; 35 | } 36 | } 37 | }); 38 | }; 39 | 40 | var FontFaceSet = fl.FontFaceSet; 41 | 42 | /** 43 | * @param {!fl.FontFace} font 44 | * 45 | * return {fl.FontFaceSet} 46 | */ 47 | FontFaceSet.prototype['add'] = function (font) { 48 | if (!this['has'](font)) { 49 | font.insert(); 50 | this.fonts.push(font); 51 | } 52 | }; 53 | 54 | /** 55 | * @param {!fl.FontFace} font 56 | * 57 | * @return {boolean} 58 | */ 59 | FontFaceSet.prototype['delete'] = function (font) { 60 | var index = this.fonts.indexOf(font); 61 | 62 | if (index !== -1) { 63 | font.remove(); 64 | this.fonts.splice(index, 1); 65 | return true; 66 | } else { 67 | return false; 68 | } 69 | }; 70 | 71 | FontFaceSet.prototype['clear'] = function () { 72 | this.fonts = []; 73 | }; 74 | 75 | /** 76 | * @param {!fl.FontFace} font 77 | * 78 | * @return {boolean} 79 | */ 80 | FontFaceSet.prototype['has'] = function (font) { 81 | return this.fonts.indexOf(font) !== -1; 82 | }; 83 | 84 | /** 85 | * @param {function(fl.FontFace, number, fl.FontFaceSet)} fn 86 | */ 87 | FontFaceSet.prototype['forEach'] = function (fn) { 88 | var set = this; 89 | 90 | this.fonts.forEach(function (font, index) { 91 | fn(font, index, set); 92 | }); 93 | }; 94 | 95 | /** 96 | * @param {string} font 97 | * @param {string=} opt_text 98 | * 99 | * @return {!Array.|null} 100 | */ 101 | FontFaceSet.prototype.match = function (font, opt_text) { 102 | function normalize(weight) { 103 | if (weight === 'bold') { 104 | return 700; 105 | } else if (weight === 'normal') { 106 | return 400; 107 | } else { 108 | return weight; 109 | } 110 | } 111 | 112 | var properties = Font.parse(font); 113 | 114 | if (properties === null) { 115 | return null; 116 | } 117 | 118 | // TODO: match on opt_text 119 | return this.fonts.filter(function (f) { 120 | var families = properties.family; 121 | 122 | for (var i = 0; i < families.length; i++) { 123 | if (f['family'] === families[i] && 124 | f['style'] === properties.style && 125 | f['stretch'] === properties.stretch && 126 | normalize(f['weight']) === normalize(properties.weight)) { 127 | return true; 128 | } 129 | } 130 | return false; 131 | }); 132 | }; 133 | 134 | /** 135 | * @param {string} font 136 | * @param {string=} opt_text 137 | * 138 | * @return {!Promise.>} 139 | */ 140 | FontFaceSet.prototype['load'] = function (font, opt_text) { 141 | var set = this, 142 | matches = this.match(font, opt_text); 143 | 144 | if (matches === null) { 145 | return Promise.reject([]); 146 | } else if (matches.length) { 147 | set.loadStatus = FontFaceSetLoadStatus.LOADING; 148 | 149 | return Promise.all(matches.map(function (font) { 150 | return font.load(); 151 | })).then(function () { 152 | set.loadStatus = FontFaceSetLoadStatus.LOADED; 153 | return matches; 154 | }).catch(function () { 155 | set.loadStatus = FontFaceSetLoadStatus.LOADED; 156 | return matches; 157 | }); 158 | } else { 159 | return Promise.resolve([]); 160 | } 161 | }; 162 | 163 | /** 164 | * @param {string} font 165 | * @param {string} opt_text 166 | * 167 | * @return {boolean} 168 | */ 169 | FontFaceSet.prototype['check'] = function (font, opt_text) { 170 | var matches = this.match(font, opt_text); 171 | 172 | if (matches.length === 0) { 173 | return false; 174 | } else { 175 | for (var i = 0; i < matches.length; i++) { 176 | if (matches[i]['status'] !== "loaded") { 177 | return false; 178 | } 179 | } 180 | return true; 181 | } 182 | }; 183 | }); 184 | -------------------------------------------------------------------------------- /test/fontface-test.js: -------------------------------------------------------------------------------- 1 | describe('FontFace', function () { 2 | var FontFace = fl.FontFace; 3 | 4 | describe('#constructor', function () { 5 | it('accepts family names starting with non-valid identifier characters', function () { 6 | expect(new FontFace('3four', 'url(font.woff)').family, 'to equal', '3four'); 7 | expect(new FontFace('-5f', 'url(font.woff)').family, 'to equal', '-5f'); 8 | expect(new FontFace('--vendor', 'url(font.woff)').family, 'to equal', '--vendor'); 9 | }); 10 | 11 | it('parses descriptors', function () { 12 | expect(new FontFace('My Family', 'url(font.woff)', { style: 'italic' }).style, 'to equal', 'italic'); 13 | expect(new FontFace('My Family', 'url(font.woff)', { weight: 'bold' }).weight, 'to equal', 'bold'); 14 | expect(new FontFace('My Family', 'url(font.woff)', { stretch: 'condensed' }).stretch, 'to equal', 'condensed'); 15 | expect(new FontFace('My Family', 'url(font.woff)', { variant: 'small-caps' }).variant, 'to equal', 'small-caps'); 16 | }); 17 | 18 | it('defaults descriptors if not given', function () { 19 | var font = new FontFace('My Family', 'url(font.woff)'); 20 | 21 | expect(font.style, 'to equal', 'normal'); 22 | expect(font.weight, 'to equal', 'normal'); 23 | expect(font.variant, 'to equal', 'normal'); 24 | }); 25 | }); 26 | 27 | describe('#getMatchingUrls', function () { 28 | it('returns the first URL if there are no format identifiers', function () { 29 | var font = new FontFace('My Family', 'url(font.woff), url(font.otf)'); 30 | 31 | expect(font.getMatchingUrls(['woff', 'opentype']), 'to equal', 'font.woff'); 32 | }); 33 | 34 | it('returns the first matching format URL', function () { 35 | var font = new FontFace('My Family', 'url(font.woff) format("woff"), url(font.otf) format("opentype")'); 36 | 37 | expect(font.getMatchingUrls(['woff']), 'to equal', 'font.woff'); 38 | expect(font.getMatchingUrls(['opentype']), 'to equal', 'font.otf'); 39 | }); 40 | 41 | it('returns null when the browser does not support anything', function () { 42 | var font = new FontFace('My Family', 'url(font.woff) format("woff"), url(font.otf) format("opentype")'); 43 | 44 | expect(font.getMatchingUrls([]), 'to be null'); 45 | }); 46 | }); 47 | 48 | describe('#load', function () { 49 | it('resolves when a font loads', function (done) { 50 | this.timeout(6000); 51 | var font = new FontFace('My Family', 'url(./assets/sourcesanspro-regular.woff) format("woff"), url(./assets/sourcesanspro-regular.ttf) format("truetype")'); 52 | 53 | expect(font.status, 'to equal', 'unloaded'); 54 | 55 | font.load().then(function (f) { 56 | expect(font, 'to equal', f); 57 | expect(font.status, 'to equal', 'loaded'); 58 | done(); 59 | }).catch(function () { 60 | done(new Error('Should not fail')); 61 | }); 62 | }); 63 | 64 | it('rejects when a font fails to load', function (done) { 65 | var font = new FontFace('My Family', 'url(./assets/unknown.woff) format("woff"), url(./assets/unknown.ttf) format("truetype")'); 66 | 67 | expect(font.status, 'to equal', 'unloaded'); 68 | 69 | font.load().then(function (f) { 70 | done(new Error('Should not succeed')); 71 | }).catch(function (f) { 72 | expect(font, 'to equal', f); 73 | expect(font.status, 'to equal', 'error'); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('loads immediately when given an arraybuffer', function (done) { 79 | var font = new FontFace('My Family', new ArrayBuffer(4), {}); 80 | 81 | expect(font.status, 'to equal', "loaded"); 82 | 83 | font.load().then(function (f) { 84 | expect(font.status, 'to equal', "loaded"); 85 | expect(f, 'to equal', font); 86 | done(); 87 | }, function (r) { 88 | done(r); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('#insert', function () { 94 | it('inserts a @font-face rule into the DOM', function () { 95 | var font = new FontFace('My Family', new ArrayBuffer(4), {}); 96 | 97 | font.insert(); 98 | 99 | expect(font.rule, 'not to be null'); 100 | expect(font.rule.parentStyleSheet, 'not to be null'); 101 | }); 102 | 103 | it('inserts multiple @font-face rules into the DOM', function () { 104 | var font1 = new FontFace('Font1', new ArrayBuffer(4), {}), 105 | font2 = new FontFace('Font2', new ArrayBuffer(4), {}); 106 | 107 | font1.insert(); 108 | font2.insert(); 109 | 110 | expect(font1.rule, 'not to be null'); 111 | expect(font1.rule.parentStyleSheet, 'not to be null'); 112 | expect(font2.rule, 'not to be null'); 113 | expect(font2.rule.parentStyleSheet, 'not to be null'); 114 | 115 | expect(font1.rule.parentStyleSheet, 'to equal', font2.rule.parentStyleSheet); 116 | }); 117 | }); 118 | 119 | describe('#remove', function () { 120 | it('removes @ font-face rule from the DOM', function () { 121 | var font = new FontFace('My Family', new ArrayBuffer(4), {}); 122 | 123 | font.insert(); 124 | 125 | expect(font.rule, 'not to be null'); 126 | expect(font.rule.parentStyleSheet, 'not to be null'); 127 | 128 | font.remove(); 129 | expect(font.rule, 'to be null'); 130 | }); 131 | 132 | it('removes multiple @font-face rules from the DOM', function () { 133 | var font1 = new FontFace('Font1', new ArrayBuffer(4), {}), 134 | font2 = new FontFace('Font2', new ArrayBuffer(4), {}); 135 | 136 | font1.insert(); 137 | font2.insert(); 138 | 139 | expect(font1.rule, 'not to be null'); 140 | expect(font1.rule.parentStyleSheet, 'not to be null'); 141 | expect(font2.rule, 'not to be null'); 142 | expect(font2.rule.parentStyleSheet, 'not to be null'); 143 | 144 | expect(font1.rule.parentStyleSheet, 'to equal', font2.rule.parentStyleSheet); 145 | 146 | font1.remove(); 147 | expect(font1.rule, 'to be null'); 148 | expect(font2.rule, 'not to be null'); 149 | expect(font2.rule.parentStyleSheet, 'not to be null'); 150 | 151 | font2.remove(); 152 | 153 | expect(font2.rule, 'to be null'); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/fontface.js: -------------------------------------------------------------------------------- 1 | goog.provide('fl.FontFace'); 2 | 3 | goog.require('fl.FontFaceLoadStatus'); 4 | goog.require('fl.FontFormat'); 5 | 6 | goog.require('cssvalue.Src'); 7 | goog.require('net.fetch'); 8 | goog.require('dom'); 9 | 10 | goog.scope(function () { 11 | var FontFaceLoadStatus = fl.FontFaceLoadStatus, 12 | FontFormat = fl.FontFormat, 13 | Src = cssvalue.Src, 14 | fetch = net.fetch; 15 | 16 | /** 17 | * @constructor 18 | * 19 | * @param {string} family 20 | * @param {fl.BinaryData|string} source 21 | * @param {fl.FontFaceDescriptors=} opt_descriptors 22 | */ 23 | fl.FontFace = function (family, source, opt_descriptors) { 24 | var fontface = this, 25 | descriptors = opt_descriptors || {}; 26 | 27 | /** 28 | * @type {fl.BinaryData|string} 29 | */ 30 | this.source = source; 31 | 32 | /** 33 | * @type {ArrayBuffer} 34 | */ 35 | this.buffer = null; 36 | 37 | /** 38 | * @type {Array.} 39 | */ 40 | this.urls = []; 41 | 42 | /** 43 | * @type {function(fl.FontFace)} 44 | */ 45 | this.resolveLoad; 46 | 47 | /** 48 | * @type {function(fl.FontFace)} 49 | */ 50 | this.rejectLoad; 51 | 52 | /** 53 | * @type {Promise.} 54 | */ 55 | this.promise = new Promise(function (resolve, reject) { 56 | fontface.resolveLoad = resolve; 57 | fontface.rejectLoad = reject; 58 | }); 59 | 60 | /** 61 | * @type {fl.FontFaceLoadStatus} 62 | */ 63 | this.loadStatus = FontFaceLoadStatus.UNLOADED; 64 | 65 | /** 66 | * @type {CSSRule|null} 67 | */ 68 | this.rule = null; 69 | 70 | Object.defineProperties(this, { 71 | 'family': { 72 | get: function () { 73 | return family; 74 | } 75 | }, 76 | 'style': { 77 | get: function () { 78 | return descriptors.style || 'normal'; 79 | } 80 | }, 81 | 'weight': { 82 | get: function () { 83 | return descriptors.weight || 'normal'; 84 | } 85 | }, 86 | 'stretch': { 87 | get: function () { 88 | return descriptors.stretch || 'normal'; 89 | } 90 | }, 91 | 'display': { 92 | get: function () { 93 | return descriptors.display || 'auto'; 94 | } 95 | }, 96 | 'unicodeRange': { 97 | get: function () { 98 | return descriptors.unicodeRange || 'U+0-10FFFF'; 99 | } 100 | }, 101 | 'variant': { 102 | get: function () { 103 | return descriptors.variant || 'normal'; 104 | } 105 | }, 106 | 'featureSettings': { 107 | get: function () { 108 | return descriptors.featureSettings || 'normal'; 109 | } 110 | }, 111 | 'status': { 112 | get: function () { 113 | return this.loadStatus; 114 | } 115 | }, 116 | 'loaded': { 117 | get: function () { 118 | return this.promise; 119 | } 120 | } 121 | }); 122 | 123 | if (typeof source === 'string') { 124 | this.urls = Src.parse(/** @type {string} */ (source)); 125 | } else { 126 | this.buffer = /** @type {ArrayBuffer} */ (source); 127 | this.loadStatus = FontFaceLoadStatus.LOADED; 128 | this.resolveLoad(fontface); 129 | } 130 | }; 131 | 132 | var FontFace = fl.FontFace; 133 | 134 | /** 135 | * @type {Element|null} 136 | */ 137 | FontFace.STYLE_ELEMENT = null; 138 | 139 | /** 140 | * Inserts the FontFace in the document. 141 | */ 142 | FontFace.prototype.insert = function () { 143 | if (!FontFace.STYLE_ELEMENT) { 144 | FontFace.STYLE_ELEMENT = dom.createElement('style'); 145 | dom.append(document.head, FontFace.STYLE_ELEMENT); 146 | } 147 | 148 | var src = null; 149 | 150 | if (this.loadStatus === FontFaceLoadStatus.LOADED) { 151 | var bytes = new Uint8Array(this.buffer), 152 | tmp = ''; 153 | 154 | for (var i = 0; i < bytes.length; i++) { 155 | tmp += String.fromCharCode(bytes[i]); 156 | } 157 | // TODO: Set the correct font format. 158 | src = 'url(data:font/opentype;base64,' + btoa(tmp) + ')'; 159 | } else { 160 | src = this.source; 161 | } 162 | 163 | // This doesn't use font-stretch, font-variant or font-feature-settings 164 | // because support is hardly there and horribly broken. 165 | var css = '@font-face{' + 166 | 'font-family:"' + this['family'] + '";' + 167 | 'font-style:' + this['style'] + ';' + 168 | 'font-weight:' + this['weight'] + ';' + 169 | 'font-display:' + this['display'] + ';' + 170 | // TODO: unicode-range is fairly buggy in Chrome when it is used in combination 171 | // with characters that also have OpenType features. Disable it for now because 172 | // it's causing those characters not to render. 173 | // 'unicode-range:' + this['unicodeRange'] + ';' + 174 | 'src:' + src + ';' + 175 | '}'; 176 | 177 | FontFace.STYLE_ELEMENT.sheet.insertRule(css, 0); 178 | 179 | this.rule = FontFace.STYLE_ELEMENT.sheet.cssRules[0]; 180 | }; 181 | 182 | /** 183 | * Remove the FontFace from the document. 184 | */ 185 | FontFace.prototype.remove = function () { 186 | if (FontFace.STYLE_ELEMENT && this.rule) { 187 | for (var i = 0; i < FontFace.STYLE_ELEMENT.sheet.cssRules.length; i++) { 188 | if (this.rule === FontFace.STYLE_ELEMENT.sheet.cssRules[i]) { 189 | FontFace.STYLE_ELEMENT.sheet.deleteRule(i); 190 | this.rule = null; 191 | break; 192 | } 193 | } 194 | } 195 | }; 196 | 197 | /** 198 | * @private 199 | * 200 | * @param {Array.} formats 201 | * 202 | * @return {string|null} 203 | */ 204 | FontFace.prototype.getMatchingUrls = function (formats) { 205 | var url = null; 206 | 207 | // find matching format in urls 208 | for (var i = 0; i < formats.length; i++) { 209 | for (var j = 0; j < this.urls.length; j++) { 210 | if (formats[i] === this.urls[j].format && url === null) { 211 | url = this.urls[j].url; 212 | break; 213 | } 214 | } 215 | } 216 | 217 | // If there is no format but the browser supports at least 218 | // one format, just load the first one. 219 | if (!url && formats.length !== 0) { 220 | url = this.urls[0].url; 221 | } 222 | 223 | return url; 224 | }; 225 | 226 | /** 227 | * @return {Promise.} 228 | */ 229 | FontFace.prototype.load = function () { 230 | var fontface = this; 231 | 232 | if (fontface.loadStatus === FontFaceLoadStatus.UNLOADED) { 233 | fontface.loadStatus = FontFaceLoadStatus.LOADING; 234 | 235 | FontFormat.detect().then(function (formats) { 236 | var url = fontface.getMatchingUrls(formats); 237 | 238 | if (url) { 239 | fetch(url).then(function (response) { 240 | if (response.ok) { 241 | return response.arrayBuffer(); 242 | } else { 243 | throw response; 244 | } 245 | }).then(function (buffer) { 246 | fontface.buffer = buffer; 247 | fontface.loadStatus = FontFaceLoadStatus.LOADED; 248 | fontface.resolveLoad(fontface); 249 | }).catch(function (e) { 250 | fontface.loadStatus = FontFaceLoadStatus.ERROR; 251 | fontface.rejectLoad(fontface); 252 | }); 253 | } else { 254 | fontface.loadStatus = FontFaceLoadStatus.ERROR; 255 | fontface.rejectLoad(fontface); 256 | } 257 | }).catch(function () { 258 | fontface.loadStatus = FontFaceLoadStatus.ERROR; 259 | fontface.rejectLoad(fontface); 260 | }); 261 | } 262 | return this.promise; 263 | }; 264 | }); 265 | -------------------------------------------------------------------------------- /test/fontfaceset-test.js: -------------------------------------------------------------------------------- 1 | describe('FontFaceSet', function () { 2 | var FontFaceSet = fl.FontFaceSet, 3 | FontFace = fl.FontFace, 4 | 5 | font1 = null, 6 | font2 = null, 7 | font3 = null, 8 | font4 = null; 9 | 10 | beforeEach(function () { 11 | font1 = new FontFace('1', 'url(font.woff)', {}); 12 | font2 = new FontFace('2', 'url(font.woff)', {}); 13 | font3 = new FontFace('3', 'url(font.woff)', {}); 14 | font4 = new FontFace('4', 'url(font.woff)', {}); 15 | }); 16 | 17 | 18 | describe('#constructor', function () { 19 | it('sets the size to zero and status to "loaded"', function () { 20 | var set = new FontFaceSet(); 21 | 22 | expect(set.size, 'to equal', 0); 23 | expect(set.status, 'to equal', 'loaded'); 24 | }); 25 | }); 26 | 27 | describe('#add', function () { 28 | it('adds non duplicate items', function () { 29 | var set = new FontFaceSet(); 30 | 31 | set.add(font1); 32 | set.add(font2); 33 | set.add(font3); 34 | 35 | expect(set.size, 'to equal', 3); 36 | expect(set.fonts, 'to equal', [font1, font2, font3]); 37 | }); 38 | 39 | it('adds duplicate items only once', function () { 40 | var set = new FontFaceSet(); 41 | 42 | set.add(font1); 43 | set.add(font2); 44 | 45 | set.add(font1); 46 | set.add(font2); 47 | 48 | expect(set.size, 'to equal', 2); 49 | expect(set.fonts, 'to equal', [font1, font2]); 50 | }); 51 | }); 52 | 53 | describe('#has', function () { 54 | it('has items that are in the set', function () { 55 | var set = new FontFaceSet(); 56 | 57 | set.add(font1); 58 | set.add(font3); 59 | 60 | expect(set.has(font1), 'to be true'); 61 | expect(set.has(font3), 'to be true'); 62 | }); 63 | 64 | it('does not have items that are in the set', function () { 65 | var set = new FontFaceSet(); 66 | 67 | set.add(font1); 68 | set.add(font3); 69 | 70 | expect(set.has(font2), 'to be false'); 71 | expect(set.has(font4), 'to be false'); 72 | }); 73 | }); 74 | 75 | describe('#delete', function () { 76 | it('removes the value and returns true', function () { 77 | var set = new FontFaceSet(); 78 | 79 | set.add(font1); 80 | set.add(font2); 81 | set.add(font3); 82 | 83 | expect(set.delete(font2), 'to be true'); 84 | expect(set.size, 'to equal', 2); 85 | expect(set.has(font1), 'to be true'); 86 | expect(set.has(font2), 'to be false'); 87 | expect(set.has(font3), 'to be true'); 88 | }); 89 | 90 | it('returns false if the value was not found', function () { 91 | var set = new FontFaceSet(); 92 | 93 | set.add(font1); 94 | set.add(font2); 95 | 96 | expect(set.delete(font3), 'to be false'); 97 | expect(set.size, 'to equal', 2); 98 | expect(set.has(font1), 'to be true'); 99 | expect(set.has(font2), 'to be true'); 100 | }); 101 | }); 102 | 103 | describe('#clear', function () { 104 | it('removes all values from the set', function () { 105 | var set = new FontFaceSet(); 106 | 107 | set.add(font1); 108 | set.add(font2); 109 | 110 | set.clear(); 111 | expect(set.size, 'to equal', 0); 112 | expect(set.fonts, 'to equal', []); 113 | }); 114 | }); 115 | 116 | describe('#forEach', function () { 117 | it('iterates over all values', function () { 118 | var set = new FontFaceSet(), 119 | values = []; 120 | 121 | set.add(font1); 122 | set.add(font2); 123 | 124 | set.forEach(function (value, key, set) { 125 | values.push([value, key, set]); 126 | }); 127 | 128 | expect(values, 'to equal', [[font1, 0, set], [font2, 1, set]]); 129 | }); 130 | }); 131 | 132 | describe('#match', function () { 133 | it('returns an empty array', function () { 134 | var set = new FontFaceSet(); 135 | 136 | expect(set.match('300px "My Family"'), 'to have length', 0); 137 | }); 138 | 139 | it('returns one match', function () { 140 | var set = new FontFaceSet(), 141 | font = new FontFace('My Family', 'url(font.woff)'); 142 | 143 | set.add(font); 144 | 145 | expect(set.match('16px "My Family"'), 'to equal', [font]); 146 | }); 147 | 148 | it('returns two matches', function () { 149 | var set = new FontFaceSet(), 150 | font1 = new FontFace('My Family', 'url(font.woff)', {}), 151 | font2 = new FontFace('My Other Family', 'url(font.woff)', {}); 152 | 153 | set.add(font1); 154 | set.add(font2); 155 | 156 | expect(set.match('16px "My Family", "My Other Family"'), 'to equal', [font1, font2]); 157 | }); 158 | 159 | it('returns one match out of two possible', function () { 160 | var set = new FontFaceSet(), 161 | font1 = new FontFace('My Family', 'url(font.woff)', {}), 162 | font2 = new FontFace('My Other Family', 'url(font.woff)', {}); 163 | 164 | set.add(font1); 165 | set.add(font2); 166 | 167 | expect(set.match('16px "My Other Family"'), 'to equal', [font2]); 168 | }); 169 | 170 | it('returns null on an invalid font string', function () { 171 | var set = new FontFaceSet(); 172 | 173 | expect(set.match('san-see'), 'to be null'); 174 | }); 175 | 176 | xit('rejects a match because of a mismatching unicode range', function () { 177 | var set = new FontFaceSet(), 178 | font = new FontFace('My Family', 'url(font.woff)', { unicodeRange: 'u+0' }); 179 | 180 | set.add(font); 181 | 182 | expect(set.match('16px "My Family"'), 'to equal', []); 183 | }); 184 | }); 185 | 186 | describe('#load', function () { 187 | it('loads a font', function (done) { 188 | var set = new FontFaceSet(), 189 | font = new FontFace('Font1', 'url(./assets/sourcesanspro-regular.woff) format("woff"), url(./assets/sourcesanspro-regular.ttf) format("truetype")'); 190 | 191 | set.add(font); 192 | 193 | set.load('16px Font1').then(function (fonts) { 194 | expect(set.status, 'to equal', 'loaded'); 195 | expect(fonts[0], 'to equal', font); 196 | done(); 197 | }).catch(function () { 198 | done(new Error('Should not be called')); 199 | }); 200 | }); 201 | 202 | it('fails to load a font', function (done) { 203 | var set = new FontFaceSet(), 204 | font = new FontFace('Font2', 'url(unknown.woff) format("woff"), url(unknown.ttf) format("truetype")'); 205 | 206 | set.add(font); 207 | 208 | set.load('16px Font2').then(function (fonts) { 209 | expect(set.status, 'to equal', 'loaded'); 210 | expect(fonts, 'to equal', [font]); 211 | done(); 212 | }).catch(function () { 213 | done(new Error('Should not be called')); 214 | }); 215 | }); 216 | 217 | it('returns an empty array when there are no matches in the set', function (done) { 218 | var set = new FontFaceSet(); 219 | 220 | set.load('16px Font').then(function (fonts) { 221 | expect(set.status, 'to equal', 'loaded'); 222 | expect(fonts, 'to equal', []); 223 | done(); 224 | }).catch(function () { 225 | done(new Error('Should not be called')); 226 | }); 227 | }); 228 | 229 | it('rejects the promise if the font string is invalid', function (done) { 230 | var set = new FontFaceSet(); 231 | 232 | set.load('see-sa').then(function () { 233 | done(new Error('Should not be called')); 234 | }).catch(function (fonts) { 235 | expect(fonts, 'to equal', []); 236 | done(); 237 | }); 238 | }); 239 | 240 | it('loads multiple matching fonts', function (done) { 241 | var set = new FontFaceSet(), 242 | font1 = new FontFace('Font1', 'url(./assets/sourcesanspro-regular.woff) format("woff"), url(./assets/sourcesanspro-regular.ttf) format("truetype")'), 243 | font2 = new FontFace('Font2', 'url(./assets/sourcesanspro-regular.woff) format("woff"), url(./assets/sourcesanspro-regular.ttf) format("truetype")'); 244 | 245 | set.add(font1); 246 | set.add(font2); 247 | 248 | set.load('16px Font1, Font2').then(function (fonts) { 249 | expect(set.status, 'to equal', 'loaded'); 250 | expect(fonts, 'to equal', [font1, font2]); 251 | done(); 252 | }).catch(function () { 253 | done(new Error('Should not be called')); 254 | }); 255 | }); 256 | 257 | it('loads a preloaded font', function (done) { 258 | var set = new FontFaceSet(), 259 | font = new FontFace('Font1', 'url(./assets/sourcesanspro-regular.woff) format("woff"), url(./assets/sourcesanspro-regular.ttf) format("truetype")'); 260 | 261 | font.load().then(function () { 262 | set.add(font); 263 | 264 | set.load('16px Font1').then(function (fonts) { 265 | expect(set.status, 'to equal', 'loaded'); 266 | expect(fonts, 'to equal', [font]); 267 | done(); 268 | }).catch(function () { 269 | done(new Error('Should not be called')); 270 | }); 271 | }); 272 | }); 273 | }); 274 | 275 | describe('#check', function () { 276 | it('returns true if the family is in the set and loaded', function () { 277 | var set = new FontFaceSet(), 278 | font1 = new FontFace('My Family', new ArrayBuffer(1)); 279 | 280 | set.add(font1); 281 | 282 | expect(set.check('16px My Family'), 'to be true'); 283 | }); 284 | 285 | it('returns false if the family is not in the set', function () { 286 | var set = new FontFaceSet(); 287 | 288 | expect(set.check('16px My Family'), 'to be false'); 289 | }); 290 | 291 | it('returns false if the family is in the set and not loaded', function () { 292 | var set = new FontFaceSet(), 293 | font1 = new FontFace('My Other Family', 'url(font.woff)'); 294 | 295 | set.add(font1); 296 | 297 | expect(set.check('16px My Other Family'), 'to be false'); 298 | }); 299 | 300 | it('returns false if only some of the fonts are loaded', function () { 301 | var set = new FontFaceSet(), 302 | font1 = new FontFace('My Family', new ArrayBuffer(1)), 303 | font2 = new FontFace('My Other Family', 'url(font.woff)'); 304 | 305 | set.add(font1); 306 | set.add(font2); 307 | 308 | expect(set.check('16px My Family'), 'to be true'); 309 | expect(set.check('16px My Other Family'), 'to be false'); 310 | 311 | expect(set.check('16px My Family, My Other Family'), 'to be false'); 312 | }); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /fontloader.js: -------------------------------------------------------------------------------- 1 | (function(){'use strict';var f=[];function g(a){f.push(a);1===f.length&&l()}function m(){for(;f.length;)f[0](),f.shift()}if(window.MutationObserver){var n=document.createElement("div");(new MutationObserver(m)).observe(n,{attributes:!0});var l=function(){n.setAttribute("x",0)}}else l=function(){setTimeout(m)};function p(a){this.a=q;this.b=void 0;this.f=[];var b=this;try{a(function(a){r(b,a)},function(a){t(b,a)})}catch(c){t(b,c)}}var q=2;function u(a){return new p(function(b,c){c(a)})}function v(a){return new p(function(b){b(a)})} 2 | function r(a,b){if(a.a===q){if(b===a)throw new TypeError("Promise settled with itself.");var c=!1;try{var d=b&&b.then;if(null!==b&&"object"===typeof b&&"function"===typeof d){d.call(b,function(b){c||r(a,b);c=!0},function(b){c||t(a,b);c=!0});return}}catch(e){c||t(a,e);return}a.a=0;a.b=b;w(a)}}function t(a,b){if(a.a===q){if(b===a)throw new TypeError("Promise settled with itself.");a.a=1;a.b=b;w(a)}} 3 | function w(a){g(function(){if(a.a!==q)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)}}})}p.prototype.g=function(a){return this.c(void 0,a)};p.prototype.c=function(a,b){var c=this;return new p(function(d,e){c.f.push([a,b,d,e]);w(c)})}; 4 | function x(a){return new p(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;kb.status||0===b.status;this.statusText=b.statusText;this.body=a}y.prototype.arrayBuffer=function(){return Promise.resolve(this.body)};var z=!(window.XDomainRequest&&!("responseType"in XMLHttpRequest.prototype)); 13 | function A(a){var b={};return new Promise(function(c,e){if(z){var d=new XMLHttpRequest;d.onload=function(){c(new y(d.response,{status:d.status,statusText:d.statusText}))};d.onerror=function(){e(new TypeError("Network request failed"))};d.open("GET",a);d.responseType="arraybuffer";b&&Object.keys(b).forEach(function(a){d.setRequestHeader(a,b[a])});d.send(null)}else d=new XDomainRequest,d.open("GET",a.replace(/^http(s)?:/i,window.location.protocol)),d.ontimeout=function(){return!0},d.onprogress=function(){return!0}, 14 | d.onload=function(){c(new y(d.responseText,{status:d.status,statusText:d.statusText}))},d.onerror=function(){e(new TypeError("Network request failed"))},setTimeout(function(){d.send(null)},0)})};function C(a){document.body?a():document.addEventListener("DOMContentLoaded",a)};function D(){this.a=document.createElement("div");this.a.setAttribute("aria-hidden","true");this.a.appendChild(document.createTextNode("@"));this.c=document.createElement("span");this.f=document.createElement("span");this.i=document.createElement("span");this.h=document.createElement("span");this.g=-1;this.c.style.cssText="display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.f.style.cssText="display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;"; 15 | this.h.style.cssText="display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.i.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;";this.c.appendChild(this.i);this.f.appendChild(this.h);this.a.appendChild(this.c);this.a.appendChild(this.f)} 16 | function E(a,b,c){a.a.style.cssText="min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;left:-999px;white-space:nowrap;font-size:100px;font-family:"+b+";"+c}function F(a){var b=a.a.offsetWidth,c=b+100;a.h.style.width=c+"px";a.f.scrollLeft=c;a.c.scrollLeft=a.c.scrollWidth+100;return a.g!==b?(a.g=b,!0):!1} 17 | function G(a,b){a.c.addEventListener("scroll",function(){F(a)&&null!==a.a.parentNode&&b(a.g)},!1);a.f.addEventListener("scroll",function(){F(a)&&null!==a.a.parentNode&&b(a.g)},!1);F(a)};function H(){var a={};this.family="_fff_";this.style=a.style||"normal";this.variant=a.variant||"normal";this.weight=a.weight||"normal";this.stretch=a.stretch||"stretch";this.featureSettings=a.featureSettings||"normal"}var I=null; 18 | function J(){var a=new H,b="font-style:"+a.style+";font-variant:"+a.variant+";font-weight:"+a.weight+";font-stretch:"+a.stretch+";font-feature-settings:"+a.featureSettings+";-moz-font-feature-settings:"+a.featureSettings+";-webkit-font-feature-settings:"+a.featureSettings+";",c=document.createElement("div"),e=new D,d=new D,p=new D,f=-1,k=-1,l=-1,t=-1,u=-1,v=-1;return new Promise(function(q,M){function B(){null!==c.parentNode&&c.parentNode.removeChild(c)}function w(){if(-1!==f&&-1!==k||-1!==f&&-1!== 19 | l||-1!==k&&-1!==l)if(f===k||f===l||k===l){if(null===I){var b=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);I=!!b&&(536>parseInt(b[1],10)||536===parseInt(b[1],10)&&11>=parseInt(b[2],10))}I?f===t&&k===t&&l===t||f===u&&k===u&&l===u||f===v&&k===v&&l===v||(B(),q(a)):(B(),q(a))}}C(function(){function q(){if(5E3<=Date.now()-N)B(),M(a);else{var b=document.hidden;if(!0===b||void 0===b)f=e.a.offsetWidth,k=d.a.offsetWidth,l=p.a.offsetWidth,w();setTimeout(q,50)}}var N=Date.now();E(e, 20 | "sans-serif",b);E(d,"serif",b);E(p,"monospace",b);c.appendChild(e.a);c.appendChild(d.a);c.appendChild(p.a);document.body.appendChild(c);t=e.a.offsetWidth;u=d.a.offsetWidth;v=p.a.offsetWidth;q();G(e,function(a){f=a;w()});E(e,'"'+a.family+'",sans-serif',b);G(d,function(a){k=a;w()});E(d,'"'+a.family+'",serif',b);G(p,function(a){l=a;w()});E(p,'"'+a.family+'",monospace',b)})})};var K=null; 21 | function L(){if(!K){var a=document.createElement("style"),b=document.getElementsByTagName("head")[0];a.appendChild(document.createTextNode('@font-face{font-family:"_fff_";src:url(data:font/woff2;base64,d09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAciuMTaGVo8IaqBbcKPeB3CyAAIO4unr9nb72QE3p00iGQQIZcAAcAMEJOztBx7zdWVWn//BAPW1l0BN429cPrCPE75MA637gPs0DjavNxzHtWeXXErKIV3AF9TbHqCTOATL2BgjeIH30lQwSAonU1LabV8Iz12wDvgd/obV5QVxXDKvUhW1QfWNrS6HzEQJaP4tBA==) format("woff2"),url(data:application/font-woff;base64,d09GRgABAAAAAAHgAAoAAAAAAggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABUAAAABcAAABOBIQEIWNtYXAAAAFwAAAAJgAAACwADABzZ2x5ZgAAAaAAAAAUAAAAFAwBPQJoZWFkAAAA9AAAAC0AAAA2CHEB92hoZWEAAAEkAAAAFgAAACQMAQgDaG10eAAAAWgAAAAIAAAACAgAAABsb2NhAAABmAAAAAYAAAAGAAoAAG1heHAAAAE8AAAAEwAAACAABAACbmFtZQAAAbQAAAAeAAAAIAAjCF5wb3N0AAAB1AAAAAwAAAAgAAMAAHgBY2BkYABhb81vuvH8Nl8ZmFgYQOBCWvVrMP3VURxEczBAxBmYQAQAAFIIBgAAAHgBY2BkYGBhAAEOKAkUQQVMAAJKABkAAHgBY2BkYGBgAkIgjQ0AAAC+AAcAeAFjAIEUBkYGcoECgwILmAEiASBRAK4AAAAAAAgAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoQgvsP//xDy/0EwnwEATX4GfAAAAAAAAAAKAAAAAQAAAAAIAAQAAAEAADEBCAAEAHgBY2BgYGKQY2BmYGThZGAEshmgbCYw2wEABjMAigAAeAFjYGbACwAAfQAE) format("woff")}'));b.appendChild(a); 22 | K=J().then(function(){var c=new D,e=["opentype","truetype"];E(c,"_fff_","");document.body.appendChild(c.a);var d=c.a.offsetWidth;200<=d&&e.unshift("woff");300==d&&e.unshift("woff2");b.removeChild(a);document.body.removeChild(c.a);return e},function(){return["opentype","truetype"]})}return K};function O(a,b,c){var e=this,d=c||{};this.l=b;this.f=null;this.a=[];this.j=new Promise(function(a,b){e.i=a;e.g=b});this.b="unloaded";this.c=null;Object.defineProperties(this,{family:{get:function(){return a}},style:{get:function(){return d.style||"normal"}},weight:{get:function(){return d.weight||"normal"}},stretch:{get:function(){return d.stretch||"normal"}},unicodeRange:{get:function(){return d.unicodeRange||"U+0-10FFFF"}},variant:{get:function(){return d.variant||"normal"}},featureSettings:{get:function(){return d.featureSettings|| 23 | "normal"}},status:{get:function(){return this.b}},loaded:{get:function(){return this.j}}});"string"===typeof b?this.a=x(b):(this.f=b,this.b="loaded",this.i(e))}var n=null;function P(a,b){for(var c=null,e=0;eCLOSURE_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 | '