mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Merge pull request #129 from locomotivemtl/feature/eager-fonts
Add support for the CSS Font Loading API
This commit is contained in:
@@ -2,6 +2,7 @@ import modular from 'modujs';
|
||||
import * as modules from './modules';
|
||||
import globals from './globals';
|
||||
import { html } from './utils/environment';
|
||||
import { isFontLoadingAPIAvailable, loadFonts } from './utils/fonts';
|
||||
|
||||
const app = new modular({
|
||||
modules: modules
|
||||
@@ -23,6 +24,11 @@ window.onload = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const EAGER_FONTS = [
|
||||
{ family: 'Source Sans', style: 'normal', weight: 400 },
|
||||
{ family: 'Source Sans', style: 'normal', weight: 700 },
|
||||
];
|
||||
|
||||
function init() {
|
||||
globals();
|
||||
|
||||
@@ -31,5 +37,21 @@ function init() {
|
||||
html.classList.add('is-loaded');
|
||||
html.classList.add('is-ready');
|
||||
html.classList.remove('is-loading');
|
||||
|
||||
/**
|
||||
* Eagerly load the following fonts.
|
||||
*/
|
||||
if (isFontLoadingAPIAvailable) {
|
||||
loadFonts(EAGER_FONTS).then((eagerFonts) => {
|
||||
html.classList.add('fonts-loaded');
|
||||
// console.group('Eager fonts loaded!', eagerFonts.length, '/', document.fonts.size);
|
||||
// console.group('State of eager fonts:')
|
||||
// eagerFonts.forEach((font) => console.log(font.family, font.style, font.weight, font.status/*, font*/))
|
||||
// console.groupEnd()
|
||||
// console.group('State of all fonts:')
|
||||
// document.fonts.forEach((font) => console.log(font.family, font.style, font.weight, font.status/*, font*/))
|
||||
// console.groupEnd()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export {default as Example} from './modules/Example';
|
||||
export {default as Load} from './modules/Load';
|
||||
export {default as Scroll} from './modules/Scroll';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { module } from 'modujs';
|
||||
import { EAGER_FONTS } from '../app';
|
||||
import { whenReady } from '../utils/fonts';
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
@@ -6,5 +8,10 @@ export default class extends module {
|
||||
}
|
||||
|
||||
init() {
|
||||
whenReady(EAGER_FONTS).then((fonts) => this.onFontsLoaded(fonts));
|
||||
}
|
||||
|
||||
onFontsLoaded(fonts) {
|
||||
console.log('Example: Eager Fonts Loaded!', fonts)
|
||||
}
|
||||
}
|
||||
|
||||
402
assets/scripts/utils/fonts.js
Normal file
402
assets/scripts/utils/fonts.js
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Font Faces
|
||||
*
|
||||
* Provides utilities to facilitate interactions with the CSS Font Loading API.
|
||||
*
|
||||
* Features functions to:
|
||||
*
|
||||
* - Retrieve one or more `FontFace` instances based on a font search query.
|
||||
* - Check if a `FontFace` instance matches a font search query.
|
||||
* - Eagerly load fonts that match a font search query.
|
||||
* - Wait until fonts that match a font search query are loaded.
|
||||
*
|
||||
* References:
|
||||
*
|
||||
* - {@link https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FontFaceReference
|
||||
*
|
||||
* @property {string} family - The name used to identify the font in our CSS.
|
||||
* @property {string} [style] - The style used by the font in our CSS.
|
||||
* @property {string} [weight] - The weight used by the font in our CSS.
|
||||
*/
|
||||
|
||||
const isFontLoadingAPIAvailable = ('fonts' in document);
|
||||
|
||||
/**
|
||||
* Determines if the given font matches the given `FontFaceReference`.
|
||||
*
|
||||
* @param {FontFace} font - The font to inspect.
|
||||
* @param {FontFaceReference} criterion - The object of property values to match.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function conformsToReference(font, criterion)
|
||||
{
|
||||
for (const [ key, value ] of Object.entries(criterion)) {
|
||||
switch (key) {
|
||||
case 'family': {
|
||||
if (trim(font[key]) !== value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'weight': {
|
||||
/**
|
||||
* Note concerning font weights:
|
||||
* Loose equality (`==`) is used to compare numeric weights,
|
||||
* a number (`400`) and a numeric string (`"400"`).
|
||||
* Comparison between numeric and keyword values is neglected.
|
||||
*
|
||||
* @link https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
*/
|
||||
if (font[key] != value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (font[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given font matches the given font shorthand.
|
||||
*
|
||||
* @param {FontFace} font - The font to inspect.
|
||||
* @param {string} criterion - The font shorthand to match.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function conformsToShorthand(font, criterion)
|
||||
{
|
||||
const family = trim(font.family);
|
||||
|
||||
if (trim(family) === criterion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
criterion.endsWith(trim(family)) && (
|
||||
criterion.match(font.weight) ||
|
||||
criterion.match(font.style)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given font matches any of the given criteria.
|
||||
*
|
||||
* @param {FontFace} font - The font to inspect.
|
||||
* @param {FontFaceReference[]} criteria - A list of objects with property values to match.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function conformsToAnyReference(font, criteria)
|
||||
{
|
||||
for (const criterion of criteria) {
|
||||
if (conformsToReference(font, criterion)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
|
||||
* the provided `FontFaceReference`.
|
||||
*
|
||||
* @param {FontFaceReference} font
|
||||
*
|
||||
* @returns {FontFace[]}
|
||||
*/
|
||||
function findManyByReference(search)
|
||||
{
|
||||
const found = [];
|
||||
|
||||
for (const font of document.fonts) {
|
||||
if (conformsToReference(font, search)) {
|
||||
found.push(font);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
|
||||
* the provided font shorthand.
|
||||
*
|
||||
* @param {string} font
|
||||
*
|
||||
* @returns {FontFace[]}
|
||||
*/
|
||||
function findManyByShorthand(search)
|
||||
{
|
||||
const found = [];
|
||||
|
||||
for (const font of document.fonts) {
|
||||
if (conformsToShorthand(font, search)) {
|
||||
found.push(font);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first `FontFace` from `document.fonts` that satisfies
|
||||
* the provided `FontFaceReference`.
|
||||
*
|
||||
* @param {FontFaceReference} font
|
||||
*
|
||||
* @returns {?FontFace}
|
||||
*/
|
||||
function findOneByReference(search)
|
||||
{
|
||||
for (const font of document.fonts) {
|
||||
if (conformsToReference(font, criterion)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first `FontFace` from `document.fonts` that satisfies
|
||||
* the provided font shorthand.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - "Roboto"
|
||||
* - "italic bold 16px Roboto"
|
||||
*
|
||||
* @param {string} font
|
||||
*
|
||||
* @returns {?FontFace}
|
||||
*/
|
||||
function findOneByShorthand(search)
|
||||
{
|
||||
for (const font of document.fonts) {
|
||||
if (conformsToShorthand(font, search)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `FontFace` from `document.fonts` that satisfies
|
||||
* the provided query.
|
||||
*
|
||||
* @param {FontFaceReference|string} font - Either:
|
||||
* - a `FontFaceReference` object
|
||||
* - a font family name
|
||||
* - a font specification, for example "italic bold 16px Roboto"
|
||||
*
|
||||
* @returns {?FontFace}
|
||||
*
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
function getAny(search) {
|
||||
if (search) {
|
||||
switch (typeof search) {
|
||||
case 'string':
|
||||
return findOneByShorthand(search);
|
||||
|
||||
case 'object':
|
||||
return findOneByReference(search);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
'Expected font query to be font shorthand or font reference'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of all `FontFace` from `document.fonts` that satisfy
|
||||
* the provided queries.
|
||||
*
|
||||
* @param {FontFaceReference|string|(FontFaceReference|string)[]} queries
|
||||
*
|
||||
* @returns {FontFace[]}
|
||||
*
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
function getMany(queries) {
|
||||
if (!Array.isArray(queries)) {
|
||||
queries = [ queries ];
|
||||
}
|
||||
|
||||
const found = new Set();
|
||||
|
||||
queries.forEach((search) => {
|
||||
if (search) {
|
||||
switch (typeof search) {
|
||||
case 'string':
|
||||
found.add(...findManyByShorthand(search));
|
||||
return;
|
||||
|
||||
case 'object':
|
||||
found.add(...findManyByReference(search));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
'Expected font query to be font shorthand or font reference'
|
||||
);
|
||||
})
|
||||
|
||||
return [ ...found ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a font face is registered.
|
||||
*
|
||||
* @param {FontFace|FontFaceReference|string} search - Either:
|
||||
* - a `FontFace` instance
|
||||
* - a `FontFaceReference` object
|
||||
* - a font family name
|
||||
* - a font specification, for example "italic bold 16px Roboto"
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasAny(search) {
|
||||
if (search instanceof FontFace) {
|
||||
return document.fonts.has(search);
|
||||
}
|
||||
|
||||
return getAny(search) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagerly load fonts.
|
||||
*
|
||||
* Most user agents only fetch and load fonts when they are first needed
|
||||
* ("lazy loaded"), which can result in a perceptible delay.
|
||||
*
|
||||
* This function will "eager load" the fonts.
|
||||
*
|
||||
* @param {(FontFace|FontFaceReference)[]} fontsToLoad - List of fonts to load.
|
||||
* @param {boolean} [debug] - If TRUE, log details to the console.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function loadFonts(fontsToLoad, debug = false)
|
||||
{
|
||||
if ((fontsToLoad.size ?? fontsToLoad.length) === 0) {
|
||||
throw new TypeError(
|
||||
'Expected at least one font'
|
||||
);
|
||||
}
|
||||
|
||||
return await loadFontsWithAPI([ ...fontsToLoad ], debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagerly load a font using `FontFaceSet` API.
|
||||
*
|
||||
* @param {FontFace} font
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function loadFontFaceWithAPI(font)
|
||||
{
|
||||
return await (font.status === 'unloaded'
|
||||
? font.load()
|
||||
: font.loaded
|
||||
).then((font) => font, (err) => font)
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagerly load fonts using `FontFaceSet` API.
|
||||
*
|
||||
* @param {FontFaceReference[]} fontsToLoad
|
||||
* @param {boolean} [debug]
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function loadFontsWithAPI(fontsToLoad, debug = false)
|
||||
{
|
||||
debug && console.group('[loadFonts:API]', fontsToLoad.length, '/', document.fonts.size);
|
||||
|
||||
const fontsToBeLoaded = [];
|
||||
|
||||
for (const fontToLoad of fontsToLoad) {
|
||||
if (fontToLoad instanceof FontFace) {
|
||||
if (!document.fonts.has(fontToLoad)) {
|
||||
document.fonts.add(fontToLoad);
|
||||
}
|
||||
|
||||
fontsToBeLoaded.push(
|
||||
loadFontFaceWithAPI(fontToLoad)
|
||||
);
|
||||
} else {
|
||||
fontsToBeLoaded.push(
|
||||
...getMany(fontToLoad).map((font) => loadFontFaceWithAPI(font))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
debug && console.groupEnd();
|
||||
|
||||
return await Promise.all(fontsToBeLoaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes quotes from the the string.
|
||||
*
|
||||
* When a `@font-face` is declared, the font family is sometimes
|
||||
* defined in quotes which end up included in the `FontFace` instance.
|
||||
*
|
||||
* @param {string} value
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function trim(value) {
|
||||
return value.replace(/['"]+/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves with the specified fonts
|
||||
* when they are done loading or failed.
|
||||
*
|
||||
* @param {FontFaceReference|string|(FontFaceReference|string)[]} queries
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function whenReady(queries)
|
||||
{
|
||||
const fonts = getMany(queries);
|
||||
|
||||
return await Promise.all(fonts.map((font) => font.loaded));
|
||||
}
|
||||
|
||||
export {
|
||||
getAny,
|
||||
getMany,
|
||||
hasAny,
|
||||
isFontLoadingAPIAvailable,
|
||||
loadFonts,
|
||||
whenReady,
|
||||
}
|
||||
@@ -28,9 +28,7 @@ $font-fallback-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console
|
||||
// <font-id>: (<font-name>, <font-fallbacks>)
|
||||
// ```
|
||||
$font-families: (
|
||||
"sans": ("Webfont Sans", $font-fallback-sans),
|
||||
// "serif": ("Webfont Serif", $font-fallback-serif),
|
||||
// "mono": ("Webfont Mono", $font-fallback-mono)
|
||||
sans: join(Source Sans, $font-fallback-sans, $separator: comma),
|
||||
);
|
||||
|
||||
// List of custom font faces as tuples.
|
||||
@@ -39,9 +37,10 @@ $font-families: (
|
||||
// <font-name> <font-file-basename> <font-weight> <font-style>
|
||||
// ```
|
||||
$font-faces: (
|
||||
// "Webfont Sans" "webfont-sans_regular" 400 normal,
|
||||
// "Webfont Sans" "webfont-sans_regular-italic" 400 italic,
|
||||
// "Webfont Serif" "webfont-sans_bold" 700 normal,
|
||||
(Source Sans, "SourceSans3-Bold", 700, normal),
|
||||
(Source Sans, "SourceSans3-BoldIt", 700, italic),
|
||||
(Source Sans, "SourceSans3-Regular", 400, normal),
|
||||
(Source Sans, "SourceSans3-RegularIt", 400, italic),
|
||||
);
|
||||
|
||||
// Typography
|
||||
|
||||
BIN
www/assets/fonts/SourceSans3-Bold.woff2
Normal file
BIN
www/assets/fonts/SourceSans3-Bold.woff2
Normal file
Binary file not shown.
BIN
www/assets/fonts/SourceSans3-BoldIt.woff2
Normal file
BIN
www/assets/fonts/SourceSans3-BoldIt.woff2
Normal file
Binary file not shown.
BIN
www/assets/fonts/SourceSans3-Regular.woff2
Normal file
BIN
www/assets/fonts/SourceSans3-Regular.woff2
Normal file
Binary file not shown.
BIN
www/assets/fonts/SourceSans3-RegularIt.woff2
Normal file
BIN
www/assets/fonts/SourceSans3-RegularIt.woff2
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -29,11 +29,20 @@
|
||||
-->
|
||||
<!-- <link rel="icon" href="assets/images/favicons/favicon.svg"> -->
|
||||
|
||||
<!-- Preload Fonts -->
|
||||
<link rel="preload" href="assets/fonts/SourceSans3-Bold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="assets/fonts/SourceSans3-BoldIt.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="assets/fonts/SourceSans3-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="assets/fonts/SourceSans3-RegularIt.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
<link id="main-css" rel="stylesheet" href="assets/styles/main.css" media="print"
|
||||
onload="this.media='all'; this.onload=null; this.isLoaded=true">
|
||||
</head>
|
||||
|
||||
<body data-module-load>
|
||||
|
||||
<!-- <p aria-hidden="true" class="u-screen-reader-text" style="font-family: 'Webfont';"> </p> -->
|
||||
|
||||
<div data-load-container>
|
||||
<div class="o-scroll" data-module-scroll="main">
|
||||
<header data-scroll-section>
|
||||
@@ -49,7 +58,7 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main data-scroll-section>
|
||||
<main data-module-example data-scroll-section>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Hello</h1>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user