This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,42 @@
import $ from 'jquery';
import { debounce } from 'throttle-debounce';
let jetpackLazyImagesLoadEvent;
try {
jetpackLazyImagesLoadEvent = new Event('jetpack-lazy-images-load', {
bubbles: true,
cancelable: true,
});
} catch (e) {
jetpackLazyImagesLoadEvent = document.createEvent('Event');
jetpackLazyImagesLoadEvent.initEvent(
'jetpack-lazy-images-load',
true,
true
);
}
// Fix AJAX loaded images.
$(document).on('loadedNewItems.vpf', function (event) {
if (event.namespace !== 'vpf') {
return;
}
$('body').get(0).dispatchEvent(jetpackLazyImagesLoadEvent);
});
// Fix masonry reloading when Jetpack images lazy loaded.
// https://github.com/Automattic/jetpack/issues/9595
//
// p.s. it looks like this fix is not working at all in Safari browser.
const runReLayout = debounce(200, ($gallery) => {
$gallery.vpf('imagesLoaded');
});
$(document.body).on('jetpack-lazy-loaded-image', '.vp-portfolio', function () {
const $this = $(this).closest('.vp-portfolio');
if ($this && $this.length) {
runReLayout($this);
}
});

View File

@ -0,0 +1,115 @@
import $ from 'jquery';
const { SimpleBar, navigator } = window;
const $doc = $(document);
// Don't run on Mac and mobile devices.
const allowScrollbar =
!/Mac|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
if (allowScrollbar && typeof SimpleBar !== 'undefined') {
// Extend VP class.
$doc.on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Init Simplebar plugin
*/
VP.prototype.initCustomScrollbar = function () {
const self = this;
self.emitEvent('beforeInitCustomScrollbar');
self.$items_wrap
.find('.vp-portfolio__custom-scrollbar')
.each(function () {
const instance = SimpleBar.instances.get(this);
if (!instance) {
new SimpleBar(this);
}
});
self.emitEvent('initCustomScrollbar');
};
/**
* Destroy Simplebar plugin
*/
VP.prototype.destroyCustomScrollbar = function () {
const self = this;
self.$items_wrap
.find('[data-simplebar="init"].vp-portfolio__custom-scrollbar')
.each(function () {
const instance = SimpleBar.instances.get(this);
if (instance) {
instance.unMount();
}
});
self.emitEvent('destroyCustomScrollbar');
};
});
// Add Items.
$doc.on('addItems.vpf', (event, self, $items, removeExisting) => {
if (event.namespace !== 'vpf') {
return;
}
if (removeExisting) {
self.destroyCustomScrollbar();
}
self.initCustomScrollbar();
});
// Init.
$doc.on('init.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.initCustomScrollbar();
});
// Destroy.
$doc.on('destroy.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.destroyCustomScrollbar();
});
// Init Swiper duplicated slides scrollbars.
$doc.on('initSwiper.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.sliderLoop === 'true') {
self.initCustomScrollbar();
}
});
// Fix Simplebar content size in some themes.
// For example, in Astra theme in content with enabled sidebar, Simplebar calculate wrong height automatically.
$(() => {
$('[data-simplebar="init"].vp-portfolio__custom-scrollbar').each(
function () {
const instance = SimpleBar.instances.get(this);
if (instance) {
instance.recalculate();
}
}
);
});
}

View File

@ -0,0 +1,165 @@
import $ from 'jquery';
const $wnd = $(window);
/**
* Check if lines cross
*
* @param {Object} a - first point of the first line
* @param {Object} b - second point of the first line
* @param {Object} c - first point of the second line
* @param {Object} d - second point of the second line
*
* @return {boolean} cross lines
*/
function isCrossLine(a, b, c, d) {
// Working code #1:
//
// var common = (b.x - a.x)*(d.y - c.y) - (b.y - a.y)*(d.x - c.x);
// if (common === 0) {
// return false;
// }
//
// var rH = (a.y - c.y)*(d.x - c.x) - (a.x - c.x)*(d.y - c.y);
// var sH = (a.y - c.y)*(b.x - a.x) - (a.x - c.x)*(b.y - a.y);
//
// var r = rH / common;
// var s = sH / common;
//
// return r >= 0 && r <= 1 && s >= 0 && s <= 1;
// Working code #2:
const v1 = (d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x);
const v2 = (d.x - c.x) * (b.y - c.y) - (d.y - c.y) * (b.x - c.x);
const v3 = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
const v4 = (b.x - a.x) * (d.y - a.y) - (b.y - a.y) * (d.x - a.x);
return v1 * v2 <= 0 && v3 * v4 <= 0;
}
// Init Events.
$(document).on('initEvents.vpf', (event, self) => {
if (event.namespace !== 'vpf' || self.options.itemsStyle !== 'fly') {
return;
}
const evp = `.vpf-uid-${self.uid}`;
// determine cursor position
let lastCursorPos = {};
$wnd.on(`mousemove${evp}`, (e) => {
lastCursorPos = {
x: e.clientX,
y: e.clientY,
};
});
self.$item.on(
`mouseenter${evp} mouseleave${evp}`,
'.vp-portfolio__item',
function (e) {
const $this = $(this);
const itemRect = $this[0].getBoundingClientRect();
const $overlay = $this.find('.vp-portfolio__item-overlay');
const enter = e.type === 'mouseenter';
let endX = '0%';
let endY = '0%';
const curCursorPos = {
x: e.clientX,
y: e.clientY,
};
// find the corner that placed on cursor path.
let isUp = isCrossLine(
{ x: itemRect.left, y: itemRect.top },
{ x: itemRect.left + itemRect.width, y: itemRect.top },
curCursorPos,
lastCursorPos
);
let isDown = isCrossLine(
{ x: itemRect.left, y: itemRect.top + itemRect.height },
{
x: itemRect.left + itemRect.width,
y: itemRect.top + itemRect.height,
},
curCursorPos,
lastCursorPos
);
let isLeft = isCrossLine(
{ x: itemRect.left, y: itemRect.top },
{ x: itemRect.left, y: itemRect.top + itemRect.height },
curCursorPos,
lastCursorPos
);
let isRight = isCrossLine(
{ x: itemRect.left + itemRect.width, y: itemRect.top },
{
x: itemRect.left + itemRect.width,
y: itemRect.top + itemRect.height,
},
curCursorPos,
lastCursorPos
);
// Sometimes isCrossLine returned false, so we need to check direction manually (less accurate, but it is not a big problem).
if (!isUp && !isDown && !isLeft && !isRight) {
const x =
(itemRect.width / 2 - curCursorPos.x + itemRect.left) /
(itemRect.width / 2);
const y =
(itemRect.height / 2 - curCursorPos.y + itemRect.top) /
(itemRect.height / 2);
if (Math.abs(x) > Math.abs(y)) {
if (x > 0) {
isLeft = true;
} else {
isRight = true;
}
} else if (y > 0) {
isUp = true;
} else {
isDown = true;
}
}
if (isUp) {
endY = '-100.1%';
} else if (isDown) {
endY = '100.1%';
} else if (isLeft) {
endX = '-100.1%';
} else if (isRight) {
endX = '100.1%';
}
if (enter) {
$overlay.css({
transition: 'none',
transform: `translateX(${endX}) translateY(${endY}) translateZ(0)`,
});
// Trigger a reflow, flushing the CSS changes. This need to fix some glithes in Safari and Firefox.
// Info here - https://stackoverflow.com/questions/11131875/what-is-the-cleanest-way-to-disable-css-transition-effects-temporarily
// eslint-disable-next-line no-unused-expressions
$overlay[0].offsetHeight;
}
$overlay.css({
transition: '.2s transform ease-in-out',
transform: `translateX(${enter ? '0%' : endX}) translateY(${
enter ? '0%' : endY
}) translateZ(0)`,
});
}
);
});
// Destroy Events.
$(document).on('destroyEvents.vpf', (event, self) => {
if (event.namespace !== 'vpf' || self.options.itemsStyle !== 'fly') {
return;
}
const evp = `.vpf-uid-${self.uid}`;
$wnd.off(`mousemove${evp}`);
self.$item.off(`mouseenter${evp} mouseleave${evp}`);
});

View File

@ -0,0 +1,183 @@
import $ from 'jquery';
const { screenSizes } = window.VPData;
//
// Our custom Grid layout for Isotope.
//
// * fixes grid items position in FireFox - https://wordpress.org/support/topic/gallery-difference-between-firefox-and-all-other-browsers/
//
if (
typeof window.Isotope !== 'undefined' &&
typeof window.Isotope.LayoutMode !== 'undefined'
) {
const VPRows = window.Isotope.LayoutMode.create('vpRows');
const proto = VPRows.prototype;
proto.measureColumns = function () {
// set items, used if measuring first item
this.items = this.isotope.filteredItems;
this.getContainerWidth();
// if columnWidth is 0, default to outerWidth of first item
if (!this.columnWidth) {
const firstItem = this.items[0];
const firstItemElem = firstItem && firstItem.element;
// columnWidth fall back to item of first element
this.columnWidth =
(firstItemElem && window.getSize(firstItemElem).outerWidth) ||
// if first elem has no width, default to size of container
this.containerWidth;
}
this.columnWidth += this.gutter;
// calculate columns
const containerWidth = this.containerWidth + this.gutter;
let cols = containerWidth / this.columnWidth;
// fix rounding errors, typically with gutters
const excess = this.columnWidth - (containerWidth % this.columnWidth);
// if overshoot is less than a pixel, round up, otherwise floor it
const mathMethod = excess && excess < 1 ? 'round' : 'floor';
cols = Math[mathMethod](cols);
this.cols = Math.max(cols, 1);
};
proto.getContainerWidth = function () {
// container is parent if fit width
const isFitWidth = this._getOption
? this._getOption('fitWidth')
: false;
const container = isFitWidth ? this.element.parentNode : this.element;
// check that this.size and size are there
// IE8 triggers resize on body size change, so they might not be
const size = window.getSize(container);
this.containerWidth = size && size.innerWidth;
};
proto._resetLayout = function () {
this.x = 0;
this.y = 0;
this.maxY = 0;
this.horizontalColIndex = 0;
this._getMeasurement('columnWidth', 'outerWidth');
this._getMeasurement('gutter', 'outerWidth');
this.measureColumns();
};
proto._getItemLayoutPosition = function (item) {
item.getSize();
// how many columns does this brick span
const remainder = item.size.outerWidth % this.columnWidth;
const mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
// round if off by 1 pixel, otherwise use ceil
let colSpan = Math[mathMethod](item.size.outerWidth / this.columnWidth);
colSpan = Math.min(colSpan, this.cols);
let col = this.horizontalColIndex % this.cols;
const isOver = colSpan > 1 && col + colSpan > this.cols;
// shift to next row if item can't fit on current row
col = isOver ? 0 : col;
// don't let zero-size items take up space
const hasSize = item.size.outerWidth && item.size.outerHeight;
this.horizontalColIndex = hasSize
? col + colSpan
: this.horizontalColIndex;
const itemWidth = item.size.outerWidth + this.gutter;
// if this element cannot fit in the current row
if (this.x !== 0 && this.horizontalColIndex === 1) {
this.x = 0;
this.y = this.maxY;
}
const position = {
x: this.x,
y: this.y,
};
this.maxY = Math.max(this.maxY, this.y + item.size.outerHeight);
this.x += itemWidth;
return position;
};
proto._getContainerSize = function () {
return { height: this.maxY };
};
}
// Init Options.
$(document).on('initOptions.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.defaults.gridColumns = 3;
if (!self.options.gridColumns) {
self.options.gridColumns = self.defaults.gridColumns;
}
if (!self.options.gridImagesAspectRatio) {
self.options.gridImagesAspectRatio =
self.defaults.gridImagesAspectRatio;
}
});
// Init Layout.
$(document).on('initLayout.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.layout !== 'grid') {
return;
}
// columns.
self.addStyle('.vp-portfolio__item-wrap', {
width: `${100 / self.options.gridColumns}%`,
});
// calculate responsive.
let count = self.options.gridColumns - 1;
let currentPoint = Math.min(screenSizes.length - 1, count);
for (; currentPoint >= 0; currentPoint -= 1) {
if (count > 0 && typeof screenSizes[currentPoint] !== 'undefined') {
self.addStyle(
'.vp-portfolio__item-wrap',
{
width: `${100 / count}%`,
},
`screen and (max-width: ${screenSizes[currentPoint]}px)`
);
}
count -= 1;
}
});
// Change Isotope Layout Mode.
$(document).on('beforeInitIsotope.vpf', (event, self, initOptions) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.layout !== 'grid' || typeof initOptions !== 'object') {
return;
}
initOptions.layoutMode = 'vpRows';
});

View File

