mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
4 Commits
d593fe5409
...
feature/js
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc8ff1f4a4 | ||
|
|
f714cd34e2 | ||
|
|
d70ff62294 | ||
|
|
110fb2e09b |
@@ -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
25
assets/scripts/config.js
Normal 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'
|
||||
}
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
298
assets/scripts/utils/loaders.js
Normal file
298
assets/scripts/utils/loaders.js
Normal 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
Reference in New Issue
Block a user