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

1 Commits

Author SHA1 Message Date
Pier-Luc Cossette
3e12fc31f0 Add natif lazy-load 2023-08-03 13:42:36 -04:00
14 changed files with 4196 additions and 163 deletions

View File

@@ -1,3 +1,3 @@
{ {
"version": 1690813628192 "version": 1691082391092
} }

View File

@@ -29,9 +29,10 @@ const CSS_CLASS = Object.freeze({
LOADED: 'is-loaded', LOADED: 'is-loaded',
READY: 'is-ready', READY: 'is-ready',
FONTS_LOADED: 'fonts-loaded', FONTS_LOADED: 'fonts-loaded',
LAZY_CONTAINER: 'c-lazy', IMAGE: "c-image",
LAZY_LOADED: '-lazy-loaded', IMAGE_LAZY_LOADED: "-lazy-loaded",
// ... IMAGE_LAZY_LOADING: "-lazy-loading",
IMAGE_LAZY_ERROR: "-lazy-error",
}) })
// Custom js events // Custom js events

View File

@@ -1,5 +1,6 @@
import svg4everybody from 'svg4everybody'; import svg4everybody from 'svg4everybody';
import { ENV } from './config'; import { ENV } from './config';
import { triggerLazyloadCallbacks } from './utils/image';
// Dynamic imports for development mode only // Dynamic imports for development mode only
let gridHelper; let gridHelper;
@@ -20,4 +21,9 @@ export default function () {
* Add grid helper * Add grid helper
*/ */
gridHelper?.(); gridHelper?.();
/**
* Trigger lazyload
*/
triggerLazyloadCallbacks();
} }

View File