@ -0,0 +1,19 @@
import $ from 'jquery';
// Init Options.
$(document).on('initOptions.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.defaults.justifiedRowHeight = 250;
self.defaults.justifiedRowHeightTolerance = 0.25;
if (!self.options.justifiedRowHeight) {
self.options.justifiedRowHeight = self.defaults.justifiedRowHeight;
}
if (!self.options.justifiedRowHeightTolerance) {
self.options.justifiedRowHeightTolerance =
self.defaults.justifiedRowHeightTolerance;
}
});

View File

@ -0,0 +1,53 @@
import $ from 'jquery';
const { screenSizes } = window.VPData;
// Init Options.
$(document).on('initOptions.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.defaults.masonryColumns = 3;
if (!self.options.masonryColumns) {
self.options.masonryColumns = self.defaults.masonryColumns;
}
if (!self.options.masonryImagesAspectRatio) {
self.options.masonryImagesAspectRatio =
self.defaults.masonryImagesAspectRatio;
}
});
// Init Layout.
$(document).on('initLayout.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.layout !== 'masonry') {
return;
}
// columns.
self.addStyle('.vp-portfolio__item-wrap', {
width: `${100 / self.options.masonryColumns}%`,
});
// calculate responsive.
let count = self.options.masonryColumns - 1;
let currentPoint = Math.min(screenSizes.length - 1, count);
for (; currentPoint >= 0; currentPoint -= 1) {
if (count > 0 && typeof screenSizes[currentPoint] !== 'undefined') {
self.addStyle(
'.vp-portfolio__item-wrap',
{
width: `${100 / count}%`,
},
`screen and (max-width: ${screenSizes[currentPoint]}px)`
);
}
count -= 1;
}
});

View File

@ -0,0 +1,152 @@
import isNumber from 'is-number';
import $ from 'jquery';
import { throttle } from 'throttle-debounce';
const { ResizeObserver } = window;
// Listen for slider width change to calculate dynamic height of images.
const dynamicHeightObserver = new ResizeObserver(
throttle(100, (entries) => {
entries.forEach(({ target }) => {
if (target && target.vpf) {
const self = target.vpf;
const calculatedHeight =
(self.$item.width() *
parseFloat(self.options.sliderItemsHeight)) /
100;
target
.querySelector('.vp-portfolio__items-wrap')
.style.setProperty(
'--vp-layout-slider--auto-items__height',
`${calculatedHeight}px`
);
}
});
})
);
// Init Layout.
$(document).on('initLayout.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.layout !== 'slider') {
return;
}
['items', 'thumbnails'].forEach((type) => {
let itemsHeight =
type === 'items'
? self.options.sliderItemsHeight
: self.options.sliderThumbnailsHeight;
if (itemsHeight === 'auto') {
return;
}
const typeSingle = type.replace(/s$/g, '');
let itemsMinHeight =
type === 'items' ? self.options.sliderItemsMinHeight : 0;
itemsHeight = isNumber(itemsHeight) ? `${itemsHeight}px` : itemsHeight;
// prevent minHeight option in preview, when used 'vh' units.
if (itemsMinHeight && self.isPreview() && /vh/.test(itemsMinHeight)) {
itemsMinHeight = 0;
}
const itemsPerView =
type === 'items'
? self.options.sliderSlidesPerView
: self.options.sliderThumbnailsPerView;
if (itemsPerView === 'auto') {
// fix fade slider items width.
// https://github.com/nk-crew/visual-portfolio/issues/95.
let itemsWidth = 'auto';
if (type === 'items' && self.options.sliderEffect === 'fade') {
itemsWidth = '100%';
}
// Calculate dynamic height.
// Previously we tried the pure CSS solution, but there was couple bugs like:
// - Classic styles items wrong height
// - FireFox wrong images width render
if (itemsHeight.indexOf('%') === itemsHeight.length - 1) {
dynamicHeightObserver.observe(self.$item[0]);
// Static height.
} else {
self.addStyle(`.vp-portfolio__${type}-wrap`, {
'--vp-layout-slider--auto-items__height': itemsHeight,
});
}
self.addStyle(`.vp-portfolio__${typeSingle}-wrap`, {
width: 'auto',
});
self.addStyle(
`.vp-portfolio__${typeSingle} .vp-portfolio__${typeSingle}-img img`,
{
width: itemsWidth,
height: 'var(--vp-layout-slider--auto-items__height)',
}
);
// min height.
if (itemsMinHeight) {
self.addStyle(
`.vp-portfolio__${typeSingle} .vp-portfolio__${typeSingle}-img img`,
{
'min-height': itemsMinHeight,
}
);
}
} else {
// We have to use this hack with Before to support Dynamic height.
// Also, previously we used the `margin-top`,
// but it is not working correctly with Items Mininmal Height option.
self.addStyle(`.vp-portfolio__${typeSingle}-img-wrap::before`, {
'padding-top': itemsHeight,
});
self.addStyle(`.vp-portfolio__${typeSingle}-img img`, {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
});
self.addStyle(`.vp-portfolio__${typeSingle}-img`, {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
});
self.addStyle(
`.vp-portfolio__${typeSingle} .vp-portfolio__${typeSingle}-img img`,
{
width: '100%',
height: '100%',
}
);
// min height.
if (itemsMinHeight) {
self.addStyle(`.vp-portfolio__${typeSingle}-img-wrap::before`, {
'min-height': itemsMinHeight,
});
}
}
});
// thumbnails top gap.
if (self.options.sliderThumbnailsGap) {
self.addStyle('.vp-portfolio__thumbnails-wrap', {
'margin-top': `${self.options.sliderThumbnailsGap}px`,
});
}
});

View File

@ -0,0 +1,179 @@
import $ from 'jquery';
const { screenSizes } = window.VPData;
// fix masonry items position for Tiles layout.
// https://github.com/nk-crew/visual-portfolio/issues/111
if (
typeof window.Isotope !== 'undefined' &&
typeof window.Isotope.LayoutMode !== 'undefined'
) {
const MasonryMode = window.Isotope.LayoutMode.modes.masonry;
if (MasonryMode) {
const defaultMeasureColumns = MasonryMode.prototype.measureColumns;
MasonryMode.prototype.measureColumns = function () {
let runDefault = true;
// if columnWidth is 0, default to columns count size.
if (!this.columnWidth) {
const $vp = $(this.element).closest(
'.vp-portfolio[data-vp-layout="tiles"]'
);
// change column size for Tiles type only.
if ($vp.length && $vp[0].vpf) {
this.getContainerWidth();
const { vpf } = $vp[0];
const settings = vpf.getTilesSettings();
// get columns number
let columns = parseInt(settings[0], 10) || 1;
// calculate responsive.
let count = columns - 1;
let currentPoint = Math.min(screenSizes.length - 1, count);
for (; currentPoint >= 0; currentPoint -= 1) {
if (
count > 0 &&
typeof screenSizes[currentPoint] !== 'undefined'
) {
if (
window.innerWidth <= screenSizes[currentPoint]
) {
columns = count;
}
}
count -= 1;
}
if (columns) {
this.columnWidth = this.containerWidth / columns;
this.columnWidth += this.gutter;
this.cols = columns;
runDefault = false;
}
}
}
if (runDefault) {
defaultMeasureColumns.call(this);
}
};
}
}
// Extend VP class.
$(document).on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Get Tiles Layout Settings
*
* @return {string} tiles layout
*/
VP.prototype.getTilesSettings = function () {
const self = this;
const layoutArr = self.options.tilesType.split(/[:|]/);
// remove last empty item
if (
typeof layoutArr[layoutArr.length - 1] !== 'undefined' &&
!layoutArr[layoutArr.length - 1]
) {
layoutArr.pop();
}
return layoutArr;
};
});
// Init Options.
$(document).on('initOptions.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.defaults.tilesType = '3|1,1|';
if (!self.options.tilesType) {
self.options.tilesType = self.defaults.tilesType;
}
});
// Init Layout.
$(document).on('initLayout.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (self.options.layout !== 'tiles') {
return;
}
const settings = self.getTilesSettings();
// get columns number
const columns = parseInt(settings[0], 10) || 1;
settings.shift();
// set columns
self.addStyle('.vp-portfolio__item-wrap', {
width: `${100 / columns}%`,
});
// set items sizes
if (settings && settings.length) {
for (let k = 0; k < settings.length; k += 1) {
const size = settings[k].split(',');
const w = parseFloat(size[0]) || 1;
const h = parseFloat(size[1]) || 1;
let itemSelector = '.vp-portfolio__item-wrap';
if (settings.length > 1) {
itemSelector += `:nth-of-type(${settings.length}n+${k + 1})`;
}
if (w && w !== 1) {
self.addStyle(itemSelector, {
width: `${(w * 100) / columns}%`,
});
}
self.addStyle(
`${itemSelector} .vp-portfolio__item-img-wrap::before`,
{
'padding-top': `${h * 100}%`,
}
);
}
}
// calculate responsive.
let count = columns - 1;
let currentPoint = Math.min(screenSizes.length - 1, count);
for (; currentPoint >= 0; currentPoint -= 1) {
if (count > 0 && typeof screenSizes[currentPoint] !== 'undefined') {
self.addStyle(
'.vp-portfolio__item-wrap',
{
width: `${100 / count}%`,
},
`screen and (max-width: ${screenSizes[currentPoint]}px)`
);
self.addStyle(
'.vp-portfolio__item-wrap:nth-of-type(n)',
{
width: `${100 / count}%`,
},
`screen and (max-width: ${screenSizes[currentPoint]}px)`
);
}
count -= 1;
}
});

View File

@ -0,0 +1,26 @@
/*
* Visual Portfolio images lazy load fallback for browsers
* which does not support CSS :has()
*/
// Lazyloaded - remove preloader images placeholder effect.
document.addEventListener('lazybeforeunveil', (e) => {
const vpfImgWrapper = e.target.closest(
'.vp-portfolio__item-img, .vp-portfolio__thumbnail-img'
);
if (vpfImgWrapper) {
vpfImgWrapper.classList.add('vp-has-lazyloading');
}
});
document.addEventListener('lazyloaded', (e) => {
const vpfImgWrapper = e.target.closest(
'.vp-portfolio__item-img, .vp-portfolio__thumbnail-img'
);
if (vpfImgWrapper) {
vpfImgWrapper.classList.add('vp-has-lazyloaded');
vpfImgWrapper.classList.add('vp-has-lazyloading');
}
});

View File

@ -0,0 +1,26 @@
// Recalculate image size if parent is <picture>
document.addEventListener('lazybeforesizes', (e) => {
// for some reason sometimes e.detail is undefined, so we need to check it.
if (!e.detail || !e.detail.width || !e.target) {
return;
}
const parent = e.target.closest(':not(picture)');
if (parent) {
e.detail.width = parent.clientWidth || e.detail.width;
}
});
/**
* Remove <noscript> tag.
* Some of optimization plugin make something, that killed our styles with noscript tag.
* Related topic: https://wordpress.org/support/topic/visual-portfolio-and-sg-optimizer-dont-play-well/
*/
document.addEventListener('lazybeforeunveil', (e) => {
const prevEl = e.target.previousElementSibling;
if (prevEl && prevEl.matches('noscript')) {
prevEl.remove();
}
});

View File

@ -0,0 +1,12 @@
window.lazySizesConfig = window.lazySizesConfig || {};
window.lazySizesConfig = {
...window.lazySizesConfig,
lazyClass: 'vp-lazyload',
loadedClass: 'vp-lazyloaded',
preloadClass: 'vp-lazypreload',
loadingClass: 'vp-lazyloading',
srcAttr: 'data-src',
srcsetAttr: 'data-srcset',
sizesAttr: 'data-sizes',
};

View File

@ -0,0 +1,67 @@
(function (window, factory) {
const globalInstall = function () {
factory(window.lazySizes);
window.removeEventListener('lazyunveilread', globalInstall, true);
};
factory = factory.bind(null, window, window.document);
if (window.lazySizes) {
globalInstall();
} else {
window.addEventListener('lazyunveilread', globalInstall, true);
}
})(window, (window, document, lazySizes) => {
if (!window.addEventListener) {
return;
}
const getCSS = function (elem) {
return window.getComputedStyle(elem, null) || {};
};
const objectFitCover = {
calculateSize(element, width) {
const CSS = getCSS(element);
if (CSS && CSS.objectFit && CSS.objectFit === 'cover') {
const blockHeight = parseInt(
element.getAttribute('height'),
10
);
const blockWidth = parseInt(element.getAttribute('width'), 10);
if (blockHeight) {
if (
blockWidth / blockHeight >
element.clientWidth / element.clientHeight
) {
width = parseInt(
(element.clientHeight * blockWidth) / blockHeight,
10
);
}
}
}
return width;
},
};
lazySizes.objectFitCover = objectFitCover;
document.addEventListener('lazybeforesizes', (e) => {
// for some reason sometimes e.detail is undefined, so we need to check it.
if (
e.defaultPrevented ||
!e.detail ||
!e.detail.width ||
!e.target ||
e.detail.instance !== lazySizes
) {
return;
}
const element = e.target;
e.detail.width = objectFitCover.calculateSize(element, e.detail.width);
});
});

