@eclipse-scout/core
Version:
Eclipse Scout runtime
335 lines (304 loc) • 12.2 kB
text/typescript
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, graphics, scout, strings} from '../index';
import $ from 'jquery';
export interface FontDescriptor {
family: string;
style?: string;
testString?: string;
testFonts?: string;
}
export interface FontPreloadOptions {
/**
* A single string or object (or an array of them) specifying which fonts should be preloaded.
* A string is interpreted as font-family.
* If the style is relevant too, an object with the properties 'family' and 'style' should be provided.
* Alternatively, the style can be specified in the string after the font name, separated by a pipe character ('|').
* The property {@link testString} (or a third component in a '|' separated string) may be specified to set the characters to measure for this specific font (can be useful for icon fonts).
*/
fonts: string | string[] | FontDescriptor | FontDescriptor[];
/**
* Mandatory function to be called when all of the specified fonts have been loaded or if a timeout occurs. If this option is omitted, the call to this method returns immediately.
* @param success indicate whether loading was completed successfully or execution was interrupted by a timeout.
* @param badFonts The bad fonts
*/
onComplete?: (success: boolean, badFonts: string[]) => void;
/**
* Optional timeout in milliseconds. If fonts could not be loaded within this time, loading is stopped and the {@link onComplete} method is called with argument 'false'.
* Defaults to {@link TEST_TIMEOUT}.
*/
timeout?: number;
/**
* Optional. Test fonts (string separated by commas) to used as baseline when checking if the specified fonts have been loaded. Defaults to {@link TEST_FONTS}.
*/
testFonts?: string;
/**
* Optional. The test string to use when checking if the specified fonts have been loaded. Should not be empty, because the empty string has always the width 0.
* The default is {@link TEST_STRING}. The test string may also be specified individually per font.
*/
testString?: string;
}
export const fonts = {
_deferred: $.Deferred(),
/**
* Indicates whether all fonts have been loaded successfully. Check this variable before
* waiting for the promise object returned by preloader().
*/
loadingComplete: true,
/**
* Start preloading the specified fonts. If no fonts are specified, the list of fonts
* to preload is automatically calculated from the available CSS "@font-face" definitions.
* To disable preloading entirely, pass an empty array to this function.
*
* @param fontArr (optional) array of fonts
* @returns promise that is resolved when all fonts are loaded
*/
bootstrap(fontArr: FontDescriptor[]): JQuery.Promise<void> {
fontArr = fontArr || fonts.autoDetectFonts();
if (fontArr.length === 0) {
fonts.loadingComplete = true;
return $.resolvedPromise();
}
// Start preloading
fonts.loadingComplete = false;
fonts.preload({
fonts: fontArr,
onComplete: (success, badFonts) => {
if (!success && badFonts && badFonts.length) {
$.log.warn('Timeout occurred while pre-loading the following fonts:\n\n- ' + badFonts.join('\n- ') + '\n\n' +
'Rendering will now continue, but font measurements may be inaccurate. ' +
'To prevent unnecessary startup delays and layout problems, check the @font-face ' +
'definitions and the referenced "src" URLs or programmatically add additional font-specific ' +
'characters to TEST_STRING before calling app.init().');
}
fonts.loadingComplete = true;
fonts._deferred.resolve();
}
});
return $.resolvedPromise();
},
/**
* @returns a promise object that is notified when the font preloading was completed.
* Important: Before waiting for this promise, always check that value of
* loadingComplete first! Do not wait for the promise when loadingComplete
* is true, because the promise will never be resolved.
*/
preloader(): JQuery.Promise<void> {
return fonts._deferred.promise();
},
TEST_FONTS: 'monospace',
/**
* Test string used for font measurements. Used to detect when a font is fully loaded
* and available in the browser.
*
* Custom characters may be added to this test string if a font is not detected correctly
* because it does not contain any of the default characters.
*
* U+E000 = Start of Unicode private use zone (e.g. scoutIcons)
* U+F118 = Font Awesome: "smile"
*/
TEST_STRING: 'ABC abc 123 .,_ LlIi1 oO0 !#@ \uE000\uE001\uE002 \uF118',
/**
* Time in milliseconds to wait for the fonts to be loaded.
*/
TEST_TIMEOUT: 12 * 1000, // 12 sec
/**
* Loads the specified fonts in a hidden div, forcing the browser to load them.
* Examples:
* preload({fonts: 'Sauna Pro'});
* preload({fonts: 'Sauna Pro|font-style:italic'});
* preload({fonts: 'Sauna Pro|font-style:italic|The quick brown fox jumps over the lazy dog'});
* preload({fonts: 'Sauna Pro | font-style: italic; font-weight: 700'});
* preload({fonts: 'Sauna Pro', onComplete: handleLoadFinished});
* preload({fonts: ['Sauna Pro', 'Dolly Pro']});
* preload({fonts: {family:'Sauna', style: 'font-style:italic; font-weight:700', testString: 'MyString012345'}, timeout: 999});
* preload({fonts: ['Fakir-Black', {family:'Fakir-Italic', style:'font-style:italic'}], timeout: 2500, onComplete: function() { setCookie('fakir','loaded') }});
*
* Inspired by Zenfonts (https://github.com/zengabor/zenfonts, public domain).
*/
preload(options?: FontPreloadOptions) {
options = options || {fonts: null};
let fontArr = arrays.ensure(options.fonts);
if (!options.onComplete) {
// preloading is not useful, because there is no callback on success
return;
}
// Create a DIV for each font
let divs = [];
fontArr.forEach(font => {
// Convert to object
if (typeof font === 'string') {
let fontParts = strings.splitMax(font, '|', 3).map(s => {
return s.trim();
});
font = {
family: fontParts[0],
style: fontParts[1],
testString: fontParts[2]
};
}
font.family = font.family || '';
font.style = font.style || '';
font.testString = font.testString || options.testString || fonts.TEST_STRING;
// these fonts are compared to the custom fonts, strings separated by comma
let testFonts = font.testFonts || options.testFonts || fonts.TEST_FONTS;
// Create DIV with default fonts
// (Because preloader functionality should not depend on a CSS style sheet we set the required properties programmatically.)
let $div = $('body').appendDiv('font-preloader')
.text(font.testString)
.css('display', 'block')
.css('visibility', 'hidden')
.css('position', 'absolute')
.css('top', 0)
.css('left', 0)
.css('width', 'auto')
.css('height', 'auto')
.css('margin', 0)
.css('padding', 0)
.css('white-space', 'nowrap')
.css('line-height', 'normal')
.css('font-variant', 'normal')
.css('font-size', '20em')
.css('font-family', testFonts);
// Remember size, set new font, and then measure again
let originalSize = fonts.measureSize($div);
$div.data('original-size', originalSize);
$div.data('font-family', font.family);
$div.css('font-family', '\'' + font.family + '\',' + testFonts);
if (font.style) {
let style = ($div.attr('style') || '').trim();
let sep = (style.substr(-1) === ';' ? '' : ';') + (style ? ' ' : '');
$div.attr('style', style + sep + font.style);
}
if (fonts.measureSize($div) !== originalSize) {
// Font already loaded, nothing to do
$div.remove();
} else {
// Remember DIV
divs.push($div);
}
});
if (divs.length === 0) {
// No fonts need to be watched, everything is loaded already
complete(true);
return;
}
let onFinished = complete;
let timeout = scout.nvl(options.timeout, fonts.TEST_TIMEOUT);
let watchTimerId, timeoutTimerId;
if (timeout && timeout >= 0) {
// Add timeout
timeoutTimerId = setTimeout(() => {
clearTimeout(watchTimerId);
complete(false);
}, timeout);
onFinished = () => {
clearTimeout(timeoutTimerId);
complete(true);
};
}
// Start watching (initially 50ms delay)
watchWidthChange(50, onFinished);
// ----- Helper functions -----
function watchWidthChange(delay, onFinished) {
// Check each DIV
let i = divs.length;
while (i--) {
let $div = divs[i];
if (fonts.measureSize($div) !== $div.data('original-size')) {
divs.splice(i, 1);
$div.remove();
}
}
if (divs.length === 0) {
// All completed
onFinished(true);
return;
}
// Watch again after a small delay
watchTimerId = setTimeout(() => {
// Slowly increase delay up to 1 second
if (delay < 1000) {
delay = delay * 1.2;
}
watchWidthChange(delay, onFinished);
}, delay);
}
function complete(success) {
options.onComplete(success, divs.map($div => {
return $div.data('font-family');
}));
}
},
measureSize($div: JQuery): string {
let size = graphics.size($div, {
exact: true
});
return size.width + 'x' + size.height;
},
/**
* Reads all "@font-face" CSS rules from the current document and returns an array of
* font definition objects, suitable for passing to the preload() function (see above).
*/
autoDetectFonts(): FontDescriptor[] {
let fontArr = [];
// Implementation note: "styleSheets" and "cssRules" are not arrays (they only look like arrays)
let styleSheets = document.styleSheets;
for (let i = 0; i < styleSheets.length; i++) {
let styleSheet = styleSheets[i];
let cssRules;
try {
cssRules = styleSheet.cssRules;
} catch (error) {
// In some browsers, access to style sheets of other origins is blocked:
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#Notes
$.log.info('Skipped automatic font detection for style sheet ' + styleSheet.href +
' (access blocked by browser). Use the bootstrap argument "fonts" to manually list fonts to pre-load.');
continue;
}
for (let j = 0; j < styleSheet.cssRules.length; j++) {
let cssRule = styleSheet.cssRules[j];
if (cssRule.type === window.CSSRule.FONT_FACE_RULE) {
// @ts-expect-error
let style = cssRule.style;
let ff = style.getPropertyValue('font-family');
let fw = style.getPropertyValue('font-weight');
let fs = style.getPropertyValue('font-style');
let fv = style.getPropertyValue('font-variant');
let ft = style.getPropertyValue('font-stretch');
if (ff) {
ff = ff.replace(/^["']|["']$/g, ''); // Unquote strings, they will be quoted again automatically
let s = [];
if (fw && fw !== 'normal') {
s.push('font-weight:' + fw);
}
if (fs && fs !== 'normal') {
s.push('font-style:' + fs);
}
if (fv && fv !== 'normal') {
s.push('font-variant:' + fv);
}
if (ft && ft !== 'normal') {
s.push('font-stretch:' + ft);
}
let font: FontDescriptor = {
family: ff
};
if (s.length) {
font.style = s.join(';');
}
fontArr.push(font);
}
}
}
}
return fontArr;
}
};