1
0
mirror of https://github.com/locomotivemtl/locomotive-boilerplate.git synced 2026-01-15 00:55:08 +08:00

4 Commits

Author SHA1 Message Date
Lucas Vallenet
cc8ff1f4a4 Add config vars / Reformat app.js and add explicite comments 2022-06-03 10:55:12 +02:00
Lucas Vallenet
f714cd34e2 Add config image selectors and css class / Refactor app.js 2022-06-02 17:31:19 +02:00
Lucas Vallenet
d70ff62294 Add config.js / Update app.js with js class / Add loaders.js 2022-06-02 16:17:25 +02:00
Lucas Vallenet
110fb2e09b Loaders (wip) 2022-06-02 10:07:13 +02:00
9 changed files with 5321 additions and 117 deletions

View File

@@ -1,35 +1,109 @@
import modular from 'modujs';
import * as modules from './modules';
import globals from './globals';
import { html } from './utils/environment';
import modular from 'modujs'
import * as modules from './modules'
import globals from './globals'
import { fontsLoader, preloadImages, styleSheetsLoader } from './utils/loaders'
import { html } from './utils/environment'
import config from './config'
const app = new modular({
modules: modules
});
class App {
constructor() {
console.log(`${this.constructor.name}:constructor`)
window.onload = (event) => {
const $style = document.getElementById('main-css');
this.options = Object.freeze({
fonts: [
// { name: '<font-name>', style: '<font-style>', weight: '<font-weight>' }
],
preloadImage: [
config.SELECTORS.IMAGE_PRELOAD
],
styleSheets: [
config.SELECTORS.MAIN_STYLESHEET
]
})
if ($style) {
if ($style.isLoaded) {
init();
} else {
$style.addEventListener('load', (event) => {
init();
});
}
} else {
console.warn('The "main-css" stylesheet not found');
// Create app
this.moduleManager = new modular({
modules: modules
})
// this.addCustomEvents()
this.setInitialVars()
}
};
function init() {
globals();
load() {
console.log(`${this.constructor.name}:load`)
app.init(app);
// Font load
const fontLoad = new Promise(resolve => {
fontsLoader(this.options.fonts, () => {
html.classList.add(config.CSS_CLASS.FONTS_LOADED)
resolve()
})
})
html.classList.add('is-loaded');
html.classList.add('is-ready');
html.classList.remove('is-loading');
// Image preload
const imagePreload = new Promise(resolve => {
preloadImages(this.options.preloadImage, () => {
html.classList.add(config.CSS_CLASS.IMAGES_PRELOADED)
resolve()
})
})
// Stylesheets load
const styleSheetLoad = new Promise(resolve => styleSheetsLoader(this.options.styleSheets, () => resolve()))
Promise.all([
fontLoad,
imagePreload,
styleSheetLoad,
]).then(() => {
this.init()
}).catch(e => {
console.log(e)
})
}
init() {
console.log(`${this.constructor.name}:init`)
// Init globals
globals()
// Init modular app
this.moduleManager.init(this.moduleManager)
// Update classes
html.classList.add(config.CSS_CLASS.LOADED)
html.classList.add(config.CSS_CLASS.READY)
html.classList.remove(config.CSS_CLASS.LOADING)
}
addCustomEvents() {
console.log(`${this.constructor.name}:addCustomEvents`)
}
/*
* Set initial variables.
*/
setInitialVars() {
/**
* Store the initial viewport height in a CSS property.
*
* @see {@link https://css-tricks.com/the-trick-to-viewport-units-on-mobile/}
* This can be applied to elements, instead of using the `vh` unit,
* for consistent and correct sizing on mobile browsers.
*
* @see {@link https://caniuse.com/viewport-unit-variants}
* This trick should be replaced with viewport-relative units
* once browser support has improved.
*/
html.style.setProperty('--vh-initial', `${0.01 * html.clientHeight}px`)
}
}
const app = new App()
window.addEventListener('load', () => app.load(), { once: true })

25
assets/scripts/config.js Normal file
View File

@@ -0,0 +1,25 @@
const env = process.env.NODE_ENV
export default config = Object.freeze({
// Environments
ENV: env,
IS_PROD: env === 'production',
IS_DEV: env === 'development',
// CSS class names
CSS_CLASS: {
LOADING: 'is-loading',
READY: 'is-ready',
LOADED: 'is-loaded',
FONTS_LOADED: 'has-fonts-loaded',
IMAGES_PRELOADED: 'has-images-preloaded',
LAZY_LOADED: 'is-lazy-loaded',
},
// JS selectors
SELECTORS: {
IMAGE_LAZY: '.c-lazy',
IMAGE_PRELOAD: 'img[data-preload]',
MAIN_STYLESHEET: '#main-css'
}
})

View File

@@ -1,5 +1,5 @@
import { module } from 'modujs';
import { lazyLoadImage } from '../utils/image';
import { lazyLoadImage } from '../utils/loaders';
import LocomotiveScroll from 'locomotive-scroll';
export default class extends module {

View File

@@ -1,87 +1,19 @@
const LAZY_LOADED_IMAGES = []
export function loadImage(url, options = {}) {
return new Promise((resolve, reject) => {
const $img = new Image();
if (options.crossOrigin) {
$img.crossOrigin = options.crossOrigin;
}
const loadCallback = () => {
resolve({
element: $img,
...getImageMetadata($img),
});
}
if($img.decode) {
$img.src = url
$img.decode().then(loadCallback).catch(e => {
reject(e)
})
} else {
$img.onload = loadCallback
$img.onerror = (e) => {
reject(e);
};
$img.src = url
}
});
}
export function getImageMetadata($img) {
return {
url: $img.src,
width: $img.naturalWidth,
height: $img.naturalHeight,
ratio: $img.naturalWidth / $img.naturalHeight,
};
}
/**
* Lazy load the given image.
* Get an image meta data
*
* @param {HTMLImageElement} $el - The image element.
* @param {?string} url - The URI to lazy load into $el.
* If falsey, the value of the `data-src` attribute on $el will be used as the URI.
* @param {?function} callback - A function to call when the image is loaded.
* @param {HTMLImageElement} $img - The image element.
* @return {object} The given image meta data
*/
export async function lazyLoadImage($el, url, callback) {
let src = url ? url : $el.dataset.src
let loadedImage = LAZY_LOADED_IMAGES.find(image => image.url === src)
const getImageMetadata = $img => ({
url: $img.src,
width: $img.naturalWidth,
height: $img.naturalHeight,
ratio: $img.naturalWidth / $img.naturalHeight,
})
if (!loadedImage) {
loadedImage = await loadImage(src)
if (!loadedImage.url) {
return;
}
LAZY_LOADED_IMAGES.push(loadedImage)
}
if($el.src === src) {
return
}
if ($el.tagName === 'IMG') {
$el.src = loadedImage.url;
} else {
$el.style.backgroundImage = `url(${loadedImage.url})`;
}
requestAnimationFrame(() => {
let lazyParent = $el.closest('.c-lazy');
if(lazyParent) {
lazyParent.classList.add('-lazy-loaded')
lazyParent.style.backgroundImage = ''
}
$el.classList.add('-lazy-loaded')
callback?.()
})
export {
getImageMetadata,
}

View File

@@ -0,0 +1,298 @@
import config from '../config'
import { getImageMetadata } from './image'
/**
* Load the given font
*
* @param {string} fontName - The font name
* @param {string} fontStyle - The font style
* @param {string} fontWeight - The font weight
* @return {function} Promise
*/
const loadFont = (fontName, fontStyle, fontWeight) => {
return new Promise(resolve => {
let loop = null
const clearLoop = () => {
if (loop) {
clearInterval(loop)
loop = null
}
}
const tryToLoadFont = () => {
let hasLoaded = false
try {
hasLoaded = document.fonts.check(`${fontStyle} ${fontWeight} 16px ${fontName}`)
} catch (e) {
console.warn(`CSS font loading API error with ${fontName} ${fontStyle} ${fontWeight}`, e)
clearLoop()
resolve()
}
if (hasLoaded) {
console.info(`${fontName} ${fontStyle} ${fontWeight} loaded`)
clearLoop()
resolve()
}
}
loop = setInterval(tryToLoadFont, 500)
})
}
/**
* Load an array of promises
*
* @param {array} fonts - An array of objects of all the fonts to load
* [{ <fontName>, <fontStyle>, <fontWeight> }]
* @return {function} Callback
*/
const fontsLoader = async (fonts, callback) => {
if (!fonts.length) {
return callback?.()
}
const fontFaceObservers = []
let observer
fonts.forEach((font) => {
observer = loadFont(font.name, font.style, font.weight)
fontFaceObservers.push(observer)
})
try {
await Promise.all(fontFaceObservers)
callback?.()
} catch (e) {
console.warn('Some critical font are not available:', e)
}
}
/**
* Load the given stylesheet
*
* @param {DOMElement} $styleSheet - The stylesheet element
* @return {function} Promise
*/
const loadStylesheet = $styleSheet => {
return new Promise(resolve => {
let loop = null
const clearLoop = () => {
if (loop) {
clearInterval(loop)
loop = null
}
}
const checkStyleSheetLoading = () => {
let hasLoaded = false
try {
hasLoaded = $styleSheet.isLoaded
} catch (e) {
console.warn(`Error with the styleSheet ${$styleSheet}`, e)
clearLoop()
resolve()
}
if (hasLoaded) {
console.info('This stylesheet is loaded', $styleSheet)
clearLoop()
resolve()
}
}
loop = setInterval(checkStyleSheetLoading, 100)
})
}
/**
* Load an array of stylesheets
*
* @param {array} $styleSheets - An array of DOMElements of all the stylesheets to load
* @return {function} Callback
*/
const styleSheetsLoader = async (styleSheets, callback) => {
if (!styleSheets.length) {
console.log('Uh oh ! You need to select a <link> element')
return callback?.()
}
const styleSheetObservers = []
let observer
let $styleSheet
styleSheets.forEach(styleSheet => {
$styleSheet = document.querySelector(styleSheet)
if(typeof $styleSheet !== undefined) {
observer = loadStylesheet($styleSheet)
styleSheetObservers.push(observer)
}
})
try {
await Promise.all(styleSheetObservers)
callback?.()
} catch (e) {
console.warn('Some critical font are not available:', e)
}
}
/**
* Load the given image
*
* @param {string} url - The URI to lazy load into $el.
* @param {object} options - An object of options
* @return {void}
*/
const LOADED_IMAGES = []
const loadImage = (url, options = {}) => {
return new Promise((resolve, reject) => {
let loadedImage = LOADED_IMAGES.find(image => image.url === url)
if (loadedImage) {
resolve({
...loadedImage,
...options
})
} else {
const $img = new Image()
if (options.crossOrigin) {
$img.crossOrigin = options.crossOrigin
}
const loadCallback = () => {
const result = {
element: $img,
...getImageMetadata($img),
...options
}
LOADED_IMAGES.push(result)
resolve(result)
}
if ($img.decode) {
$img.src = url
$img.decode().then(loadCallback).catch(e => {
reject(e)
})
} else {
$img.onload = loadCallback
$img.onerror = (e) => {
reject(e)
}
$img.src = url
}
}
})
}
/**
* Lazy load the given image
*
* @param {HTMLImageElement} $el - The image element.
* @param {?string} url - The URI to lazy load into $el.
* If falsey, the value of the `data-src` attribute on $el will be used as the URI.
* @param {?function} callback - A function to call when the image is loaded.
*/
const LAZY_LOADED_IMAGES = []
const lazyLoadImage = async ($el, url, callback) => {
let src = url ? url : $el.dataset.src
let loadedImage = LAZY_LOADED_IMAGES.find(image => image.url === src)
if (!loadedImage) {
loadedImage = await loadImage(src)
if (!loadedImage.url) {
return
}
LAZY_LOADED_IMAGES.push(loadedImage)
}
if ($el.src === src) {
return
}
if ($el.tagName === 'IMG') {
$el.src = loadedImage.url
} else {
$el.style.backgroundImage = `url(${loadedImage.url})`
}
requestAnimationFrame(() => {
let lazyParent = $el.closest(config.SELECTORS.IMAGE_LAZY)
if (lazyParent) {
lazyParent.classList.add(config.CSS_CLASS.LAZY_LOADED)
lazyParent.style.backgroundImage = ''
}
$el.classList.add(config.CSS_CLASS.LAZY_LOADED)
callback?.()
})
}
/**
* Preload images that contains data-preload attribute
*
* @param {?function} callback - A function to call when all images are loaded.
*/
const preloadImages = (selector = config.SELECTORS.IMAGE_PRELOAD, callback) => {
const $imagesToLoad = document.querySelectorAll(selector)
if (!$imagesToLoad.length) {
callback?.()
return
}
const promises = []
$imagesToLoad.forEach($image => {
const url = $image.dataset.src
const promise = lazyLoadImage($image, url)
promises.push(promise)
})
Promise.all(promises).then(() => {
callback?.()
})
}
export {
loadFont,
fontsLoader,
loadStylesheet,
styleSheetsLoader,
loadImage,
lazyLoadImage,
preloadImages,
}

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