View File

@ -0,0 +1,91 @@
/**
* Load duplicated Swiper slides to prevent images "blink" effect after swipe.
*
* @param window
* @param factory
*/
(function (window, factory) {
const globalInstall = function () {
factory(window.lazySizes);
window.removeEventListener('lazyunveilread', globalInstall, true);
};
factory = factory.bind(null, window, window.document);
if (window.lazySizes) {
globalInstall();
} else {
window.addEventListener('lazyunveilread', globalInstall, true);
}
})(window, (window, document, lazySizes) => {
if (!window.addEventListener) {
return;
}
const { unveil } = lazySizes.loader;
const getSiblings = (el, filter) =>
[...el.parentNode.children].filter(
(child) =>
child.nodeType === 1 &&
child !== el &&
(!filter || child.matches(filter))
);
const swiperDuplicatesLoad = {
getSlideData(element) {
const $el = element.closest('.swiper-slide');
const slideIndex = $el
? $el.getAttribute('data-swiper-slide-index')
: false;
return {
$el,
slideIndex,
};
},
run(element) {
const slideData = this.getSlideData(element);
if (slideData.slideIndex) {
const $siblingDuplicates = getSiblings(
slideData.$el,
`[data-swiper-slide-index="${slideData.slideIndex}"]`
);
$siblingDuplicates.forEach((el) => {
// We should also get images in `loading` state, because in some rare situations
// duplicated images by default has this class and not displaying correctly.
const $images = el.querySelectorAll(
'img.vp-lazyload, img.vp-lazyloading'
);
if ($images) {
$images.forEach(($img) => {
unveil($img);
});
}
});
}
return true;
},
};
lazySizes.swiperDuplicatesLoad = swiperDuplicatesLoad;
document.addEventListener('lazyloaded', (e) => {
// for some reason sometimes e.detail is undefined, so we need to check it.
if (
e.defaultPrevented ||
!e.detail ||
e.detail.swiperDuplicatesChecked ||
!e.target ||
e.detail.instance !== lazySizes
) {
return;
}
const element = e.target;
e.detail.swiperDuplicatesChecked = swiperDuplicatesLoad.run(element);
});
});

View File

