mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
1 Commits
mcaskill/b
...
feature/la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e12fc31f0 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": 1690813628192
|
||||
"version": 1691082391092
|
||||
}
|
||||
@@ -29,9 +29,10 @@ const CSS_CLASS = Object.freeze({
|
||||
LOADED: 'is-loaded',
|
||||
READY: 'is-ready',
|
||||
FONTS_LOADED: 'fonts-loaded',
|
||||
LAZY_CONTAINER: 'c-lazy',
|
||||
LAZY_LOADED: '-lazy-loaded',
|
||||
// ...
|
||||
IMAGE: "c-image",
|
||||
IMAGE_LAZY_LOADED: "-lazy-loaded",
|
||||
IMAGE_LAZY_LOADING: "-lazy-loading",
|
||||
IMAGE_LAZY_ERROR: "-lazy-error",
|
||||
})
|
||||
|
||||
// Custom js events
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import svg4everybody from 'svg4everybody';
|
||||
import { ENV } from './config';
|
||||
import { triggerLazyloadCallbacks } from './utils/image';
|
||||
|
||||
// Dynamic imports for development mode only
|
||||
let gridHelper;
|
||||
@@ -20,4 +21,9 @@ export default function () {
|
||||
* Add grid helper
|
||||
*/
|
||||
gridHelper?.();
|
||||
|
||||
/**
|
||||
* Trigger lazyload
|
||||
*/
|
||||
triggerLazyloadCallbacks();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { module } from 'modujs';
|
||||
import modularLoad from 'modularload';
|
||||
import { resetLazyloadCallbacks, triggerLazyloadCallbacks } from "../utils/image";
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
@@ -7,16 +8,28 @@ export default class extends module {
|
||||
}
|
||||
|
||||
init() {
|
||||
const load = new modularLoad({
|
||||
this.load = new modularLoad({
|
||||
enterDelay: 0,
|
||||
transitions: {
|
||||
customTransition: {}
|
||||
}
|
||||
});
|
||||
|
||||
load.on('loaded', (transition, oldContainer, newContainer) => {
|
||||
this.load.on('loaded', (transition, oldContainer, newContainer) => {
|
||||
this.call('destroy', oldContainer, 'app');
|
||||
this.call('update', newContainer, 'app');
|
||||
|
||||
/**
|
||||
* Trigger lazyload
|
||||
*/
|
||||
triggerLazyloadCallbacks();
|
||||
});
|
||||
|
||||
this.load.on("loading", () => {
|
||||
/**
|
||||
* Remove previous lazyload callbacks
|
||||
*/
|
||||
resetLazyloadCallbacks();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { module } from 'modujs'
|
||||
import { lazyLoadImage } from '../utils/image'
|
||||
import LocomotiveScroll from 'locomotive-scroll'
|
||||
|
||||
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) {
|
||||
let { target, ...options } = params
|
||||
|
||||
@@ -53,6 +29,24 @@ export default class extends module {
|
||||
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() {
|
||||
this.scroll.destroy();
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
* @return {string} escaped string
|
||||
*/
|
||||
|
||||
const escapeHtml = str =>
|
||||
str.replace(/[&<>'"]/g, tag => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'
|
||||
}[tag]))
|
||||
|
||||
const escapeHtml = (str) =>
|
||||
str.replace(
|
||||
/[&<>'"]/g,
|
||||
(tag) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"'": "'",
|
||||
'"': """,
|
||||
}[tag])
|
||||
);
|
||||
|
||||
/**
|
||||
* Unescape HTML string
|
||||
@@ -20,13 +23,13 @@ const escapeHtml = str =>
|
||||
* @return {string} unescaped string
|
||||
*/
|
||||
|
||||
const unescapeHtml = str =>
|
||||
str.replace('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
.replace(''', "'")
|
||||
.replace('"', '"')
|
||||
|
||||
const unescapeHtml = (str) =>
|
||||
str
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("'", "'")
|
||||
.replace(""", '"');
|
||||
|
||||
/**
|
||||
* Get element data attributes
|
||||
@@ -34,46 +37,41 @@ const unescapeHtml = str =>
|
||||
* @return {array} node data
|
||||
*/
|
||||
|
||||
const getNodeData = node => {
|
||||
|
||||
const getNodeData = (node) => {
|
||||
// All attributes
|
||||
const attributes = node.attributes
|
||||
const attributes = node.attributes;
|
||||
|
||||
// Regex Pattern
|
||||
const pattern = /^data\-(.+)$/
|
||||
const pattern = /^data\-(.+)$/;
|
||||
|
||||
// Output
|
||||
const data = {}
|
||||
const data = {};
|
||||
|
||||
for (let i in attributes) {
|
||||
if (!attributes[i]) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attributes name (ex: data-module)
|
||||
let name = attributes[i].name
|
||||
let name = attributes[i].name;
|
||||
|
||||
// This happens.
|
||||
if (!name) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
let match = name.match(pattern)
|
||||
let match = name.match(pattern);
|
||||
if (!match) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this throws an error, you have some
|
||||
// serious problems in your HTML.
|
||||
data[match[1]] = getData(node.getAttribute(name))
|
||||
data[match[1]] = getData(node.getAttribute(name));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse value to data type.
|
||||
@@ -83,32 +81,31 @@ const getNodeData = node => {
|
||||
* @return {mixed} value in its natural data type
|
||||
*/
|
||||
|
||||
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/
|
||||
const getData = data => {
|
||||
if (data === 'true') {
|
||||
return true
|
||||
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/;
|
||||
const getData = (data) => {
|
||||
if (data === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data === 'false') {
|
||||
return false
|
||||
if (data === "false") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data === 'null') {
|
||||
return null
|
||||
if (data === "null") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only convert to a number if it doesn't change the string
|
||||
if (data === +data+'') {
|
||||
return +data
|
||||
if (data === +data + "") {
|
||||
return +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
|
||||
@@ -116,20 +113,45 @@ const getData = data => {
|
||||
* @return {array} parent nodes
|
||||
*/
|
||||
|
||||
const getParents = $el => {
|
||||
|
||||
const getParents = ($el) => {
|
||||
// Set up a parent array
|
||||
let parents = []
|
||||
let parents = [];
|
||||
|
||||
// Push each parent element to the array
|
||||
for (; $el && $el !== document; $el = $el.parentNode) {
|
||||
parents.push($el)
|
||||
parents.push($el);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
escapeHtml,
|
||||
@@ -137,4 +159,5 @@ export {
|
||||
getNodeData,
|
||||
getData,
|
||||
getParents,
|
||||
}
|
||||
queryClosestParent,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CSS_CLASS } from '../config'
|
||||
import { queryClosestParent } from './html'
|
||||
|
||||
/**
|
||||
* Get an image meta data
|
||||
@@ -91,22 +92,111 @@ const lazyLoadImage = async ($el, url, callback) => {
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`)
|
||||
let lazyParent = $el.closest(`.${CSS_CLASS.IMAGE}`)
|
||||
|
||||
if(lazyParent) {
|
||||
lazyParent.classList.add(CSS_CLASS.LAZY_LOADED)
|
||||
lazyParent.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED)
|
||||
lazyParent.style.backgroundImage = ''
|
||||
}
|
||||
|
||||
$el.classList.add(CSS_CLASS.LAZY_LOADED)
|
||||
$el.classList.add(CSS_CLASS.IMAGE_LAZY_LOADED)
|
||||
|
||||
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 {
|
||||
getImageMetadata,
|
||||
loadImage,
|
||||
lazyLoadImage
|
||||
lazyLoadImage,
|
||||
triggerLazyloadCallbacks,
|
||||
resetLazyloadCallbacks
|
||||
}
|
||||
|
||||
20
assets/styles/components/_image.scss
Normal file
20
assets/styles/components/_image.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
@import "components/heading";
|
||||
@import "components/button";
|
||||
@import "components/form";
|
||||
@import "components/image";
|
||||
|
||||
// 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
@@ -39,78 +39,18 @@
|
||||
|
||||
<h3 class="c-heading -h3">Basic</h3>
|
||||
|
||||
<div style="width: 640px; max-width: 100%;">
|
||||
<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>
|
||||
<img src="http://picsum.photos/800/600?v=1" alt="" loading="lazy" class="c-image_img" width="800" height="600"/>
|
||||
|
||||
<div style="width: 640px; max-width: 100%;">
|
||||
<div class="o-ratio u-4:3">
|
||||
<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>
|
||||
<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 class="c-image"><img src="http://picsum.photos/800/600?v=2" alt="" loading="lazy" class="c-image_img" width="800" height="600"/></div>
|
||||
<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>
|
||||
|
||||
<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="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 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 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 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 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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user