996 lines
22 KiB
JavaScript
996 lines
22 KiB
JavaScript
|
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();
|
||
|
});
|
||
|
}
|