@ -0,0 +1,995 @@
import $ from 'jquery';
import rafSchd from 'raf-schd';
import { throttle } from 'throttle-debounce';
const { VPData } = window;
const { __ } = VPData;
const $wnd = $(window);
/**
* Emit Resize Event.
*/
function windowResizeEmit() {
if (typeof window.Event === 'function') {
// modern browsers
window.dispatchEvent(new window.Event('resize'));
} else {
// for IE and other old browsers
// causes deprecation warning on modern browsers
const evt = window.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(evt);
}
}
const visibilityData = {};
let shouldCheckVisibility = false;
let checkVisibilityTimeout = false;
let isFocusVisible = false;
// fix portfolio inside Tabs and Accordions
// check visibility by timer https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom/33456469
//
// https://github.com/nk-crew/visual-portfolio/issues/11
// https://github.com/nk-crew/visual-portfolio/issues/113
function checkVisibility() {
clearTimeout(checkVisibilityTimeout);
if (!shouldCheckVisibility) {
return;
}
const $items = $('.vp-portfolio__ready');
if ($items.length) {
let isVisibilityChanged = false;
$items.each(function () {
const { vpf } = this;
if (!vpf) {
return;
}
const currentState = visibilityData[vpf.uid] || 'none';
visibilityData[vpf.uid] =
this.offsetParent === null ? 'hidden' : 'visible';
// changed from hidden to visible.
if (
currentState === 'hidden' &&
visibilityData[vpf.uid] === 'visible'
) {
isVisibilityChanged = true;
}
});
// resize, if visibility changed.
if (isVisibilityChanged) {
windowResizeEmit();
}
} else {
shouldCheckVisibility = false;
}
// run again.
checkVisibilityTimeout = setTimeout(checkVisibility, 500);
}
// run check function only after portfolio inited.
$(document).on('inited.vpf', (event) => {
if (event.namespace !== 'vpf') {
return;
}
shouldCheckVisibility = true;
checkVisibility();
});
/**
* If the most recent user interaction was via the keyboard;
* and the key press did not include a meta, alt/option, or control key;
* then the modality is keyboard. Otherwise, the modality is not keyboard.
*/
document.addEventListener(
'keydown',
function (e) {
if (e.metaKey || e.altKey || e.ctrlKey) {
return;
}
isFocusVisible = true;
},
true
);
/**
* If at any point a user clicks with a pointing device, ensure that we change
* the modality away from keyboard.
* This avoids the situation where a user presses a key on an already focused
* element, and then clicks on a different element, focusing it with a
* pointing device, while we still think we're in keyboard modality.
*/
document.addEventListener(
'mousedown',
() => {
isFocusVisible = false;
},
true
);
document.addEventListener(
'pointerdown',
() => {
isFocusVisible = false;
},
true
);
document.addEventListener(
'touchstart',
() => {
isFocusVisible = false;
},
true
);
/**
* Main VP class
*/
class VP {
constructor($item, userOptions) {
const self = this;
self.$item = $item;
// get id from class
const classes = $item[0].className.split(/\s+/);
for (let k = 0; k < classes.length; k += 1) {
if (classes[k] && /^vp-uid-/.test(classes[k])) {
self.uid = classes[k].replace(/^vp-uid-/, '');
}
if (classes[k] && /^vp-id-/.test(classes[k])) {
self.id = classes[k].replace(/^vp-id-/, '');
}
}
if (!self.uid) {
// eslint-disable-next-line no-console
console.error(__.couldnt_retrieve_vp);
return;
}
self.href = window.location.href;
self.$items_wrap = $item.find('.vp-portfolio__items');
self.$slider_thumbnails_wrap = $item.find('.vp-portfolio__thumbnails');
self.$pagination = $item.find('.vp-portfolio__pagination-wrap');
self.$filter = $item.find('.vp-portfolio__filter-wrap');
self.$sort = $item.find('.vp-portfolio__sort-wrap');
// find single filter block.
if (self.id) {
self.$filter = self.$filter.add(
`.vp-single-filter.vp-id-${self.id} .vp-portfolio__filter-wrap`
);
}
// find single sort block.
if (self.id) {
self.$sort = self.$sort.add(
`.vp-single-sort.vp-id-${self.id} .vp-portfolio__sort-wrap`
);
}
// user options
self.userOptions = userOptions;
self.firstRun = true;
self.init();
}
// emit event
// Example:
// $(document).on('init.vpf', function (event, infiniteObject) {
// console.log(infiniteObject);
// });
emitEvent(event, data) {
data = data ? [this].concat(data) : [this];
this.$item.trigger(`${event}.vpf`, data);
this.$item.trigger(`${event}.vpf-uid-${this.uid}`, data);
}
/**
* Init
*/
init() {
const self = this;
// destroy if already inited
if (!self.firstRun) {
self.destroy();
}
self.destroyed = false;
self.$item.addClass('vp-portfolio__ready');
// init options
self.initOptions();
// init events
self.initEvents();
// init layout
self.initLayout();
// init custom colors
self.initCustomColors();
self.emitEvent('init');
if (self.id) {
$(`.vp-single-filter.vp-id-${self.id}`)
.addClass('vp-single-filter__ready')
.parent('.vp-portfolio__layout-elements')
.addClass('vp-portfolio__layout-elements__ready');
$(`.vp-single-sort.vp-id-${self.id}`)
.addClass('vp-single-sort__ready')
.parent('.vp-portfolio__layout-elements')
.addClass('vp-portfolio__layout-elements__ready');
}
// resized
self.resized();
// images loaded
self.imagesLoaded();
self.emitEvent('inited');
self.firstRun = false;
}
/**
* Check if script loaded in preview.
*
* @return {boolean} is in preview.
*/
isPreview() {
const self = this;
return !!self.$item.closest('#vp_preview').length;
}
/**
* Called after resized container.
*/
resized() {
windowResizeEmit();
this.emitEvent('resized');
}
/**
* Images loaded.
*/
imagesLoaded() {
const self = this;
if (!self.$items_wrap.imagesLoaded) {
return;
}
self.$items_wrap.imagesLoaded().progress(() => {
this.emitEvent('imagesLoaded');
});
}
/**
* Destroy
*/
destroy() {
const self = this;
// remove loaded class
self.$item.removeClass('vp-portfolio__ready');
if (self.id) {
$(`.vp-single-filter.vp-id-${self.id}`)
.removeClass('vp-single-filter__ready')
.parent('.vp-portfolio__layout-elements')
.removeClass('vp-portfolio__layout-elements__ready');
$(`.vp-single-sort.vp-id-${self.id}`)
.removeClass('vp-single-sort__ready')
.parent('.vp-portfolio__layout-elements')
.removeClass('vp-portfolio__layout-elements__ready');
}
// destroy events
self.destroyEvents();
// remove all generated styles
self.removeStyle();
self.renderStyle();
self.emitEvent('destroy');
self.destroyed = true;
}
/**
* Add style to the current portfolio list
*
* @param {string} selector css selector
* @param {string} styles object with styles
* @param {string} media string with media query
*/
addStyle(selector, styles, media) {
media = media || '';
const self = this;
const { uid } = self;
if (!self.stylesList) {
self.stylesList = {};
}
if (typeof self.stylesList[uid] === 'undefined') {
self.stylesList[uid] = {};
}
if (typeof self.stylesList[uid][media] === 'undefined') {
self.stylesList[uid][media] = {};
}
if (typeof self.stylesList[uid][media][selector] === 'undefined') {
self.stylesList[uid][media][selector] = {};
}
self.stylesList[uid][media][selector] = $.extend(
self.stylesList[uid][media][selector],
styles
);
self.emitEvent('addStyle', [selector, styles, media, self.stylesList]);
}
/**
* Remove style from the current portfolio list
*
* @param {string} selector css selector (if not set - removed all styles)
* @param {string} styles object with styles
* @param {string} media string with media query
*/
removeStyle(selector, styles, media) {
media = media || '';
const self = this;
const { uid } = self;
if (!self.stylesList) {
self.stylesList = {};
}
if (typeof self.stylesList[uid] !== 'undefined' && !selector) {
self.stylesList[uid] = {};
}
if (
typeof self.stylesList[uid] !== 'undefined' &&
typeof self.stylesList[uid][media] !== 'undefined' &&
typeof self.stylesList[uid][media][selector] !== 'undefined' &&
selector
) {
delete self.stylesList[uid][media][selector];
}
self.emitEvent('removeStyle', [selector, styles, self.stylesList]);
}
/**
* Render style for the current portfolio list
*/
renderStyle() {
const self = this;
// timeout for the case, when styles added one by one
const { uid } = self;
let stylesString = '';
if (!self.stylesList) {
self.stylesList = {};
}
// create string with styles
if (typeof self.stylesList[uid] !== 'undefined') {
Object.keys(self.stylesList[uid]).forEach((m) => {
// media
if (m) {
stylesString += `@media ${m} {`;
}
Object.keys(self.stylesList[uid][m]).forEach((s) => {
// selector
const selectorParent = `.vp-uid-${uid}`;
let selector = `${selectorParent} ${s}`;
// add parent selector after `,`.
selector = selector.replace(
/, |,/g,
`, ${selectorParent} `
);
stylesString += `${selector} {`;
Object.keys(self.stylesList[uid][m][s]).forEach((p) => {
// property and value
stylesString += `${p}:${self.stylesList[uid][m][s][p]};`;
});
stylesString += '}';
});
// media
if (m) {
stylesString += '}';
}
});
}
// add in style tag
let $style = $(`#vp-style-${uid}`);
if (!$style.length) {
$style = $('<style>')
.attr('id', `vp-style-${uid}`)
.appendTo('head');
}
$style.html(stylesString);
self.emitEvent('renderStyle', [stylesString, self.stylesList, $style]);
}
/**
* First char to lower case
*
* @param {string} str string to transform
* @return {string} result string
*/
firstToLowerCase(str) {
return str.substr(0, 1).toLowerCase() + str.substr(1);
}
/**
* Init options
*
* @param {Object} userOptions user options
*/
initOptions(userOptions) {
const self = this;
// default options
self.defaults = {
layout: 'tile',
itemsGap: 0,
pagination: 'load-more',
};
// new user options
if (userOptions) {
self.userOptions = userOptions;
}
// prepare data options
const dataOptions = self.$item[0].dataset;
const pureDataOptions = {};
Object.keys(dataOptions).forEach((k) => {
if (k && k.substring(0, 2) === 'vp') {
pureDataOptions[self.firstToLowerCase(k.substring(2))] =
dataOptions[k];
}
});
self.options = $.extend(
{},
self.defaults,
pureDataOptions,
self.userOptions
);
self.emitEvent('initOptions');
}
/**
* Init events
*/
initEvents() {
const self = this;
const evp = `.vpf-uid-${self.uid}`;
// Stretch
function stretch() {
const rect = self.$item[0].getBoundingClientRect();
const { left } = rect;
const right = window.innerWidth - rect.right;
const ml = parseFloat(self.$item.css('margin-left') || 0);
const mr = parseFloat(self.$item.css('margin-right') || 0);
self.$item.css({
marginLeft: ml - left,
marginRight: mr - right,
maxWidth: 'none',
width: 'auto',
});
}
if (self.$item.hasClass('vp-portfolio__stretch') && !self.isPreview()) {
$wnd.on(`load${evp} resize${evp} orientationchange${evp}`, () => {
stretch();
});
stretch();
}
// add helper focus class
// TODO: change to CSS :has() when will be widely available
// @link https://caniuse.com/?search=%3Ahas
self.$item.on(`focus${evp}`, '.vp-portfolio__item a', function () {
const $item = $(this).closest('.vp-portfolio__item');
$item.addClass('vp-portfolio__item-focus');
if (isFocusVisible) {
$item.addClass('vp-portfolio__item-focus-visible');
}
});
self.$item.on(`blur${evp}`, '.vp-portfolio__item a', function () {
$(this)
.closest('.vp-portfolio__item')
.removeClass(
'vp-portfolio__item-focus vp-portfolio__item-focus-visible'
);
});
// on filter click
self.$filter.on(
`click${evp}`,
'.vp-filter .vp-filter__item a',
function (e) {
e.preventDefault();
const $this = $(this);
if (!self.loading) {
$this
.closest('.vp-filter__item')
.addClass('vp-filter__item-active')
.siblings()
.removeClass('vp-filter__item-active');
}
self.loadNewItems($this.attr('href'), true);
}
);
// on sort click
self.$sort.on(`click${evp}`, '.vp-sort .vp-sort__item a', function (e) {
e.preventDefault();
const $this = $(this);
if (!self.loading) {
$this
.closest('.vp-sort__item')
.addClass('vp-sort__item-active')
.siblings()
.removeClass('vp-sort__item-active');
}
self.loadNewItems($this.attr('href'), true);
});
// on filter/sort select change
self.$filter
.add(self.$sort)
.on(
`change${evp}`,
'.vp-filter select, .vp-sort select',
function () {
const $this = $(this);
const value = $this.val();
const $option = $this.find(`[value="${value}"]`);
if ($option.length) {
self.loadNewItems($option.attr('data-vp-url'), true);
}
}
);
// on pagination click
self.$item.on(
`click${evp}`,
'.vp-pagination .vp-pagination__item a',
function (e) {
e.preventDefault();
const $this = $(this);
const $pagination = $this.closest('.vp-pagination');
if (
$pagination.hasClass('vp-pagination__no-more') &&
self.options.pagination !== 'paged'
) {
return;
}
self.loadNewItems(
$this.attr('href'),
self.options.pagination === 'paged'
);
// Scroll to top
if (
self.options.pagination === 'paged' &&
$pagination.hasClass('vp-pagination__scroll-top')
) {
const $adminBar = $('#wpadminbar');
const currentTop =
window.pageYOffset ||
document.documentElement.scrollTop;
let { top } = self.$item.offset();
// Custom user offset.
if ($pagination.attr('data-vp-pagination-scroll-top')) {
top -=
parseInt(
$pagination.attr(
'data-vp-pagination-scroll-top'
),
10
) || 0;
}
// Admin bar offset.
if (
$adminBar.length &&
$adminBar.css('position') === 'fixed'
) {
top -= $adminBar.outerHeight();
}
// Limit max offset.
top = Math.max(0, top);
if (currentTop > top) {
window.scrollTo({
top,
behavior: 'smooth',
});
}
}
}
);
// on categories of item click
self.$item.on(
`click${evp}`,
'.vp-portfolio__items .vp-portfolio__item-meta-category a',
function (e) {
e.preventDefault();
e.stopPropagation();
self.loadNewItems($(this).attr('href'), true);
}
);
// resized container
self.$item.on(`transitionend${evp}`, '.vp-portfolio__items', (e) => {
if (e.currentTarget === e.target) {
self.resized();
}
});
self.emitEvent('initEvents');
}
/**
* Destroy events
*/
destroyEvents() {
const self = this;
const evp = `.vpf-uid-${self.uid}`;
// destroy click events
self.$item.off(evp);
self.$filter.off(evp);
self.$sort.off(evp);
// destroy window events
$wnd.off(evp);
self.emitEvent('destroyEvents');
}
/**
* Init layout
*/
initLayout() {
const self = this;
self.emitEvent('initLayout');
self.renderStyle();
}
/**
* Init custom color by data attributes:
* data-vp-bg-color
* data-vp-text-color
*/
initCustomColors() {
const self = this;
self.$item.find('[data-vp-bg-color]').each(function () {
const val = $(this).attr('data-vp-bg-color');
self.addStyle(`[data-vp-bg-color="${val}"]`, {
'background-color': `${val} !important`,
});
});
self.$item.find('[data-vp-text-color]').each(function () {
const val = $(this).attr('data-vp-text-color');
self.addStyle(`[data-vp-text-color="${val}"]`, {
color: `${val} !important`,
});
});
self.renderStyle();
self.emitEvent('initCustomColors');
}
/**
* Add New Items
*
* @param {object|dom|jQuery} $items - elements.
* @param {bool} removeExisting - remove existing elements.
* @param {Object} $newVP - new visual portfolio jQuery.
*/
addItems($items, removeExisting, $newVP) {
const self = this;
self.emitEvent('addItems', [$items, removeExisting, $newVP]);
}
/**
* Remove Items
*
* @param {object|dom|jQuery} $items - elements.
*/
removeItems($items) {
const self = this;
self.emitEvent('removeItems', [$items]);
}
/**
* AJAX Load New Items
*
* @param {string} url - url to request.
* @param {bool} removeExisting - remove existing elements.
* @param {Function} cb - callback.
*/
loadNewItems(url, removeExisting, cb) {
const self = this;
const { randomSeed } = self.options;
if (
(self.loading && typeof self.loading.readyState === 'undefined') ||
!url ||
self.href === url
) {
return;
}
// Abort previous AJAX loader to prevent conflict.
// We need it mostly for Search feature, because users can type also when already in loading state.
if (self.loading && self.loading.readyState && self.loading.abort) {
self.loading.abort();
}
const ajaxData = {
method: 'POST',
url,
data: {
vpf_ajax_call: true,
vpf_random_seed:
typeof randomSeed !== 'undefined' ? randomSeed : false,
},
complete({ responseText }) {
self.href = url;
self.replaceItems(responseText, removeExisting, cb);
},
};
self.loading = true;
self.$item.addClass('vp-portfolio__loading');
self.emitEvent('startLoadingNewItems', [url, ajaxData]);
self.loading = $.ajax(ajaxData);
}
/**
* Replace items to the new loaded using AJAX
*
* @param {string} content - new page content.
* @param {bool} removeExisting - remove existing elements.
* @param {Function} cb - callback.
*/
replaceItems(content, removeExisting, cb) {
const self = this;
if (!content) {
return;
}
// load to invisible container, then append to posts container
content = content
.replace('<body', '<body><div id="vp-ajax-load-body"')
.replace('</body>', '</div></body>');
const $body = $(content).filter('#vp-ajax-load-body');
// find current block on new page
const $newVP = $body.find(`.vp-portfolio.vp-uid-${self.uid}`);
// insert new items
if ($newVP.length) {
const newItems = $newVP.find('.vp-portfolio__items').html();
const nothingFound = $newVP.hasClass('vp-portfolio-not-found');
// We should clean up notices here, as they may be cloned over and over.
self.$item.find('.vp-notice').remove();
if (nothingFound) {
self.$item
.find('.vp-portfolio__items-wrap')
.before($newVP.find('.vp-notice').clone());
self.$item.addClass('vp-portfolio-not-found');
} else {
self.$item.removeClass('vp-portfolio-not-found');
}
// update filter
if (self.$filter.length) {
self.$filter.each(function () {
const $filter = $(this);
let newFilterContent = '';
if ($filter.parent().hasClass('vp-single-filter')) {
newFilterContent = $body
.find(
`[class="${$filter
.parent()
.attr('class')
.replace(
' vp-single-filter__ready',
''
)}"] .vp-portfolio__filter-wrap`
)
.html();
} else {
newFilterContent = $newVP
.find('.vp-portfolio__filter-wrap')
.html();
}
$filter.html(newFilterContent);
});
}
// update sort
if (self.$sort.length) {
self.$sort.each(function () {
const $sort = $(this);
let newFilterContent = '';
if ($sort.parent().hasClass('vp-single-sort')) {
newFilterContent = $body
.find(
`[class="${$sort
.parent()
.attr('class')
.replace(
' vp-single-sort__ready',
''
)}"] .vp-portfolio__sort-wrap`
)
.html();
} else {
newFilterContent = $newVP
.find('.vp-portfolio__sort-wrap')
.html();
}
$sort.html(newFilterContent);
});
}
// update pagination
if (self.$pagination.length) {
self.$pagination.html(
$newVP.find('.vp-portfolio__pagination-wrap').html()
);
}
self.addItems($(newItems), removeExisting, $newVP);
self.emitEvent('loadedNewItems', [$newVP, removeExisting, content]);
if (cb) {
cb();
}
}
// update next page data
const nextPageUrl = $newVP.attr('data-vp-next-page-url');
self.options.nextPageUrl = nextPageUrl;
self.$item.attr('data-vp-next-page-url', nextPageUrl);
self.$item.removeClass('vp-portfolio__loading');
self.loading = false;
self.emitEvent('endLoadingNewItems');
// images loaded
self.imagesLoaded();
// init custom colors
self.initCustomColors();
}
}
// extend VP object.
$(document).trigger('extendClass.vpf', [VP]);
// global definition
const plugin = function (options, ...args) {
let ret;
this.each(function () {
if (typeof ret !== 'undefined') {
return;
}
if (typeof options === 'object' || typeof options === 'undefined') {
if (!this.vpf) {
this.vpf = new VP($(this), options);
}
} else if (this.vpf) {
ret = this.vpf[options](...args);
}
});
return typeof ret !== 'undefined' ? ret : this;
};
plugin.constructor = VP;
// no conflict
const oldPlugin = $.fn.vpf;
$.fn.vpf = plugin;
$.fn.vpf.noConflict = function () {
$.fn.vpf = oldPlugin;
return this;
};
// initialization
$(() => {
$('.vp-portfolio').vpf();
});
const throttledInit = throttle(
200,
rafSchd(() => {
$('.vp-portfolio:not(.vp-portfolio__ready)').vpf();
})
);
if (window.MutationObserver) {
new window.MutationObserver(throttledInit).observe(
document.documentElement,
{
childList: true,
subtree: true,
}
);
} else {
$(document).on('DOMContentLoaded DOMNodeInserted load', () => {
throttledInit();
});
}

View File

@ -0,0 +1,51 @@
import $ from 'jquery';
import rafSchd from 'raf-schd';
import { throttle } from 'throttle-debounce';
const $wnd = $(window);
// Init infinite scroll pagination.
$(document).on('initEvents.vpf', (event, self) => {
if (event.namespace !== 'vpf' || self.options.pagination !== 'infinite') {
return;
}
const evp = `.vpf-uid-${self.uid}`;
const scrollThreshold = 400;
let visibilityCheckBusy = false;
function checkVisibilityAndLoad() {
if (visibilityCheckBusy || !self.options.nextPageUrl) {
return;
}
visibilityCheckBusy = true;
const rect = self.$item[0].getBoundingClientRect();
if (
rect.bottom > 0 &&
rect.bottom - scrollThreshold <= window.innerHeight
) {
self.loadNewItems(self.options.nextPageUrl, false, () => {
setTimeout(() => {
visibilityCheckBusy = false;
checkVisibilityAndLoad();
}, 300);
});
} else {
visibilityCheckBusy = false;
}
}
checkVisibilityAndLoad();
$wnd.on(
`load${evp} scroll${evp} resize${evp} orientationchange${evp}`,
throttle(
150,
rafSchd(() => {
checkVisibilityAndLoad();
})
)
);
});

View File

@ -0,0 +1,46 @@
import $ from 'jquery';
const { ResizeObserver } = window;
function setImgWidth($el) {
if ($el && $el.height > 1) {
$el.style.width = `${$el.height}px`;
}
}
// We need to use resize observer because for some reason in the Preview
// and on some mobile devices image height is 1px.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(({ target }) => {
if (target) {
setImgWidth(target);
}
});
});
// Init minimal paged pagination.
$(document).on('init.vpf loadedNewItems.vpf', (event, self) => {
if (
event.namespace !== 'vpf' ||
self.options.pagination !== 'paged' ||
!self.$pagination.children('.vp-pagination__style-minimal').length
) {
return;
}
// Hack used in Paged active item to make circle using hidden <img>.
// See styles for <img> tag in /templates/pagination/style.scss
const $activeItem = self.$pagination.find('.vp-pagination__item-active');
let $img = $activeItem.find('img');
if (!$img.length) {
$img = $(
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="">'
);
resizeObserver.observe($img[0]);
$activeItem.prepend($img);
setImgWidth($img[0]);
}
});