@@ -1,5 +1,6 @@
import { module } from 'modujs'; import { module } from 'modujs';
import modularLoad from 'modularload'; import modularLoad from 'modularload';
import { resetLazyloadCallbacks, triggerLazyloadCallbacks } from "../utils/image";
export default class extends module { export default class extends module {
constructor(m) { constructor(m) {
@@ -7,16 +8,28 @@ export default class extends module {
} }
init() { init() {
const load = new modularLoad({ this.load = new modularLoad({
enterDelay: 0, enterDelay: 0,
transitions: { transitions: {
customTransition: {} customTransition: {}
} }
}); });
load.on('loaded', (transition, oldContainer, newContainer) => { this.load.on('loaded', (transition, oldContainer, newContainer) => {
this.call('destroy', oldContainer, 'app'); this.call('destroy', oldContainer, 'app');
this.call('update', newContainer, 'app'); this.call('update', newContainer, 'app');
/**
* Trigger lazyload
*/
triggerLazyloadCallbacks();
});
this.load.on("loading", () => {
/**
* Remove previous lazyload callbacks
*/
resetLazyloadCallbacks();
}); });
} }
} }

View File

@@ -1,5 +1,4 @@
import { module } from 'modujs' import { module } from 'modujs'
import { lazyLoadImage } from '../utils/image'
import LocomotiveScroll from 'locomotive-scroll' import LocomotiveScroll from 'locomotive-scroll'
export default class extends module { export default class extends module {
@@ -19,29 +18,6 @@ export default class extends module {
// } // }
} }
/**
* Lazy load the related image.
*
* @see ../utils/image.js
*
* It is recommended to wrap your `<img>` into an element with the
* CSS class name `.c-lazy`. The CSS class name modifier `.-lazy-loaded`
* will be applied on both the image and the parent wrapper.
*
* ```html
* <div class="c-lazy o-ratio u-4:3">
* <img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/640/480?v=1" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
* </div>
* ```
*
* @param {LocomotiveScroll} args - The Locomotive Scroll instance.
*/
lazyLoad(args) {
lazyLoadImage(args.target, null, () => {
//callback
})
}
scrollTo(params) { scrollTo(params) {
let { target, ...options } = params let { target, ...options } = params
@@ -53,6 +29,24 @@ export default class extends module {
this.scroll?.scrollTo(target, options) this.scroll?.scrollTo(target, options)
} }
/**
* Observe new scroll elements
*
* @param $newContainer (HTMLElement)
*/
addScrollElements($newContainer) {
this.scroll?.addScrollElements($newContainer)
}
/**
* Unobserve scroll elements
*
* @param $oldContainer (HTMLElement)
*/
removeScrollElements($oldContainer) {
this.scroll?.removeScrollElements($oldContainer)
}
destroy() { destroy() {
this.scroll.destroy(); this.scroll.destroy();
} }

View File

@@ -4,15 +4,18 @@
* @return {string} escaped string * @return {string} escaped string
*/ */
const escapeHtml = str => const escapeHtml = (str) =>
str.replace(/[&<>'"]/g, tag => ({ str.replace(
'&': '&amp;', /[&<>'"]/g,
'<': '&lt;', (tag) =>
'>': '&gt;', ({
"'": '&#39;', "&": "&amp;",
'"': '&quot;' "<": "&lt;",
}[tag])) ">": "&gt;",
"'": "&#39;",
'"': "&quot;",
}[tag])
);
/** /**
* Unescape HTML string * Unescape HTML string
@@ -20,13 +23,13 @@ const escapeHtml = str =>
* @return {string} unescaped string * @return {string} unescaped string
*/ */
const unescapeHtml = str => const unescapeHtml = (str) =>
str.replace('&amp;', '&') str
.replace('&lt;', '<') .replace("&amp;", "&")
.replace('&gt;', '>') .replace("&lt;", "<")
.replace('&#39;', "'") .replace("&gt;", ">")
.replace('&quot;', '"') .replace("&#39;", "'")
.replace("&quot;", '"');
/** /**
* Get element data attributes * Get element data attributes
@@ -34,46 +37,41 @@ const unescapeHtml = str =>
* @return {array} node data * @return {array} node data
*/ */
const getNodeData = node => { const getNodeData = (node) => {
// All attributes // All attributes
const attributes = node.attributes const attributes = node.attributes;
// Regex Pattern // Regex Pattern
const pattern = /^data\-(.+)$/ const pattern = /^data\-(.+)$/;
// Output // Output
const data = {} const data = {};
for (let i in attributes) { for (let i in attributes) {
if (!attributes[i]) { if (!attributes[i]) {
continue continue;
} }
// Attributes name (ex: data-module) // Attributes name (ex: data-module)
let name = attributes[i].name let name = attributes[i].name;
// This happens. // This happens.
if (!name) { if (!name) {
continue continue;
} }
let match = name.match(pattern) let match = name.match(pattern);
if (!match) { if (!match) {
continue continue;
} }
// If this throws an error, you have some // If this throws an error, you have some
// serious problems in your HTML. // serious problems in your HTML.
data[match[1]] = getData(node.getAttribute(name)) data[match[1]] = getData(node.getAttribute(name));
} }
return data; return data;
};
}
/** /**
* Parse value to data type. * Parse value to data type.
@@ -83,32 +81,31 @@ const getNodeData = node => {
* @return {mixed} value in its natural data type * @return {mixed} value in its natural data type
*/ */
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/ const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/;
const getData = data => { const getData = (data) => {
if (data === 'true') { if (data === "true") {
return true return true;
} }
if (data === 'false') { if (data === "false") {
return false return false;
} }
if (data === 'null') { if (data === "null") {
return null return null;
} }
// Only convert to a number if it doesn't change the string // Only convert to a number if it doesn't change the string
if (data === +data+'') { if (data === +data + "") {
return +data return +data;
} }
if (rbrace.test(data)) { if (rbrace.test(data)) {
return JSON.parse(data) return JSON.parse(data);
} }
return data return data;
} };
/** /**
* Returns an array containing all the parent nodes of the given node * Returns an array containing all the parent nodes of the given node
@@ -116,20 +113,45 @@ const getData = data => {
* @return {array} parent nodes * @return {array} parent nodes
*/ */
const getParents = $el => { const getParents = ($el) => {
// Set up a parent array // Set up a parent array
let parents = [] let parents = [];
// Push each parent element to the array // Push each parent element to the array
for (; $el && $el !== document; $el = $el.parentNode) { for (; $el && $el !== document; $el = $el.parentNode) {
parents.push($el) parents.push($el);
} }
// Return our parent array // Return our parent array
return parents return parents;
} };
// https://gomakethings.com/how-to-get-the-closest-parent-element-with-a-matching-selector-using-vanilla-javascript/
const queryClosestParent = ($el, selector) => {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function (s) {
var matches = (
this.document || this.ownerDocument
).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Get the closest matching element
for (; $el && $el !== document; $el = $el.parentNode) {
if ($el.matches(selector)) return $el;
}
return null;
};
export { export {
escapeHtml, escapeHtml,
@@ -137,4 +159,5 @@ export {
getNodeData, getNodeData,
getData, getData,
getParents, getParents,
} queryClosestParent,
};

View File

@@ -1,4 +1,5 @@
import { CSS_CLASS } from '../config' import { CSS_CLASS } from '../config'
import { queryClosestParent } from './html'
/** /**
* Get an image meta data * Get an image meta data
@@ -91,22 +92,111 @@ const lazyLoadImage = async ($el, url, callback) => {
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`) let lazyParent = $el.closest(`.${CSS_CLASS.IMAGE}`)
if(lazyParent) { if(lazyParent) {
lazyParent.classList.add(CSS_CLASS.LAZY_LOADED) lazyParent.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED)
lazyParent.style.backgroundImage = '' lazyParent.style.backgroundImage = ''
} }
$el.classList.add(CSS_CLASS.LAZY_LOADED) $el.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED)
callback?.() callback?.()
}) })
} }
/**
* Lazyload Callbacks
*
*/
const lazyImageLoad = (e) => {
const $img = e.currentTarget;
const $parent = queryClosestParent($img, `.${CSS_CLASS.IMAGE}`);
requestAnimationFrame(() => {
if ($parent) {
$parent.classList.remove(CSS_CLASS.IMAGE_LAZY_LOADING);
$parent.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED);
}
$img.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED);
});
};
const lazyImageError = (e) => {
const $img = e.currentTarget;
const $parent = queryClosestParent($img, `.${CSS_CLASS.IMAGE}`);
requestAnimationFrame(() => {
if ($parent) {
$parent.classList.remove(CSS_CLASS.IMAGE_LAZY_LOADING);
$parent.classList.add(CSS_CLASS.IMAGE_LAZY_ERROR);
}
});
};
/* Trigger Lazyload Callbacks */
const triggerLazyloadCallbacks = ($lazyImagesArgs) => {
const $lazyImages = $lazyImagesArgs
? $lazyImagesArgs
: document.querySelectorAll('[loading="lazy"]');
if ("loading" in HTMLImageElement.prototype) {
for (const $img of $lazyImages) {
const $parent = queryClosestParent(
$img,
`.${CSS_CLASS.IMAGE}`
);
if (!$img.complete) {
if($parent) {
$parent.classList.add(
CSS_CLASS.IMAGE_LAZY_LOADING
);
}
$img.addEventListener("load", lazyImageLoad, { once: true });
$img.addEventListener("error", lazyImageError, { once: true });
} else {
if (!$img.complete) {
$parent.classList.add(
CSS_CLASS.IMAGE_LAZY_LOADED
);
}
}
}
} else {
// if 'loading' supported
for (const $img of $lazyImages) {
const $parent = queryClosestParent(
$img,
`.${CSS_CLASS.IMAGE}`
);
if($parent) {
$parent.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED);
}
}
}
};
/* Reset Lazyload Callbacks */
const resetLazyloadCallbacks = () => {
if ("loading" in HTMLImageElement.prototype) {
const $lazyImages = document.querySelectorAll('[loading="lazy"]');
for (const $img of $lazyImages) {
$img.removeEventListener("load", lazyImageLoad, { once: true });
$img.removeEventListener("error", lazyImageError, { once: true });
}
}
};
export { export {
getImageMetadata, getImageMetadata,
loadImage, loadImage,
lazyLoadImage lazyLoadImage,
triggerLazyloadCallbacks,
resetLazyloadCallbacks
} }

View File

@@ -0,0 +1,20 @@
// ==========================================================================
// Components / Image
// ==========================================================================
.c-image {
}
.c-image_img {
// Lazy loading styles
.c-image.-lazy-load & {
transition: opacity $speed $easing;
opacity: 0;
}
.c-image.-lazy-loaded & {
opacity: 1;
}
}

View File

@@ -57,6 +57,7 @@
@import "components/heading"; @import "components/heading";
@import "components/button"; @import "components/button";
@import "components/form"; @import "components/form";
@import "components/image";
// Utilities // Utilities
// ========================================================================== // ==========================================================================

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

View File

@@ -39,78 +39,18 @@
<h3 class="c-heading -h3">Basic</h3> <h3 class="c-heading -h3">Basic</h3>
<div style="width: 640px; max-width: 100%;"> <img src="http://picsum.photos/800/600?v=1" alt="" loading="lazy" class="c-image_img" width="800" height="600"/>
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/800/600?v=1" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/800/600?v=2" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
</div>
<h4 class="c-heading -h3">Using o-ratio & background-image</h3>
<div style="width: 480px; max-width: 100%;">
<div class="o-ratio u-16:9" data-load-style="background-size: cover; background-position: center; background-image: url(http://picsum.photos/640/480?v=1);"></div>
<div class="o-ratio u-16:9" data-load-style="background-size: cover; background-position: center; background-image: url(http://picsum.photos/640/480?v=2);"></div>
</div>
</section>
<section>
<h3 class="c-heading -h3">Relative to scroll</h3>
<h4 class="c-heading -h3">Using o-ratio & img</h3>
<div style="width: 640px; max-width: 100%;"> <div style="width: 640px; max-width: 100%;">
<div class="o-ratio u-4:3"> <div class="c-image"><img src="http://picsum.photos/800/600?v=2" alt="" loading="lazy" class="c-image_img" width="800" height="600"/></div>
<img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/800/600?v=1" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /> <div class="c-image"><img src="http://picsum.photos/800/600?v=3" alt="" loading="lazy" class="c-image_img" width="800" height="600"/></div>
</div>
<div class="o-ratio u-4:3">
<img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/800/600?v=2" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
</div>
<div class="o-ratio u-4:3">
<img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/800/600?v=3" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
</div>
<div class="o-ratio u-4:3">
<img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/800/600?v=4" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
</div>
<div class="o-ratio u-4:3">
<img data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/800/600?v=5" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
</div>
</div> </div>
<h4 class="c-heading -h3">Using o-ratio & background-image</h3> <h4 class="c-heading -h3">Using o-ratio</h3>
<div style="width: 480px; max-width: 100%;"> <div style="width: 480px; max-width: 100%;">
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/1280/720?v=1"></div> <div class="o-ratio u-4:3"><div class="c-image || o-ratio_content"><img src="http://picsum.photos/800/600?v=4" alt="" loading="lazy" class="c-image_img"/></div></div>
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/1280/720?v=2"></div> <div class="o-ratio u-4:3"><div class="c-image || o-ratio_content"><img src="http://picsum.photos/800/600?v=5" alt="" loading="lazy" class="c-image_img"/></div></div>
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/1280/720?v=3"></div>
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/1280/720?v=4"></div>
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/1280/720?v=5"></div>
</div>
<h4 class="c-heading -h3">Using SVG viewport for ratio</h3>
<div style="width: 480px; max-width: 100%;">
<img
data-scroll
data-scroll-call="lazyLoad, Scroll, main"
data-src="http://picsum.photos/640/480?v=6"
alt=""
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3C/svg%3E"
/>
<img
data-scroll
data-scroll-call="lazyLoad, Scroll, main"
data-src="http://picsum.photos/640/480?v=7"
alt=""
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3C/svg%3E"
/>
<img
data-scroll
data-scroll-call="lazyLoad, Scroll, main"
data-src="http://picsum.photos/640/480?v=8"
alt=""
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3C/svg%3E"
/>
</div> </div>
</section> </section>
</div> </div>