♻️ - Suppression de la classe JustifiedGallery et ajout de nouveaux effets de survol pour les images
This commit is contained in:
659
JS/JustifedGallery/assets/js/lib/gallery/gallery.js
Normal file
659
JS/JustifedGallery/assets/js/lib/gallery/gallery.js
Normal file
@@ -0,0 +1,659 @@
|
||||
/**
|
||||
* JustifiedGallery
|
||||
*
|
||||
* A responsive justified image gallery with smooth transitions, modal preview,
|
||||
* custom item styles, and dynamic hover effects.
|
||||
*
|
||||
* Features:
|
||||
* - Responsive columns & max row height (with breakpoints)
|
||||
* - Optional modal (zoomable, draggable, navigable)
|
||||
* - Configurable hover effects (Lens, Ripple, Spotlight, etc.)
|
||||
* - Custom per-item inline styles via `itemStyle`
|
||||
* - Smooth animated layout transitions when column count changes (FLIP via overlay)
|
||||
*
|
||||
* Author: Aranéite (Antoine)
|
||||
* Version: 2.4
|
||||
*/
|
||||
|
||||
export default class JustifiedGallery {
|
||||
/* =========================================================
|
||||
* Constructor & public state
|
||||
* ======================================================= */
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container - The gallery container element.
|
||||
* @param {Object} options - Configuration options.
|
||||
*/
|
||||
constructor(container, options = {}) {
|
||||
if (!container) return;
|
||||
|
||||
this.container = container;
|
||||
this.images = [];
|
||||
this.imagesClone = [];
|
||||
this.currentIndex = 0;
|
||||
this.isOpenModal = false;
|
||||
this.currentCols = null; // track current column count for breakpoint animation
|
||||
|
||||
this.options = {
|
||||
margin: options.margin ?? 5,
|
||||
columns: options.columns ?? 5,
|
||||
breakpoints: options.breakpoints ?? { 1248: 3, 768: 2, 480: 1 },
|
||||
hasModal: options.hasModal ?? false,
|
||||
linkAttribute: options.linkAttribute || 'src',
|
||||
justifyLastRow: options.justifyLastRow ?? false,
|
||||
hoverEffect: options.hoverEffect || null,
|
||||
itemStyle: options.itemStyle || {},
|
||||
|
||||
maxRowHeight: options.maxRowHeight ?? 350,
|
||||
maxRowHeightBreakpoints: options.maxRowHeightBreakpoints ?? {
|
||||
1248: 300,
|
||||
768: 250,
|
||||
480: 200,
|
||||
},
|
||||
};
|
||||
|
||||
// Initialisation
|
||||
this.init();
|
||||
|
||||
// Handle responsive relayout on resize
|
||||
this._onResize = () => {
|
||||
if (!this.container) return;
|
||||
|
||||
const newCols = this._getColumns();
|
||||
const shouldAnimateWrap =
|
||||
this.currentCols !== null && newCols !== this.currentCols;
|
||||
|
||||
this.currentCols = newCols;
|
||||
this.layoutGallery({ animateWrap: shouldAnimateWrap });
|
||||
};
|
||||
|
||||
window.addEventListener('resize', this._onResize);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Initialization
|
||||
* ======================================================= */
|
||||
|
||||
/**
|
||||
* Initialize gallery:
|
||||
* - wrap images in anchors
|
||||
* - wait for load
|
||||
* - initial layout
|
||||
* - modal / hover / custom styles
|
||||
*/
|
||||
async init() {
|
||||
// Start hidden → fade-in after first layout
|
||||
this.container.style.opacity = '0';
|
||||
|
||||
// Initial raw images
|
||||
this.images = this._getImages();
|
||||
this.imagesClone = this._getImagesClones();
|
||||
|
||||
// Wrap each image in a link (<a>) and attach hover class
|
||||
this.images.forEach(img => this._wrapImage(img));
|
||||
|
||||
// Refresh internal image list (now wrapped by <a>)
|
||||
this.images = this._getImages();
|
||||
|
||||
// Wait for all images to be loaded (or failed)
|
||||
const promises = this.images.map(img => this._waitForImageLoad(img));
|
||||
await Promise.all(promises);
|
||||
|
||||
// First layout (no animation on initial render)
|
||||
this.currentCols = this._getColumns();
|
||||
this.layoutGallery({ animateWrap: false });
|
||||
|
||||
// Modal, hover effects, and custom item styles
|
||||
this.setupModal();
|
||||
this.initHoverEffects();
|
||||
this.applyItemStyles();
|
||||
|
||||
// Smooth fade-in
|
||||
requestAnimationFrame(() => {
|
||||
this.container.style.transition = 'opacity 0.6s ease';
|
||||
this.container.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom styles defined in options.itemStyle
|
||||
* to each gallery item (link wrapper).
|
||||
*/
|
||||
applyItemStyles() {
|
||||
this.images.forEach(img => {
|
||||
const wrapper = img.parentNode;
|
||||
for (const key in this.options.itemStyle) {
|
||||
wrapper.style[key] = this.options.itemStyle[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hover effects (spotlight, ripple, lens...)
|
||||
* once DOM is ready and images are wrapped.
|
||||
*/
|
||||
initHoverEffects() {
|
||||
const effect = this.options.hoverEffect;
|
||||
|
||||
// Spotlight effect (dynamic import)
|
||||
if (effect === 'hover-spotlight') {
|
||||
import('./effects/spotlight.js')
|
||||
.then(({ enableSpotlightEffect }) => {
|
||||
enableSpotlightEffect(this.container);
|
||||
})
|
||||
}
|
||||
|
||||
// Ripple effect: radial animation on mouse enter
|
||||
if (effect === 'hover-ripple-animate') {
|
||||
import('./effects/ripple-animate.js')
|
||||
.then(({ enableRippleEffect }) => {
|
||||
enableRippleEffect(this.container);
|
||||
})
|
||||
}
|
||||
|
||||
// Lens effect: zoomed circular region following the cursor
|
||||
if (effect === 'hover-lens') {
|
||||
import('./effects/lens.js')
|
||||
.then(({ enableLensEffect }) => {
|
||||
enableLensEffect(this.container);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up listeners and DOM references.
|
||||
*/
|
||||
destroy() {
|
||||
window.removeEventListener('resize', this._onResize);
|
||||
|
||||
if (this.modal && document.body.contains(this.modal)) {
|
||||
this.modal.remove();
|
||||
}
|
||||
|
||||
// Remove any listeners on images by cloning them
|
||||
if (this.images && this.images.length) {
|
||||
this.images.forEach(img => {
|
||||
const clone = img.cloneNode(true);
|
||||
img.replaceWith(clone);
|
||||
});
|
||||
}
|
||||
|
||||
this.container = null;
|
||||
this.images = null;
|
||||
this.modal = null;
|
||||
this.modalImg = null;
|
||||
this.closeBtn = null;
|
||||
this.prevBtn = null;
|
||||
this.nextBtn = null;
|
||||
this.isOpenModal = false;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Layout computation & application
|
||||
* ======================================================= */
|
||||
|
||||
/**
|
||||
* Compute layout data for all rows without mutating the DOM.
|
||||
*
|
||||
* @returns {Array<{ item: HTMLElement, img: HTMLImageElement,
|
||||
* width: number, height: number,
|
||||
* marginRight: number, marginBottom: number }>}
|
||||
*/
|
||||
_computeLayout() {
|
||||
const containerWidth = this.container.clientWidth;
|
||||
const margin = this.options.margin;
|
||||
const cols = this._getColumns();
|
||||
const maxRowHeight = this._getMaxRowHeight();
|
||||
const justifyLastRow = this.options.justifyLastRow;
|
||||
|
||||
const layout = [];
|
||||
let rowImages = [];
|
||||
let totalRatio = 0;
|
||||
|
||||
this.images.forEach((img, index) => {
|
||||
const ratio = img.naturalWidth / img.naturalHeight || 1;
|
||||
const item = img.parentNode;
|
||||
|
||||
rowImages.push({ item, img, ratio });
|
||||
totalRatio += ratio;
|
||||
|
||||
const isLastRow = index === this.images.length - 1;
|
||||
|
||||
if (rowImages.length === cols || isLastRow) {
|
||||
let rowHeight;
|
||||
|
||||
if (isLastRow && rowImages.length < cols && !justifyLastRow) {
|
||||
// Do not stretch last row: use average ratio and target less height
|
||||
const avgRatio = totalRatio / rowImages.length;
|
||||
rowHeight = Math.floor((containerWidth / cols) / avgRatio);
|
||||
rowHeight = Math.min(rowHeight, maxRowHeight);
|
||||
} else {
|
||||
// Normal justified row: fill container width
|
||||
rowHeight = Math.floor(
|
||||
(containerWidth - margin * (rowImages.length - 1)) / totalRatio
|
||||
);
|
||||
rowHeight = Math.min(rowHeight, maxRowHeight);
|
||||
|
||||
// Stretch to perfectly fill the row horizontally
|
||||
const totalWidth =
|
||||
rowImages.reduce((sum, r) => sum + rowHeight * r.ratio, 0) +
|
||||
margin * (rowImages.length - 1);
|
||||
|
||||
if (totalWidth > 0) {
|
||||
const stretchFactor = containerWidth / totalWidth;
|
||||
rowHeight *= stretchFactor;
|
||||
}
|
||||
}
|
||||
|
||||
rowImages.forEach((r, idx) => {
|
||||
const width = rowHeight * r.ratio;
|
||||
layout.push({
|
||||
item: r.item,
|
||||
img: r.img,
|
||||
width,
|
||||
height: rowHeight,
|
||||
marginRight: idx < rowImages.length - 1 ? margin : 0,
|
||||
marginBottom: margin,
|
||||
});
|
||||
});
|
||||
|
||||
rowImages = [];
|
||||
totalRatio = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Keep in sync with actual columns used
|
||||
this.currentCols = this._getColumns();
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply computed layout to DOM elements (without animation).
|
||||
* @param {Array} layout - Layout entries from _computeLayout().
|
||||
*/
|
||||
_applyLayout(layout) {
|
||||
layout.forEach(entry => {
|
||||
const { item, img, width, height, marginRight, marginBottom } = entry;
|
||||
|
||||
// Anchor <a> wrapper sized using computed layout
|
||||
item.style.width = `${width}px`;
|
||||
item.style.height = `${height}px`;
|
||||
item.style.display = 'inline-block';
|
||||
item.style.marginRight = `${marginRight}px`;
|
||||
item.style.marginBottom = `${marginBottom}px`;
|
||||
item.style.transition = 'none';
|
||||
|
||||
// Image fills its wrapper
|
||||
img.style.width = '100%';
|
||||
img.style.height = '100%';
|
||||
img.style.cursor = 'pointer';
|
||||
img.style.transition = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main layout function.
|
||||
* - If animateWrap = true, uses overlay + FLIP-like animation
|
||||
* when the number of columns changes (breakpoint hit).
|
||||
* - Otherwise only recomputes justification without wrap animation.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.animateWrap
|
||||
*/
|
||||
layoutGallery({ animateWrap = false } = {}) {
|
||||
if (!this.container || !this.images || !this.images.length) return;
|
||||
|
||||
const layout = this._computeLayout();
|
||||
|
||||
// Simple re-justification when column count did not change
|
||||
if (!animateWrap) {
|
||||
this._applyLayout(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------
|
||||
* Animated wrap change using fixed overlay + clones
|
||||
* ------------------------------------------- */
|
||||
|
||||
// 1) Capture current positions BEFORE applying new layout
|
||||
const prevRects = this._getWrapperRects();
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
|
||||
// 2) Create overlay (fixed) containing clones at old positions
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.left = `${containerRect.left}px`;
|
||||
overlay.style.top = `${containerRect.top}px`;
|
||||
overlay.style.width = `${containerRect.width}px`;
|
||||
overlay.style.height = `${containerRect.height}px`;
|
||||
overlay.style.pointerEvents = 'none';
|
||||
overlay.style.zIndex = '9999';
|
||||
|
||||
const wrappers = this.images.map(img => img.parentNode);
|
||||
const clones = [];
|
||||
|
||||
wrappers.forEach((wrapper, index) => {
|
||||
const r = prevRects[index];
|
||||
if (!r) return;
|
||||
|
||||
const clone = wrapper.cloneNode(true);
|
||||
|
||||
// Position & size BEFORE layout change
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.left = `${r.left - containerRect.left}px`;
|
||||
clone.style.top = `${r.top - containerRect.top}px`;
|
||||
clone.style.width = `${r.width}px`;
|
||||
clone.style.height = `${r.height}px`;
|
||||
clone.style.margin = '0';
|
||||
clone.style.boxSizing = 'border-box';
|
||||
clone.style.transition = 'none';
|
||||
|
||||
clones.push(clone);
|
||||
overlay.appendChild(clone);
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Hide real items during animation to avoid flicker/misalignment
|
||||
wrappers.forEach(w => {
|
||||
w.style.opacity = '0';
|
||||
});
|
||||
|
||||
// 3) Apply NEW layout (on the real gallery, but hidden)
|
||||
this._applyLayout(layout);
|
||||
|
||||
// 4) Measure final positions AFTER layout
|
||||
const newRects = wrappers.map(w => w.getBoundingClientRect());
|
||||
|
||||
// 5) Animate clones from old to new rects
|
||||
requestAnimationFrame(() => {
|
||||
clones.forEach((clone, index) => {
|
||||
const newRect = newRects[index];
|
||||
const final = layout[index]; // final width/height from layout
|
||||
if (!newRect || !final) return;
|
||||
|
||||
clone.style.transition = 'all 0.6s cubic-bezier(0.25, 1, 0.5, 1)';
|
||||
|
||||
// Final position
|
||||
clone.style.left = `${newRect.left - containerRect.left}px`;
|
||||
clone.style.top = `${newRect.top - containerRect.top }px`;
|
||||
|
||||
// Final size (should match real item => avoids jump)
|
||||
clone.style.width = `${newRect.width }px`;
|
||||
clone.style.height = `${newRect.height}px`;
|
||||
});
|
||||
});
|
||||
|
||||
// 6) Cleanup: remove overlay & reveal real gallery
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
wrappers.forEach(w => {
|
||||
w.style.opacity = '1';
|
||||
});
|
||||
}, 650);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Modal viewer
|
||||
* ======================================================= */
|
||||
|
||||
/**
|
||||
* Setup modal viewer if enabled.
|
||||
*/
|
||||
setupModal() {
|
||||
if (!this.options.hasModal) return;
|
||||
|
||||
this.modal = document.createElement('div');
|
||||
this.modal.classList.add('jg-modal');
|
||||
this.modal.innerHTML = `
|
||||
<span class="jg-close">×</span>
|
||||
<img class="jg-modal-img" src="">
|
||||
<div class="jg-controls">
|
||||
<span class="jg-prev">❮</span>
|
||||
<span class="jg-next">❯</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(this.modal);
|
||||
|
||||
this.modalImg = this.modal.querySelector('.jg-modal-img');
|
||||
this.closeBtn = this.modal.querySelector('.jg-close');
|
||||
this.prevBtn = this.modal.querySelector('.jg-prev');
|
||||
this.nextBtn = this.modal.querySelector('.jg-next');
|
||||
|
||||
// Open modal on image click
|
||||
this.images.forEach((img, idx) => {
|
||||
img.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
this.openModal(idx);
|
||||
});
|
||||
});
|
||||
|
||||
// Basic modal controls
|
||||
this.closeBtn.addEventListener('click', () => this.closeModal());
|
||||
this.prevBtn.addEventListener('click', () => this.prevImage());
|
||||
this.nextBtn.addEventListener('click', () => this.nextImage());
|
||||
|
||||
// Close when clicking outside image
|
||||
this.modal.addEventListener('click', e => {
|
||||
if (e.target === this.modal) this.closeModal();
|
||||
});
|
||||
|
||||
// Keyboard navigation inside modal
|
||||
document.addEventListener('keydown', e => {
|
||||
if (this.modal?.style.display === 'flex') {
|
||||
if (e.key === 'ArrowLeft') this.prevImage();
|
||||
if (e.key === 'ArrowRight') this.nextImage();
|
||||
if (e.key === 'Escape') this.closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
this.initModalInteractions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize modal drag & zoom interactions.
|
||||
*/
|
||||
initModalInteractions() {
|
||||
this.modalImg.currentScale = 1;
|
||||
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
|
||||
// Mouse drag (panning)
|
||||
this.modalImg.addEventListener('mousedown', e => {
|
||||
if (e.button !== 0) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
this.modalImg.style.cursor = 'grabbing';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.modal.addEventListener('mousemove', e => {
|
||||
if (!isDragging) return;
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
translateX += dx;
|
||||
translateY += dy;
|
||||
this.modalImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${this.modalImg.currentScale})`;
|
||||
});
|
||||
|
||||
// Stop dragging on mouse up / leave
|
||||
['mouseup', 'mouseleave'].forEach(event => {
|
||||
this.modal.addEventListener(event, () => {
|
||||
isDragging = false;
|
||||
this.modalImg.style.cursor = 'grab';
|
||||
});
|
||||
});
|
||||
|
||||
// Scroll to zoom
|
||||
this.modalImg.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||
this.modalImg.currentScale = Math.min(
|
||||
Math.max(this.modalImg.currentScale + delta, 0.5),
|
||||
3
|
||||
);
|
||||
this.modalImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${this.modalImg.currentScale})`;
|
||||
});
|
||||
}
|
||||
|
||||
openModal(index) {
|
||||
this.isOpenModal = true;
|
||||
this.currentIndex = index;
|
||||
this.modal.style.display = 'flex';
|
||||
this.modalImg.src = this.images[this.currentIndex].src;
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.isOpenModal = false;
|
||||
this.modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
this.modalImg.currentScale = 1;
|
||||
this.modalImg.style.transform = 'translate(0px, 0px) scale(1)';
|
||||
}
|
||||
|
||||
prevImage() {
|
||||
this.currentIndex =
|
||||
(this.currentIndex - 1 + this.images.length) % this.images.length;
|
||||
this.modalImg.src = this.images[this.currentIndex].src;
|
||||
}
|
||||
|
||||
nextImage() {
|
||||
this.currentIndex =
|
||||
(this.currentIndex + 1) % this.images.length;
|
||||
this.modalImg.src = this.images[this.currentIndex].src;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Private helpers
|
||||
* ======================================================= */
|
||||
|
||||
/**
|
||||
* @returns {HTMLImageElement[]} - All images inside the container.
|
||||
* @private
|
||||
*/
|
||||
_getImages() {
|
||||
return Array.from(this.container.querySelectorAll('img'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {HTMLElement[]} - All elements marked as `.jg-clone` (if any).
|
||||
* @private
|
||||
*/
|
||||
_getImagesClones() {
|
||||
return Array.from(this.container.querySelectorAll('.jg-clone'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an image inside an anchor and attach hover class.
|
||||
* @private
|
||||
*/
|
||||
_wrapImage(img) {
|
||||
const href = img.getAttribute(this.options.linkAttribute) || img.src;
|
||||
const a = document.createElement('a');
|
||||
|
||||
a.href = href;
|
||||
a.style.display = 'inline-block';
|
||||
|
||||
// Preserve caption if present
|
||||
a.dataset.caption = img.dataset.caption || '';
|
||||
img.dataset.caption = '';
|
||||
|
||||
// Attach hover class if defined
|
||||
this._setHover(a);
|
||||
|
||||
img.parentNode.insertBefore(a, img);
|
||||
a.appendChild(img);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a hover class on the wrapper if hoverEffect is configured.
|
||||
* @private
|
||||
*/
|
||||
_setHover(el) {
|
||||
if (this.options.hoverEffect) {
|
||||
el.classList.add(this.options.hoverEffect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current columns count according to container width & breakpoints.
|
||||
* @private
|
||||
*/
|
||||
_getColumns() {
|
||||
const width = this.container.clientWidth;
|
||||
let cols = this.options.columns;
|
||||
|
||||
const breakpoints = Object
|
||||
.keys(this.options.breakpoints)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const bp of breakpoints) {
|
||||
if (width <= bp) {
|
||||
cols = this.options.breakpoints[bp];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the max row height according to container width & maxRowHeightBreakpoints.
|
||||
* @private
|
||||
*/
|
||||
_getMaxRowHeight() {
|
||||
const width = this.container.clientWidth;
|
||||
let maxHeight = this.options.maxRowHeight;
|
||||
const breakpoints = this.options.maxRowHeightBreakpoints;
|
||||
|
||||
if (!breakpoints) return maxHeight;
|
||||
|
||||
const sorted = Object
|
||||
.keys(breakpoints)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const bp of sorted) {
|
||||
if (width <= bp) {
|
||||
maxHeight = breakpoints[bp];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until an image is loaded (or fails) before resolving.
|
||||
* @private
|
||||
*/
|
||||
_waitForImageLoad(img) {
|
||||
return new Promise(resolve => {
|
||||
if (img.complete) {
|
||||
resolve();
|
||||
} else {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bounding rects of wrappers (anchors) before layout.
|
||||
* @returns {DOMRect[]} rects ordered by image index
|
||||
* @private
|
||||
*/
|
||||
_getWrapperRects() {
|
||||
return this.images.map(img => img.parentNode.getBoundingClientRect());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user