View File

@ -0,0 +1,209 @@
import $ from 'jquery';
const { VPData, VPPopupAPI } = window;
const { __, settingsPopupGallery } = VPData;
const $doc = $(document);
const $window = $(window);
if (typeof $.fancybox !== 'undefined' && VPPopupAPI) {
let fancyboxInstance;
// Extend Popup API.
VPPopupAPI.vendor = 'fancybox';
VPPopupAPI.open = function (items, index, self) {
const finalItems = [];
// prepare items for fancybox api.
items.forEach((item) => {
if (item.type === 'embed' && item.src) {
finalItems.push({
type: 'iframe',
src: item.src,
opts: {
width: item.width,
height: item.height,
caption: item.caption,
},
});
} else if (item.type === 'embed' && item.embed) {
finalItems.push({
type: 'html',
src: item.embed,
opts: {
width: item.width,
height: item.height,
caption: item.caption,
},
});
} else {
finalItems.push({
type: 'image',
src: item.src,
el: item.el,
opts: {
width: item.width,
height: item.height,
srcset: item.srcset,
caption: item.caption,
thumb: item.srcSmall,
},
});
}
});
const buttons = [];
if (settingsPopupGallery.show_zoom_button) {
buttons.push('zoom');
}
if (settingsPopupGallery.show_fullscreen_button) {
buttons.push('fullScreen');
}
if (settingsPopupGallery.show_slideshow) {
buttons.push('slideShow');
}
if (settingsPopupGallery.show_thumbs) {
buttons.push('thumbs');
}
if (settingsPopupGallery.show_share_button) {
buttons.push('share');
}
if (settingsPopupGallery.show_download_button) {
buttons.push('download');
}
if (settingsPopupGallery.show_close_button) {
buttons.push('close');
}
// define options
const options = {
// Close existing modals
// Set this to false if you do not need to stack multiple instances
closeExisting: true,
// Enable infinite gallery navigation
loop: true,
// Should display navigation arrows at the screen edges
arrows: settingsPopupGallery.show_arrows,
// Should display counter at the top left corner
infobar: settingsPopupGallery.show_counter,
// Should display close button (using `btnTpl.smallBtn` template) over the content
// Can be true, false, "auto"
// If "auto" - will be automatically enabled for "html", "inline" or "ajax" items
smallBtn: false,
// Should display toolbar (buttons at the top)
// Can be true, false, "auto"
// If "auto" - will be automatically hidden if "smallBtn" is enabled
toolbar: 'auto',
// What buttons should appear in the top right corner.
// Buttons will be created using templates from `btnTpl` option
// and they will be placed into toolbar (class="fancybox-toolbar"` element)
buttons,
// Custom CSS class for layout
baseClass: 'vp-fancybox',
// Hide browser vertical scrollbars; use at your own risk
hideScrollbar: true,
// Use mousewheel to navigate gallery
// If 'auto' - enabled for images only
wheel: false,
// Clicked on the content
clickContent(current) {
return current.type === 'image' &&
settingsPopupGallery.click_to_zoom
? 'zoom'
: false;
},
lang: 'wordpress',
i18n: {
wordpress: {
CLOSE: __.fancybox_close,
NEXT: __.fancybox_next,
PREV: __.fancybox_prev,
ERROR: __.fancybox_error,
PLAY_START: __.fancybox_play_start,
PLAY_STOP: __.fancybox_play_stop,
FULL_SCREEN: __.fancybox_full_screen,
THUMBS: __.fancybox_thumbs,
DOWNLOAD: __.fancybox_download,
SHARE: __.fancybox_share,
ZOOM: __.fancybox_zoom,
},
},
beforeClose() {
const currentItemData = items[fancyboxInstance.currIndex];
if (currentItemData) {
VPPopupAPI.maybeFocusGalleryItem(currentItemData);
}
if (self) {
self.emitEvent('beforeCloseFancybox', [
options,
items,
fancyboxInstance,
]);
}
fancyboxInstance = false;
},
beforeShow(e, instance) {
if (self) {
self.emitEvent('beforeShowFancybox', [e, instance]);
}
},
afterShow(e, instance) {
if (self) {
self.emitEvent('afterShowFancybox', [e, instance]);
}
},
};
if (self) {
self.emitEvent('beforeInitFancybox', [options, finalItems, index]);
}
// Disable Loop if only 1 item in gallery.
// We need this because Fancybox still let us scroll gallery using keyboard.
if (items.length === 1) {
options.loop = false;
}
// Start new fancybox instance
fancyboxInstance = $.fancybox.open(finalItems, options, index);
if (self) {
self.emitEvent('initFancybox', [
options,
finalItems,
index,
fancyboxInstance,
]);
}
};
VPPopupAPI.close = function () {
if (fancyboxInstance) {
fancyboxInstance.close();
fancyboxInstance = false;
}
};
// Fix zoom image sizes attribute.
// https://wordpress.org/support/topic/blurry-zoom-images/
$doc.on('transitionend', '.fancybox-content', function () {
const $img = $(this).find('.fancybox-image[sizes]');
const sizes = `${Math.round(100 * ($img.width() / $window.width()))}vw`;
$img.attr('sizes', sizes);
});
}

View File

@ -0,0 +1,130 @@
import $ from 'jquery';
// Extend VP class.
$(document).on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Init fjGallery plugin
*
* @param {mixed} options - gallery options.
* @param {mixed} additional - additional args.
*/
VP.prototype.initFjGallery = function (options = false, additional = null) {
const self = this;
if (self.$items_wrap.fjGallery && self.options.layout === 'justified') {
const initOptions =
options !== false
? options
: {
gutter: {
horizontal:
parseFloat(self.options.itemsGap) || 0,
vertical:
self.options.itemsGapVertical !== ''
? parseFloat(
self.options.itemsGapVertical
) || 0
: parseFloat(self.options.itemsGap) ||
0,
},
rowHeight:
parseFloat(self.options.justifiedRowHeight) ||
200,
maxRowsCount:
parseInt(
self.options.justifiedMaxRowsCount,
10
) || 0,
lastRow: self.options.justifiedLastRow || 'left',
rowHeightTolerance:
parseFloat(
self.options.justifiedRowHeightTolerance
) || 0,
calculateItemsHeight: true,
itemSelector: '.vp-portfolio__item-wrap',
imageSelector: '.vp-portfolio__item-img img',
transitionDuration: '0.3s',
};
if (initOptions.maxRowsCount === 0) {
initOptions.maxRowsCount = Number.POSITIVE_INFINITY;
}
self.emitEvent('beforeInitFjGallery', [initOptions, additional]);
self.$items_wrap.fjGallery(initOptions, additional);
self.emitEvent('initFjGallery', [initOptions, additional]);
}
};
/**
* Destroy fjGallery plugin
*/
VP.prototype.destroyFjGallery = function () {
const self = this;
const fjGallery = self.$items_wrap.data('fjGallery');
if (fjGallery) {
self.$items_wrap.fjGallery('destroy');
self.emitEvent('destroyFjGallery');
}
};
});
// Add Items.
$(document).on('addItems.vpf', (event, self, $items, removeExisting) => {
if (event.namespace !== 'vpf') {
return;
}
const fjGallery = self.$items_wrap.data('fjGallery');
if (!fjGallery) {
return;
}
if (removeExisting) {
self.destroyFjGallery();
self.$items_wrap.find('.vp-portfolio__item-wrap').remove();
self.$items_wrap.prepend($items);
self.initFjGallery();
} else {
self.$items_wrap.append($items);
self.initFjGallery('appendImages', $items);
}
});
// Init.
$(document).on('init.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.initFjGallery();
});
// Images Loaded.
$(document).on('imagesLoaded.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
// sometimes on iOs images failed to calculate positions, so we need this imagesLoaded event.
// related issue: https://github.com/nk-crew/visual-portfolio/issues/55
self.initFjGallery();
});
// Destroy.
$(document).on('destroy.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.destroyFjGallery();
});

View File

@ -0,0 +1,206 @@
import $ from 'jquery';
import rafSchd from 'raf-schd';
import { debounce, throttle } from 'throttle-debounce';
const { getComputedStyle } = window;
const $wnd = $(window);
const $doc = $(document);
const SUPPORTED_LAYOUTS = ['tiles', 'masonry', 'grid'];
// Extend VP class.
$doc.on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Init Isotope
* TODO: Check one of these scripts as alternative
* - https://github.com/haltu/muuri
* - https://github.com/Vestride/Shuffle
* - https://github.com/patrickkunka/mixitup
*
* @param {Object} options isotope options
*/
VP.prototype.initIsotope = function (options) {
const self = this;
if (
self.$items_wrap.isotope &&
SUPPORTED_LAYOUTS.includes(self.options.layout)
) {
const isRtl =
getComputedStyle(self.$items_wrap[0]).direction === 'rtl';
const initOptions = options || {
itemSelector: '.vp-portfolio__item-wrap',
layoutMode: 'masonry',
// masonry: {
// horizontalOrder: true
// },
transitionDuration: '0.3s',
percentPosition: true,
originLeft: !isRtl,
// See `initEvents.vpf` event why we need this option disabled.
resize: false,
};
self.emitEvent('beforeInitIsotope', [initOptions]);
self.$items_wrap.isotope(initOptions);
self.emitEvent('initIsotope', [initOptions]);
}
};
/**
* Destroy Isotope
*/
VP.prototype.destroyIsotope = function () {
const self = this;
const isotope = self.$items_wrap.data('isotope');
if (isotope) {
self.$items_wrap.isotope('destroy');
self.emitEvent('destroyIsotope');
}
};
});
// Add Items.
$doc.on('addItems.vpf', (event, self, $items, removeExisting) => {
if (event.namespace !== 'vpf') {
return;
}
const isotope = self.$items_wrap.data('isotope');
if (!isotope) {
return;
}
if (removeExisting) {
const $existing = self.$items_wrap.find('.vp-portfolio__item-wrap');
self.$items_wrap.isotope('remove', $existing);
// we need to prepend items when remove existing just because Tiles layout have troubles with appending and removing items
self.$items_wrap.prepend($items).isotope('prepended', $items);
} else {
self.$items_wrap.append($items).isotope('appended', $items);
}
// idk why, but with timeout isotope recalculate all items fine.
setTimeout(() => {
self.initIsotope('layout');
}, 0);
});
// Remove Items.
$doc.on('removeItems.vpf', (event, self, $items) => {
if (event.namespace !== 'vpf') {
return;
}
const isotope = self.$items_wrap.data('isotope');
if (!isotope) {
return;
}
self.$items_wrap.isotope('remove', $items);
});
// Init.
$doc.on('init.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.initIsotope();
});
// Images Loaded.
$doc.on('imagesLoaded.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
// sometimes on iOs images failed to calculate positions, so we need this imagesLoaded event.
// related issue: https://github.com/nk-crew/visual-portfolio/issues/55
self.initIsotope('layout');
});
// Destroy.
$doc.on('destroy.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.destroyIsotope();
});
// Init events.
$doc.on('initEvents.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
// We need to resize isotope manually, since the native relayout
// is not working properly, when container size is not changed
// but items sizes are changed in CSS. For some reason Isotope don't relayout it.
if (
self.$items_wrap.isotope &&
SUPPORTED_LAYOUTS.includes(self.options.layout)
) {
const evp = `.vpf-uid-${self.uid}`;
$wnd.on(
`resize${evp}`,
throttle(
100,
rafSchd(() => {
self.initIsotope('layout');
})
)
);
}
});
// Destroy events.
$doc.on('destroyEvents.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
if (SUPPORTED_LAYOUTS.includes(self.options.layout)) {
const evp = `.vpf-uid-${self.uid}`;
$wnd.off(`resize${evp}`);
}
});
// WPBakery Page Builder fullwidth row fix.
$doc.on(
'vc-full-width-row',
debounce(
150,
rafSchd((event, el) => {
$(el)
.find('.vp-portfolio')
.each(function () {
if (!this.vpf || !this.vpf.initIsotope) {
return;
}
const isotope = this.vpf.$items_wrap.data('isotope');
if (isotope) {
this.vpf.initIsotope('layout');
}
});
})
)
);

View File

