wp_back/wp-content/plugins/visual-portfolio/assets/js/main.js
2024-05-20 15:37:46 +03:00

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();
});
}