Source: fonts.js

/*!
 * Module Fonts
 */

/**
 * @namespace Fonts
 */

const MODULE_NAME = 'Fonts';



//###[ IMPORTS ]########################################################################################################

import {hasValue, orDefault, Deferred} from './basic.js';
import {createNode} from './elements.js';
import {applyStyles} from './css.js';
import {loop, countermand} from  './timers.js';



//###[ EXPORTS ]########################################################################################################

/**
 * @namespace Fonts:waitForWebfonts
 */

/**
 * Waits for a list of webfonts to load. This includes the fact, that the font is ready to display and renders in the
 * browser's rendering engine and not just a completed request or a loaded resource.
 *
 * Also works for fonts, that have already been loaded.
 *
 * @param {String|String[]} fonts - the CSS-names of the fonts to wait for
 * @param {?String} [fallbackFontName=sans-serif] - the system font which the page falls back on if the webfont is not loaded
 * @param {?Number} [timeout=5000] - timeout in ms after which the call fails and the return value rejects
 * @returns {Basic.Deferred<Array<String>|String>} a Deferred, that resolves once all webfonts are available and rejects when the timeout is reached
 *
 * @memberof Fonts:waitForWebfonts
 * @alias waitForWebfonts
 * @example
 * waitForWebfonts(['purr-regular', 'scratch-light'], 'helvetica, sans-serif')
 *   .then(fonts => {
 *     document.body.classList.add('webfonts-loaded');
 *     alert(`${fonts.length} webfonts ready to render`);
 *   })
 *   .catch(error => {
 *     if( error.message === 'timeout' ){
 *       document.body.classList.add('webfonts-timeout');
 *     }
 *   })
 * ;
 */
export function waitForWebfonts(fonts, fallbackFontName='sans-serif', timeout=5000){
	fonts = orDefault(fonts, [], 'arr').map(font => `${font}`);
	fallbackFontName = orDefault(fallbackFontName, 'sans-serif', 'string');
	timeout = orDefault(timeout, 5000, 'int');

	const
		deferred = new Deferred(),
		start = Date.now(),
		fDimsAreIdentical = (dims1, dims2) => ((dims1.width === dims2.width) && (dims1.height === dims2.height))
	;
	let	loadedFonts = 0;

	fonts.forEach(font => {
		let node = createNode('span', null, 'giItT1WQy@!-/#');
		applyStyles(node, {
			'position' : 'absolute',
			'visibility' : 'hidden',
			'left' : '-10000px',
			'top' : '-10000px',
			'font-size' : '300px',
			'font-family' : fallbackFontName,
			'font-variant' : 'normal',
			'font-style' : 'normal',
			'font-weight' : 'normal',
			'letter-spacing' : '0',
			'white-space' : 'pre',
			'line-height' : 1
		});
		document.body.appendChild(node);

		const systemFontDims = {
			width : node.offsetWidth,
			height : node.offsetHeight
		};
		applyStyles(node, {'font-family' : `${font}, ${fallbackFontName}`});

		let fontLoadedCheckTimer = null;
		const fCheckFont = () => {
			if( (Date.now() - start) >= timeout ){
				countermand(fontLoadedCheckTimer);
				if( hasValue(node) ){
					document.body.removeChild(node);
					node = null;
				}
				deferred.reject(new Error('timeout'));
				return true;
			}

			if(
				hasValue(node)
				&& !fDimsAreIdentical({width : node.offsetWidth, height : node.offsetHeight}, systemFontDims)
			){
				countermand(fontLoadedCheckTimer);
				document.body.removeChild(node);
				node = null;
				loadedFonts++;
			}

			if( loadedFonts >= fonts.length ){
				deferred.resolve((fonts.length === 1) ? fonts[0] : fonts);
				return true;
			}

			return false;
		}
		if( !fCheckFont() ){
			fontLoadedCheckTimer = loop(100, fCheckFont);
		}
	});

	return deferred;
}