@ -0,0 +1,515 @@
import isNumber from 'is-number';
import $ from 'jquery';
const {
Image,
VPData,
VPPopupAPI,
PhotoSwipe,
PhotoSwipeUI_Default: PhotoSwipeUIDefault,
} = window;
const { __, settingsPopupGallery } = VPData;
function resizeVideo(data, curItem) {
if (typeof curItem === 'undefined') {
if (data && data.itemHolders.length) {
data.itemHolders.forEach((val) => {
if (val.item && val.item.html) {
resizeVideo(data, val.item);
}
});
}
return;
}
// calculate real viewport in pixels
const vpW = data.viewportSize.x;
let vpH = data.viewportSize.y;
const ratio = curItem.vw / curItem.vh;
let resultW;
const $container = $(curItem.container);
const bars = data.options.barsSize;
let barTop = 0;
let barBot = 0;
if (bars) {
barTop = bars.top && bars.top !== 'auto' ? bars.top : 0;
barBot = bars.bottom && bars.bottom !== 'auto' ? bars.bottom : 0;
}
vpH -= barTop + barBot;
if (ratio > vpW / vpH) {
resultW = vpW;
} else {
resultW = vpH * ratio;
}
const $videoCont = $container.find('.vp-pswp-video');
$videoCont.css('max-width', resultW);
$videoCont.children().css({
paddingBottom: `${100 * (curItem.vh / curItem.vw)}%`,
});
$container.css({
top: barTop,
bottom: barBot,
});
}
if (PhotoSwipe && VPPopupAPI) {
let pswpInstance;
// prepare photoswipe markup
if (!$('.vp-pswp').length) {
const markup = `
<div class="pswp vp-pswp${
settingsPopupGallery.click_to_zoom ? '' : ' vp-pswp-no-zoom'
}" tabindex="-1" role="dialog" aria-hidden="true">
<div class="pswp__bg"></div>
<div class="pswp__scroll-wrap">
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__top-bar">
<div class="pswp__counter"></div>
<button class="pswp__button pswp__button--close" title="${
__.pswp_close
}"></button>
<button class="pswp__button pswp__button--share" title="${
__.pswp_share
}"></button>
<button class="pswp__button pswp__button--fs" title="${
__.pswp_fs
}"></button>
<button class="pswp__button pswp__button--zoom" title="${
__.pswp_zoom
}"></button>
</div>
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="${
__.pswp_prev
}"></button>
<button class="pswp__button pswp__button--arrow--right" title="${
__.pswp_next
}"></button>
<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>
</div>
</div>
</div>
`;
$('body').append(markup);
}
// Extend Popup API.
VPPopupAPI.vendor = 'photoswipe';
VPPopupAPI.open = function (items, index, self) {
const finalItems = [];
// prepare items for fancybox api.
items.forEach((item) => {
if (item.type === 'embed') {
finalItems.push({
html: `<div class="vp-pswp-video"><div>${item.embed}</div></div>`,
vw: item.width || 0,
vh: item.height || 0,
title: item.caption,
});
} else {
finalItems.push({
src: item.src,
el: item.el,
w: item.width || 0,
h: item.height || 0,
title: item.caption,
o: {
src: item.src,
w: item.width || 0,
h: item.height || 0,
},
...(item.srcMedium
? {
m: {
src: item.srcMedium,
w: item.srcMediumWidth || 0,
h: item.srcMediumHeight || 0,
},
msrc: item.srcMedium,
}
: {}),
});
}
});
const $pswpElement = $('.vp-pswp');
const pswpElement = $pswpElement[0];
// define options (if needed)
const options = {
captionAndToolbarShowEmptyCaptions: false,
closeEl: settingsPopupGallery.show_close_button,
captionEl: true,
fullscreenEl: settingsPopupGallery.show_fullscreen_button,
zoomEl: settingsPopupGallery.show_zoom_button,
shareEl: settingsPopupGallery.show_share_button,
counterEl: settingsPopupGallery.show_counter,
arrowEl: settingsPopupGallery.show_arrows,
shareButtons: [
{
id: 'facebook',
label: __.pswp_share_fb,
url: 'https://www.facebook.com/sharer/sharer.php?u={{url}}',
},
{
id: 'twitter',
label: __.pswp_share_tw,
url: 'https://twitter.com/intent/tweet?text={{text}}&url={{url}}',
},
{
id: 'pinterest',
label: __.pswp_share_pin,
url: 'https://www.pinterest.com/pin/create/button/?url={{url}}&media={{image_url}}&description={{text}}',
},
],
getImageURLForShare() {
const currentItem = items[pswpInstance.getCurrentIndex()];
if (currentItem.type === 'image' && currentItem.src) {
return currentItem.src;
}
return pswpInstance.currItem.src || '';
},
getPageURLForShare() {
const currentItem = items[pswpInstance.getCurrentIndex()];
if (currentItem.type === 'image' && currentItem.src) {
return currentItem.src;
}
return window.location.href;
},
getTextForShare() {
const currentItem = items[pswpInstance.getCurrentIndex()];
if (currentItem.caption) {
const $caption = $(currentItem.caption);
if (
$caption.filter('.vp-portfolio__item-popup-title')
.length
) {
return $caption
.filter('.vp-portfolio__item-popup-title')
.text();
}
if (
$caption.filter('.vp-portfolio__item-popup-description')
.length
) {
return $caption
.filter('.vp-portfolio__item-popup-description')
.text();
}
}
return '';
},
bgOpacity: 1,
tapToClose: false,
tapToToggleControls: true,
showHideOpacity: true,
history: false,
getThumbBoundsFn(thumbIndex) {
if (!finalItems[thumbIndex] || !finalItems[thumbIndex].el) {
return false;
}
const $el = $(finalItems[thumbIndex].el).find('img')[0];
if (!$el) {
return false;
}
const rect = $el.getBoundingClientRect();
const pageYScroll =
window.pageYOffset || document.documentElement.scrollTop;
const pswpTop = parseFloat($pswpElement.css('top')) || 0;
return {
x: rect.left,
y: rect.top + pageYScroll - pswpTop,
w: rect.width,
h: rect.height,
};
},
getDoubleTapZoom(isMouseClick, item) {
// isMouseClick - true if mouse, false if double-tap
// item - slide object that is zoomed, usually current
// item.initialZoomLevel - initial scale ratio of image
// e.g. if viewport is 700px and image is 1400px,
// initialZoomLevel will be 0.5
if (isMouseClick) {
// is mouse click on image or zoom icon
// Click to zoom disabled.
if (!settingsPopupGallery.click_to_zoom) {
return item.initialZoomLevel;
}
// In case the image is vertically wide, zoom it to fit screen width only.
// - check if original image size is wider than screen
// - check if zoomed out image in less than 25% of the screen width
if (
item.w > window.innerWidth &&
(item.w * item.initialZoomLevel) / window.innerWidth <
0.25
) {
return window.innerWidth / item.w;
}
// zoom to original
return 1;
// e.g. for 1400px image:
// 0.5 - zooms to 700px
// 2 - zooms to 2800px
}
// zoom to original if initial zoom is less than 0.7x,
// otherwise to 1.5x, to make sure that double-tap gesture always zooms image.
return item.initialZoomLevel < 0.7 ? 1 : 1.5;
},
};
options.index = parseInt(index, 10);
// exit if index not found
if (!isNumber(options.index)) {
return;
}
// Pass data to PhotoSwipe and initialize it
pswpInstance = new PhotoSwipe(
pswpElement,
PhotoSwipeUIDefault,
finalItems,
options
);
// see: http://photoswipe.com/documentation/responsive-images.html
let realViewportWidth;
let useLargeImages = false;
let firstResize = true;
let imageSrcWillChange;
pswpInstance.listen('beforeResize', () => {
// pswpInstance.viewportSize.x - width of PhotoSwipe viewport
// pswpInstance.viewportSize.y - height of PhotoSwipe viewport
// window.devicePixelRatio - ratio between physical pixels and device independent pixels (Number)
// 1 (regular display), 2 (@2x, retina) ...
// calculate real pixels when size changes
realViewportWidth =
pswpInstance.viewportSize.x * window.devicePixelRatio;
// Code below is needed if you want image to switch dynamically on window.resize
// Find out if current images need to be changed
if (useLargeImages && realViewportWidth < 1000) {
useLargeImages = false;
imageSrcWillChange = true;
} else if (!useLargeImages && realViewportWidth >= 1000) {
useLargeImages = true;
imageSrcWillChange = true;
}
// Invalidate items only when source is changed and when it's not the first update
if (imageSrcWillChange && !firstResize) {
// invalidateCurrItems sets a flag on slides that are in DOM,
// which will force update of content (image) on window.resize.
pswpInstance.invalidateCurrItems();
}
if (firstResize) {
firstResize = false;
}
imageSrcWillChange = false;
});
pswpInstance.listen('gettingData', (idx, item) => {
// Prepare iframes.
if (item.html) {
// -- Iframe Autoplay - Part 1 --
// Disable autoplay parameter in iframes on inactive slides.
// Mostly for Youtube and Vimeo to prevent video playing in background.
// Later we add autoplay only to active slides.
item.html = item.html.replace(/autoplay=1/, 'autoplay=0');
return;
}
// Prepare image sizes.
if (useLargeImages && item.o) {
if (item.o.src) {
item.src = item.o.src;
}
if (item.o.w) {
item.w = item.o.w;
}
if (item.o.h) {
item.h = item.o.h;
}
} else if (item.m) {
if (item.m.src) {
item.src = item.m.src;
}
if (item.m.w) {
item.w = item.m.w;
}
if (item.m.h) {
item.h = item.m.h;
}
}
});
pswpInstance.listen('imageLoadComplete', (idx, item) => {
if (item.h < 1 || item.w < 1) {
const img = new Image();
img.onload = () => {
item.w = img.width;
item.h = img.height;
pswpInstance.invalidateCurrItems();
pswpInstance.updateSize(true);
};
img.src = item.src;
}
});
pswpInstance.listen('resize', function () {
resizeVideo(this);
});
pswpInstance.listen('afterChange', function () {
resizeVideo(this);
if (self) {
self.emitEvent('afterChangePhotoSwipe', [this, pswpInstance]);
}
});
// disable video play if no active.
pswpInstance.listen('beforeChange', function () {
const data = this;
// -- Iframe Autoplay - Part 2 --
// Set autoplay to 1 on active slides and to 0 on inactive.
if (data && data.itemHolders.length) {
const currentIndex = data.getCurrentIndex();
data.itemHolders.forEach((val) => {
const $iframe = val.el
? $(val.el).find('.vp-pswp-video iframe')
: false;
if ($iframe && $iframe.length) {
if (val.index === currentIndex) {
$iframe.attr(
'src',
$iframe
.attr('src')
.replace(/autoplay=0/, 'autoplay=1')
);
} else {
$iframe.attr(
'src',
$iframe
.attr('src')
.replace(/autoplay=1/, 'autoplay=0')
);
}
}
});
}
if (self) {
self.emitEvent('beforeChangePhotoSwipe', [data, pswpInstance]);
}
});
// destroy event.
pswpInstance.listen('destroy', function () {
const data = this;
if (data) {
// Remove video block.
if (data.itemHolders.length) {
data.itemHolders.forEach((val) => {
if (val.el) {
$(val.el).find('.vp-pswp-video').remove();
}
});
}
const currentItemData = items[data.getCurrentIndex()];
if (currentItemData) {
VPPopupAPI.maybeFocusGalleryItem(currentItemData);
}
if (self) {
self.emitEvent('beforeClosePhotoSwipe', [
options,
items,
pswpInstance,
]);
}
}
pswpInstance = false;
});
if (self) {
self.emitEvent('beforeInitPhotoSwipe', [
options,
finalItems,
index,
pswpInstance,
]);
}
pswpInstance.init();
if (self) {
self.emitEvent('initPhotoSwipe', [
options,
finalItems,
index,
pswpInstance,
]);
}
};
VPPopupAPI.close = function () {
if (pswpInstance) {
pswpInstance.close();
pswpInstance = false;
}
};
}

View File

@ -0,0 +1,431 @@
import isNumber from 'is-number';
import $ from 'jquery';
const $doc = $(document);
const { screenSizes } = window.VPData;
function getSwiperVersion(Swiper) {
let ver = 8;
// in version 8 added new parameter `maxBackfaceHiddenSlides`.
if (typeof Swiper.defaults.maxBackfaceHiddenSlides === 'undefined') {
ver = 7;
}
// in version 7 added new parameter `rewind`.
if (typeof Swiper.defaults.rewind === 'undefined') {
ver = 6;
}
// in version 6 added new parameter `loopPreventsSlide`.
if (typeof Swiper.defaults.loopPreventsSlide === 'undefined') {
ver = 5;
}
return ver;
}
// Extend VP class.
$doc.on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Init Swiper plugin
*
* @param {mixed} options - slider options.
*/
VP.prototype.initSwiper = function (options = false) {
const self = this;
if (
self.options.layout === 'slider' &&
typeof window.Swiper !== 'undefined'
) {
const $parent = self.$items_wrap.parent();
$parent.addClass('swiper');
self.$items_wrap.addClass('swiper-wrapper');
self.$items_wrap.children().addClass('swiper-slide');
// calculate responsive.
let slidesPerView = self.options.sliderSlidesPerView || 3;
const breakPoints = {};
if (self.options.sliderEffect === 'fade') {
slidesPerView = 1;
}
if (isNumber(slidesPerView)) {
let count = slidesPerView;
let currentPoint = Math.min(screenSizes.length - 1, count - 1);
for (; currentPoint >= 0; currentPoint -= 1) {
if (
count > 0 &&
typeof screenSizes[currentPoint] !== 'undefined'
) {
breakPoints[screenSizes[currentPoint] + 1] = {
slidesPerView: count,
};
}
count -= 1;
}
slidesPerView = count || 1;
}
let optionsThumbs = false;
let $thumbsParent = false;
options = options || {
speed: (parseFloat(self.options.sliderSpeed) || 0) * 1000,
autoHeight: self.options.sliderItemsHeight === 'auto',
effect: self.options.sliderEffect || 'slide',
// fix fade items collapse (mostly in Default items style).
fadeEffect: {
crossFade: true,
},
spaceBetween: parseFloat(self.options.itemsGap) || 0,
centeredSlides: self.options.sliderCenteredSlides === 'true',
freeMode: {
enabled: self.options.sliderFreeMode === 'true',
sticky: self.options.sliderFreeModeSticky === 'true',
},
loop: self.options.sliderLoop === 'true',
// This feature is cool, but not working properly when loop enabled
// and fast clicking on previous button is not working properly
// https://github.com/nolimits4web/swiper/issues/5945
// loopPreventsSlide: false,
autoplay: parseFloat(self.options.sliderAutoplay) > 0 && {
delay: parseFloat(self.options.sliderAutoplay) * 1000,
disableOnInteraction: false,
},
navigation: self.options.sliderArrows === 'true' && {
nextEl: '.vp-portfolio__items-arrow-next',
prevEl: '.vp-portfolio__items-arrow-prev',
},
pagination: self.options.sliderBullets === 'true' && {
el: '.vp-portfolio__items-bullets',
clickable: true,
dynamicBullets:
self.options.sliderBulletsDynamic === 'true',
renderBullet(index, className) {
return `<span class="${className}" data-bullet-index="${index}" data-bullet-number="${
index + 1
}"></span>`;
},
},
mousewheel: self.options.sliderMousewheel === 'true',
slidesPerView,
breakpoints: breakPoints,
keyboard: true,
grabCursor: true,
preloadImages: false,
// fixes text selection when swipe in the items gap.
touchEventsTarget: 'container',
};
// fix first load slide position (seems like a conflict with lazySizes)
// issue: https://github.com/nk-crew/visual-portfolio/issues/54
if (options.speed === 0) {
options.speed = 1;
}
let positionFix = 0;
options.on = {
transitionEnd() {
if (positionFix === 0) {
positionFix = 1;
this.setTransition(1);
this.setTranslate(this.translate + 0.1);
} else if (positionFix === 1) {
positionFix = 2;
this.slideReset();
}
},
// These events used to add fixes for
// conflict with custom cursor movement.
touchStart(swiper, e) {
self.emitEvent('swiperTouchStart', [swiper, e]);
},
touchMove(swiper, e) {
self.emitEvent('swiperTouchMove', [swiper, e]);
},
touchEnd(swiper, e) {
self.emitEvent('swiperTouchEnd', [swiper, e]);
},
};
self.emitEvent('beforeInitSwiper', [options]);
// thumbnails.
if (self.$slider_thumbnails_wrap.length) {
$thumbsParent = self.$slider_thumbnails_wrap.parent();
$thumbsParent.addClass('swiper');
self.$slider_thumbnails_wrap.addClass('swiper-wrapper');
self.$slider_thumbnails_wrap
.children()
.addClass('swiper-slide');
// calculate responsive.
let thumbnailsPerView =
self.options.sliderThumbnailsPerView || 8;
const thumbnailsBreakPoints = {};
if (isNumber(thumbnailsPerView)) {
let count = thumbnailsPerView;
let currentPoint = Math.min(
screenSizes.length - 1,
count - 1
);
for (; currentPoint >= 0; currentPoint -= 1) {
if (
count > 0 &&
typeof screenSizes[currentPoint] !== 'undefined'
) {
thumbnailsBreakPoints[
screenSizes[currentPoint] + 1
] = {
slidesPerView: count,
};
}
count -= 1;
}
thumbnailsPerView = count || 1;
}
optionsThumbs = {
autoHeight: self.options.sliderThumbnailsHeight === 'auto',
effect: 'slide',
spaceBetween:
parseFloat(self.options.sliderThumbnailsGap) || 0,
loop: false,
// This feature is cool, but not working properly when loop enabled
// and fast clicking on previous button is not working properly
// https://github.com/nolimits4web/swiper/issues/5945
// loopPreventsSlide: false,
freeMode: {
enabled: true,
sticky: true,
},
loopedSlides: 5,
slidesPerView: thumbnailsPerView,
breakpoints: thumbnailsBreakPoints,
keyboard: true,
grabCursor: true,
watchSlidesVisibility: true,
watchSlidesProgress: true,
preloadImages: false,
// fixed text selection when swipe in the items gap.
touchEventsTarget: 'container',
on: {
// These events used to add fixes for
// conflict with custom cursor movement.
touchStart(swiper, e) {
self.emitEvent('swiperTouchStart', [swiper, e]);
},
touchMove(swiper, e) {
self.emitEvent('swiperTouchMove', [swiper, e]);
},
touchEnd(swiper, e) {
self.emitEvent('swiperTouchEnd', [swiper, e]);
},
},
};
}
// Fallbacks for old Swiper versions.
(() => {
const swiperVersion = getSwiperVersion(window.Swiper);
const isThumbsEnabled =
optionsThumbs && $thumbsParent && $thumbsParent[0];
// Since v7 used container class `swiper`, we should also add old `swiper-container` class.
if (swiperVersion < 7) {
$parent.addClass('swiper-container');
if (isThumbsEnabled) {
$thumbsParent.addClass('swiper-container');
}
}
// Since v7 freeMode options moved under `freeMode` object.
if (swiperVersion < 7) {
options.freeModeSticky = options.freeMode.sticky;
options.freeMode = options.freeMode.enabled;
if (isThumbsEnabled) {
optionsThumbs.freeModeSticky =
optionsThumbs.freeMode.sticky;
optionsThumbs.freeMode = optionsThumbs.freeMode.enabled;
}
}
// Since v5 `breakpointsInverse` option is removed and it is now `true` by default, but in older versions it was `false`.
if (swiperVersion >= 5) {
options.breakpointsInverse = true;
if (isThumbsEnabled) {
optionsThumbs.breakpointsInverse = true;
}
}
})();
// Init Swiper.
if (optionsThumbs && $thumbsParent && $thumbsParent[0]) {
const swiperThumbs = new window.Swiper(
$thumbsParent[0],
optionsThumbs
);
options.thumbs = {
swiper: swiperThumbs,
};
}
const instance = new window.Swiper($parent[0], options);
// Autoplay Hover Pause.
if (
self.options.sliderAutoplayHoverPause === 'true' &&
parseFloat(self.options.sliderAutoplay) > 0
) {
self.$item.on(
`mouseenter.vpf-uid-${self.uid}`,
'.swiper',
() => {
$parent[0].swiper.autoplay.stop();
}
);
self.$item.on(
`mouseleave.vpf-uid-${self.uid}`,
'.swiper',
() => {
$parent[0].swiper.autoplay.start();
}
);
}
self.emitEvent('initSwiper', [options, instance]);
}
};
/**
* Destroy Swiper plugin
*/
VP.prototype.destroySwiper = function () {
const self = this;
const $parent = self.$items_wrap.parent();
const $thumbsParent = self.$slider_thumbnails_wrap.length
? self.$slider_thumbnails_wrap.parent()
: false;
const SliderSwiper = $parent[0].swiper;
const ThumbsSwiper = $thumbsParent ? $thumbsParent[0].swiper : false;
let isDestroyed = false;
// Thumbnails.
if (ThumbsSwiper) {
ThumbsSwiper.destroy();
$thumbsParent.removeClass('swiper');
self.$slider_thumbnails_wrap.removeClass('swiper-wrapper');
self.$slider_thumbnails_wrap.children().removeClass('swiper-slide');
isDestroyed = true;
}
// Slider.
if (SliderSwiper) {
SliderSwiper.destroy();
$parent.removeClass('swiper');
self.$items_wrap.removeClass('swiper-wrapper');
self.$items_wrap.children().removeClass('swiper-slide');
$parent
.find('.vp-portfolio__items-bullets')
.removeClass(
'swiper-pagination-clickable swiper-pagination-bullets-dynamic'
)
.removeAttr('style')
.html('');
isDestroyed = true;
}
if (isDestroyed) {
self.emitEvent('destroySwiper');
}
};
});
// Add Items.
$doc.on('addItems.vpf', (event, self, $items, removeExisting, $newVP) => {
if (event.namespace !== 'vpf') {
return;
}
const Swiper = self.$items_wrap.parent()[0].swiper;
if (!Swiper) {
return;
}
// Slider.
{
if (removeExisting) {
Swiper.removeAllSlides();
}
const appendArr = [];
$items.addClass('swiper-slide').each(function () {
appendArr.push(this);
});
Swiper.appendSlide(appendArr);
}
// Thumbnails.
const ThumbsSwiper = self.$slider_thumbnails_wrap.length
? self.$slider_thumbnails_wrap.parent()[0].swiper
: false;
if (ThumbsSwiper) {
if (removeExisting) {
ThumbsSwiper.removeAllSlides();
}
const appendArr = [];
$newVP
.find('.vp-portfolio__thumbnails > .vp-portfolio__thumbnail-wrap')
.clone()
.addClass('swiper-slide')
.each(function () {
appendArr.push(this);
});
ThumbsSwiper.appendSlide(appendArr);
}
});
// Init.
$doc.on('init.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.initSwiper();
});
// Destroy.
$doc.on('destroy.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.destroySwiper();
});

View File

@ -0,0 +1,635 @@
import $ from 'jquery';
const { VPData } = window;
const { settingsPopupGallery } = VPData;
const templatesSupport = 'content' in document.createElement('template');
/*
* Global Popup Gallery API.
*/
const VPPopupAPI = {
vendor: false,
vendors: [
{
vendor: 'youtube',
embedUrl: 'https://www.youtube.com/embed/{{video_id}}?{{params}}',
pattern:
/(https?:\/\/)?(www.)?(youtube\.com|youtu\.be|youtube-nocookie\.com)\/(?:embed\/|shorts\/|v\/|watch\?v=|watch\?list=(.*)&v=|watch\?(.*[^&]&)v=)?((\w|-){11})(&list=(\w+)&?)?(.*)/,
patternIndex: 6,
params: {
autoplay: 1,
autohide: 1,
fs: 1,
rel: 0,
hd: 1,
wmode: 'transparent',
enablejsapi: 1,
html5: 1,
},
paramsIndex: 10,
embedCallback(url, match) {
let result = false;
const vendorData = this;
const videoId =
match && match[vendorData.patternIndex]
? match[vendorData.patternIndex]
: false;
if (videoId) {
const isShorts = /\/shorts\//.test(url);
const width = isShorts ? 476 : 1920;
const height = isShorts ? 847 : 1080;
result = VPPopupAPI.embedCallback(
{
...vendorData,
width,
height,
},
videoId,
url,
match
);
}
return result;
},
},
{
vendor: 'vimeo',
embedUrl: 'https://player.vimeo.com/video/{{video_id}}?{{params}}',
pattern:
/https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)(.*)/,
patternIndex: 3,
params: {
autoplay: 1,
hd: 1,
show_title: 1,
show_byline: 1,
show_portrait: 0,
fullscreen: 1,
},
paramsIndex: 4,
},
],
init() {},
open() {},
close() {},
/**
* Parse query parameters.
* Thanks to https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
*
* @param {string} query - query string.
*
* @return {string}
*/
getQueryStringParams(query) {
return query
? (/^[?#]/.test(query) ? query.slice(1) : query)
.split('&')
.reduce((params, param) => {
const [key, value] = param.split('=');
params[key] = value
? decodeURIComponent(value.replace(/\+/g, ' '))
: '';
return params;
}, {})
: {};
},
/**
* Prepare params from parsed URL.
*
* @param {Object} match - url match data.
* @param {Object} vendorData - vendor data.
*
* @return {string}
*/
prepareParams(match, vendorData) {
let result = '';
// Prepare default params.
const params = vendorData.params || {};
// Parse params from URL.
if (vendorData.paramsIndex && match && match[vendorData.paramsIndex]) {
const newParams = VPPopupAPI.getQueryStringParams(
match[vendorData.paramsIndex]
);
if (newParams && typeof newParams === 'object') {
Object.keys(newParams).forEach((key) => {
if (key && newParams[key]) {
params[key] = newParams[key];
}
});
}
}
if (params && Object.keys(params).length) {
Object.keys(params).forEach((key) => {
if (key && params[key]) {
if (result) {
result += '&';
}
result += `${key}=${params[key]}`;
}
});
}
return result;
},
/**
* Prepare data for embed.
*
* @param {Object} vendorData current video vendor data.
* @param {string} videoId parsed video ID.
* @param {string} url video URL provided.
* @param {Object | boolean} match URL match data.
*
* @return {Object}
*/
embedCallback(vendorData, videoId, url, match = false) {
let { embedUrl } = vendorData;
embedUrl = embedUrl.replace(/{{video_id}}/g, videoId);
embedUrl = embedUrl.replace(/{{video_url}}/g, url);
embedUrl = embedUrl.replace(
/{{video_url_encoded}}/g,
encodeURIComponent(url)
);
embedUrl = embedUrl.replace(
/{{params}}/g,
match ? VPPopupAPI.prepareParams(match, vendorData) : ''
);
const width = vendorData.width || 1920;
const height = vendorData.height || 1080;
return {
vendor: vendorData.vendor,
id: videoId,
embed: `<iframe width="${width}" height="${height}" src="${embedUrl}" scrolling="no" frameborder="0" allowTransparency="true" allow="accelerometer; autoplay; clipboard-write; fullscreen; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`,
embedUrl,
url,
width,
height,
};
},
/**
* Parse video URL and return object with data
*
* @param {string} url - video url.
* @param {string} url - optional poster url.
*
* @param poster
* @return {object|boolean} video data
*/
parseVideo(url, poster) {
let result = false;
VPPopupAPI.vendors.forEach((vendorData) => {
if (!result) {
const match = url.match(vendorData.pattern);
const videoId =
match && match[vendorData.patternIndex]
? match[vendorData.patternIndex]
: false;
if (videoId) {
// Custom embed callback.
if (vendorData.embedCallback) {
result = vendorData.embedCallback(url, match, poster);
// Predefined embed callback.
} else {
result = VPPopupAPI.embedCallback(
vendorData,
videoId,
url,
match
);
}
}
}
});
// Unknown vendor.
if (!result) {
result = VPPopupAPI.embedCallback(
{
vendor: 'unknown',
embedUrl: url,
},
url,
url,
false
);
}
return result;
},
/**
* Parse gallery item popup data.
*
* @param {element} itemElement - gallery item
*/
parseItem(itemElement) {
let result = false;
const $dataElement =
itemElement &&
itemElement.querySelector('.vp-portfolio__item-popup');
if ($dataElement) {
result = {
$dataElement,
$content: $dataElement,
data: $dataElement.dataset,
};
// Support for <template> tag.
if (
templatesSupport &&
$dataElement.nodeName === 'TEMPLATE' &&
$dataElement.content
) {
result.$content = $dataElement.content;
}
result.$title = result?.$content?.querySelector(
'.vp-portfolio__item-popup-title'
);
result.$description = result?.$content?.querySelector(
'.vp-portfolio__item-popup-description'
);
}
return result;
},
/**
* Parse gallery
*
* @param {jQuery} $gallery - gallery element.
*
* @return {Array} gallery data
*/
parseGallery($gallery) {
const items = [];
let size;
let item;
let video;
let videoData;
// Find all gallery items
// Skip Swiper slider duplicates.
// Previously we also used the `:not(.swiper-slide-duplicate-active)`, but it contains a valid first slide.
$gallery
.find('.vp-portfolio__item-wrap:not(.swiper-slide-duplicate)')
.each(function () {
const itemData = VPPopupAPI.parseItem(this);
if (itemData) {
size = (
itemData?.data?.vpPopupImgSize || '1920x1080'
).split('x');
video = itemData?.data?.vpPopupVideo;
videoData = false;
if (video) {
videoData = VPPopupAPI.parseVideo(
video,
itemData?.data?.vpPopupPoster
);
}
if (videoData) {
item = {
type: 'embed',
el: this,
poster: videoData.poster,
src: videoData.embedUrl,
embed: videoData.embed,
width: videoData.width || 1920,
height: videoData.height || 1080,
};
} else {
// create slide object
item = {
type: 'image',
el: this,
src: itemData?.data?.vpPopupImg,
srcset: itemData?.data?.vpPopupImgSrcset,
width: parseInt(size[0], 10),
height: parseInt(size[1], 10),
};
const srcSmall =
itemData?.data?.vpPopupSmImg || item.src;
if (srcSmall) {
const smallSize = (
itemData?.data?.vpPopupSmImgSize ||
itemData?.data?.vpPopupImgSize ||
'1920x1080'
).split('x');
item.srcSmall = srcSmall;
item.srcSmallWidth = parseInt(smallSize[0], 10);
item.srcSmallHeight = parseInt(smallSize[1], 10);
}
const srcMedium =
itemData?.data?.vpPopupMdImg || item.src;
if (srcMedium) {
const mediumSize = (
itemData?.data?.vpPopupMdImgSize ||
itemData?.data?.vpPopupImgSize ||
'1920x1080'
).split('x');
item.srcMedium = srcMedium;
item.srcMediumWidth = parseInt(mediumSize[0], 10);
item.srcMediumHeight = parseInt(mediumSize[1], 10);
}
}
if (itemData?.$title || itemData?.$description) {
item.caption =
(itemData?.$title?.outerHTML || '') +
(itemData?.$description?.outerHTML || '');
}
items.push(item);
}
});
return items;
},
/**
* Try to focus gallery item link.
* Used when popup gallery is closed.
*
* @param {Object} data - data of the current item
*/
maybeFocusGalleryItem(data) {
if (!settingsPopupGallery.restore_focus) {
return;
}
// Focus native gallery item.
if (data.linkEl) {
$(data.linkEl).focus();
// Focus Visual Portfolio gallery item.
} else if (data.el) {
$(data.el).find('.vp-portfolio__item-img > a').focus();
}
},
};
window.VPPopupAPI = VPPopupAPI;
// Extend VP class.
$(document).on('extendClass.vpf', (event, VP) => {
if (event.namespace !== 'vpf') {
return;
}
/**
* Init popup gallery
*/
VP.prototype.initPopupGallery = function () {
const self = this;
if (
!self.options.itemsClickAction ||
self.options.itemsClickAction === 'url'
) {
return;
}
// prevent on preview page
if (self.isPreview()) {
return;
}
// click action
// `a.vp-portfolio__item-overlay` added as fallback for old templates, used in themes.
self.$item.on(
`click.vpf-uid-${self.uid}`,
`
.vp-portfolio__item a.vp-portfolio__item-meta,
.vp-portfolio__item .vp-portfolio__item-img > a,
.vp-portfolio__item .vp-portfolio__item-meta-title > a,
.vp-portfolio__item a.vp-portfolio__item-overlay
`,
function (e) {
if (e.isDefaultPrevented()) {
return;
}
const $this = $(this);
let $itemWrap = $this.closest('.vp-portfolio__item-wrap');
// Use Swiper data-attribute to support slide duplicates.
if (
$itemWrap.hasClass('swiper-slide-duplicate') &&
$itemWrap.attr('data-swiper-slide-index')
) {
$itemWrap = self.$item.find(
`[data-swiper-slide-index="${$itemWrap.attr(
'data-swiper-slide-index'
)}"].swiper-slide:not(.swiper-slide-duplicate)`
);
}
if (!$itemWrap.find('.vp-portfolio__item-popup').length) {
return;
}
const items = VPPopupAPI.parseGallery(self.$item);
let index = -1;
// Get gallery item index.
// We should check all items with gallery data to prevent
// issue with items and custom URL used.
items.forEach((item, idx) => {
if (item.el === $itemWrap[0]) {
index = idx;
}
});
// Let's open popup once item index found.
if (index !== -1) {
e.preventDefault();
VPPopupAPI.open(items, index, self);
}
}
);
};
/**
* Destroy popup gallery
*/
VP.prototype.destroyPopupGallery = function () {
const self = this;
if (
!self.options.itemsClickAction ||
self.options.itemsClickAction === 'url'
) {
return;
}
self.$item.off(`click.vpf-uid-${self.uid}`);
self.emitEvent('destroyPopupGallery');
};
});
// Init.
$(document).on('init.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.initPopupGallery();
});
// Destroy.
$(document).on('destroy.vpf', (event, self) => {
if (event.namespace !== 'vpf') {
return;
}
self.destroyPopupGallery();
});
// Check if link is image.
function isLinkImage(link) {
return /(.png|.jpg|.jpeg|.gif|.tiff|.tif|.jfif|.jpe|.svg|.bmp|.webp)$/.test(
link.href.toLowerCase().split('?')[0].split('#')[0]
);
}
// Parse image data from link.
function parseImgData(link) {
const $link = $(link);
let img = link.childNodes[0];
let caption = $link.next('figcaption');
// <noscript> tag used in plugins, that adds lazy loading
if (img.nodeName === 'NOSCRIPT' && link.childNodes[1]) {
img = link.childNodes[1];
}
if (!caption.length && $link.parent('.gallery-icon').length) {
caption = $link.parent('.gallery-icon').next('figcaption');
}
caption = caption.html();
if (caption) {
caption = `<div class="vp-portfolio__item-popup-description">${caption}</div>`;
}
return {
type: 'image',
el: img,
linkEl: link,
src: link.href,
caption,
};
}
/* Popup for default WordPress images */
if (settingsPopupGallery.enable_on_wordpress_images) {
$(document).on(
'click',
`
.wp-block-image > a,
.wp-block-image > figure > a,
.wp-block-gallery .blocks-gallery-item > figure > a,
.wp-block-gallery .wp-block-image > a,
.wp-block-media-text > figure > a,
.gallery .gallery-icon > a,
figure.wp-caption > a,
figure.tiled-gallery__item > a,
p > a
`,
function (e) {
if (e.isDefaultPrevented()) {
return;
}
if (!this.childNodes.length) {
return;
}
let imageNode = this.childNodes[0];
// <noscript> tag used in plugins, that adds lazy loading
if (imageNode.nodeName === 'NOSCRIPT' && this.childNodes[1]) {
imageNode = this.childNodes[1];
}
// check if child node is <img> or <picture> tag.
// <picture> tag used in plugins, that adds WebP support
if (
imageNode.nodeName !== 'IMG' &&
imageNode.nodeName !== 'PICTURE'
) {
return;
}
// check if link is image.
if (!isLinkImage(this)) {
return;
}
e.preventDefault();
const $this = $(this);
const items = [];
const currentImage = parseImgData(this);
const $gallery = $this.closest(
'.wp-block-gallery, .gallery, .tiled-gallery__gallery'
);
let activeIndex = 0;
// Block gallery, WordPress default gallery, Jetpack gallery.
if ($gallery.length) {
const $galleryItems = $gallery.find(
'.blocks-gallery-item > figure > a, .wp-block-image > a, .gallery-icon > a, figure.tiled-gallery__item > a'
);
let i = 0;
$galleryItems.each(function () {
// check if link is image.
if (isLinkImage(this)) {
if (this === currentImage.linkEl) {
activeIndex = i;
}
items.push(parseImgData(this));
i += 1;
}
});
// WordPress gallery.
} else {
items.push(currentImage);
}
VPPopupAPI.open(items, activeIndex);
}
);
}

View File

@ -0,0 +1,93 @@
import $ from 'jquery';
const $body = $('body');
const $doc = $(document);
const $preview = $('#vp_preview');
// prevent click on links.
document.addEventListener(
'click',
(e) => {
e.stopPropagation();
e.preventDefault();
if (window.parentIFrame) {
window.parentIFrame.sendMessage('clicked');
}
},
true
);
// prevent click on <select> and similar elements.
document.addEventListener(
'mousedown',
(e) => {
e.stopPropagation();
e.preventDefault();
e.target.blur();
window.focus();
},
true
);
// add dynamic data to AJAX calls.
$doc.on('startLoadingNewItems.vpf', (event, vpObject, url, ajaxData) => {
if (event.namespace !== 'vpf') {
return;
}
ajaxData.data = Object.assign(
ajaxData.data || {},
window.vp_preview_post_data
);
});
// Dynamic CSS cache.
const dynamicCSScache = {};
// configure iFrame resizer script.
window.iFrameResizer = {
log: false,
heightCalculationMethod() {
return $preview.outerHeight(true);
},
onMessage(data) {
if (!data || !data.name) {
return;
}
switch (data.name) {
case 'resize':
// This random number needed for proper resize Isotope and other plugins.
$body.css('max-width', data.width + Math.random());
break;
case 'dynamic-css': {
// Insert dynamic styles.
const styleId = `vp-dynamic-styles-${data.blockId}-inline-css`;
// Skip if styles haven't changed.
if (
dynamicCSScache[styleId] &&
data.styles === dynamicCSScache[styleId]
) {
break;
}
let $style = $(`#${styleId}`);
if (!$style.length) {
$style = $(`<style id="${styleId}"></style>`).appendTo(
'head'
);
}
dynamicCSScache[styleId] = data.styles;
$style.text(data.styles);
break;
}
// no default
}
},
};