outlayer-8.x-1.4/js/src/outlayer.isotope.js
js/src/outlayer.isotope.js
/**
* @file
* Provides Outlayer Isotope loader.
*/
(function ($, Drupal, Isotope, _win, _doc) {
'use strict';
var _id = 'outlayer';
var _plugin = 'isotope';
var _idOnce = _id + '-' + _plugin;
var _mounted = 'is-' + _plugin + '-on';
var _selector = '.' + _id + '--' + _plugin + ':not(.' + _mounted + ')';
var _emptySel = _id + '--empty';
var _isActive = 'is-active';
/**
* Outlayer utility functions.
*
* @namespace
*/
$.outLayer.isotope = $.extend({}, $.outLayer || {}, {
$empty: null,
$items: [],
$slicks: [],
$splides: [],
$filter: null,
$sorter: null,
$search: null,
$input: null,
isIsotope: false,
useGridStack: true,
dataFilter: null,
searchString: null,
activeFilters: [],
activeSorters: [],
gridHeight: 210 + 'px',
onClass: _plugin,
/**
* Initializes the Outlayer instance.
*
* Isotope is not inited during page load until one of the filter buttons
* is hit. Unless when using Grid Custom (ungridstack) which disables
* GridStack layout for non-responsive Grid Custom which simply means you
* are on your own, need to layout the grid and make it responsive manually.
*
* @param {HTMLElement} el
* The .outlayer--isotope HTML element.
*/
load: function (el) {
var me = this;
// Only initialize it if not already, or a force for ugridstack.
if (me.$instance === null) {
me.isIsotope = true;
me.$instance = new Isotope(el, me.options);
me.$instance.on('arrangeComplete', function (filteredItems) {
// Add hints about empty results.
if ($.isElm(me.$empty)) {
var empty = filteredItems.length === 0;
el.classList[empty ? 'add' : 'remove'](_emptySel);
me.$empty.textContent = empty ? Drupal.t('No data found.') : '';
}
// Prevents annoying collapsed/ layout reflow during filtering.
el.style.minHeight = me.gridHeight;
if (!me.isEnabled(el)) {
me.resetInline(el, true);
}
});
}
},
onResizeEnd: function (e, el) {
el.style.minHeight = '';
},
resetInline: function (el, reset) {
var me = this;
var gh = me.isNativeGrid ? '' : me.gridHeight;
el = el || me.$el;
reset = reset || true;
me.rePosition(el, reset);
el.style.height = gh;
el.style.minHeight = reset ? '' : gh;
},
backToGridStack: function (el) {
var me = this;
me.$instance.destroy();
// Reset to defaults.
me.isIsotope = false;
$.removeClass(el, _emptySel);
// We have no onDestroy event, and destroy is unfortunately unclean.
// This causes unwanted transition, but at least not breaking layout.
// Isotope is NOT immediately destroyed till transitions ends.
var reset = function () {
me.resetInline(el, true);
};
_win.setTimeout(reset, 1100);
},
destroy: function (el) {
var me = this;
el = el || me.$el;
// Only destroy it using gridstack to avoid breaking gridstack layouts.
if (me.$instance && !$.isUnd(me.$instance)) {
// Only destroy if using gridstack layout.
// The layout is managed by GridStack, not Isotope.
if (me.useGridStack) {
me.backToGridStack(el);
}
else {
// Reset filter in case one of its results empty.
// The layout is managed by Isotope, not GridStack.
me.$instance.arrange({filter: '', sortBy: ''});
}
}
// Switch js-driven layouts to native CSS Grid if so required.
$.removeClass(el, 'is-ol-native');
if (me.isNativeGrid) {
$.removeClass(el, 'is-gs-layout');
}
me.revalidate();
// Slicks might be borked after being hidden during filtering, refresh.
if (me.$slicks.length) {
$.each(me.$slicks, function (el) {
el.slick.refresh();
});
}
if (me.$splides.length) {
$.each(me.$splides, function (el) {
el.splide.refresh();
});
}
},
/**
* Revalidate items.
*/
revalidate: function () {
var me = this;
var blazy = '.b-lazy:not(.b-loaded)';
var $elms = $.findAll(me.$el, blazy);
// Revalidate blazy.
if ($elms.length && me.isBlazy && Drupal.blazy && Drupal.blazy.init) {
Drupal.blazy.init.load($elms);
}
}
});
/**
* Outlayer utility functions.
*
* @param {HTMLElement} root
* The Outlayer HTML element.
*/
function process(root) {
var me = $.outLayer.isotope;
var id = $.attr(root, 'data-instance-id');
// Pass data to $.outLayer.isotope for easy reference.
me.$el = root;
me.options = $.parse($.attr(root, 'data-outlayer-isotope')) || {};
me.style = _win.getComputedStyle(root);
me.prepare(root);
var height = parseInt(me.style.getPropertyValue('height'), 0);
me.gridHeight = height + 'px';
me.$filter = $.find(_doc, '.outlayer-list--filter[data-instance-id="' + id + '"]');
me.$search = $.find(_doc, '.outlayer-list--search[data-instance-id="' + id + '"]');
me.$sorter = $.find(_doc, '.outlayer-list--sorter[data-instance-id="' + id + '"]');
me.$slicks = $.findAll(root, '.slick__slider');
me.$splides = $.findAll(root, '.splide');
me.$empty = $.find(root, '.outlayer__empty');
/**
* Filter elements on a button click.
*
* @param {Event} e
* The event triggering the filter.
*/
function doFilter(e) {
var btn = e.target;
var $active = $.find(me.$filter, '.is-active');
me.dataFilter = $.attr(btn, 'data-filter');
// Only initialize it once on an event to not mess up with gridstack.
me.init();
// Toggle the current active button class.
toggleButton($active, btn);
// Filter items.
me.$instance.arrange({
filter: me.dataFilter
});
}
/**
* Filter elements on a button click.
*
* @param {Event} e
* The event triggering the filter.
*/
function doSearch(e) {
var $target = e.target;
var searchText = $target.value;
me.$input = $target;
if (me.$input.value === '') {
doDestroy();
return;
}
// Only initialize it once on an event to not destroy gridstack.
me.init();
me.searchString = Drupal.checkPlain(searchText.toLowerCase());
me.$instance.arrange({
filter: function () {
var input = this;
return input.textContent.toLowerCase().indexOf(me.searchString) !== -1 ? true : false;
}
});
}
/**
* Sorter elements on a button click.
*
* @param {Event} e
* The event triggering the filter.
*/
function doSorter(e) {
var btn = e.target;
var value = $.attr(btn, 'data-sort-by');
var $active = $.find(me.$sorter, '.is-active');
// Only initialize it once on an event to not mess up with gridstack.
me.init();
// Toggle the current active button class.
toggleButton($active, btn);
me.$instance.arrange({sortBy: value});
}
/**
* Toggle button states.
*
* @param {HTMLElement} $active
* The active button, if any.
* @param {HTMLElement} $btn
* The current clicked button
*/
function toggleButton($active, $btn) {
// Toggle the current active button class.
if ($.isElm($active)) {
$.removeClass($active, _isActive);
}
$.addClass($btn, _isActive);
// Adjusts the class for absolute positioning required by Isotope.
$.addClass(me.$el, 'is-gs-layout');
// If using native Grid, disable absolute positioning required by Isotope.
// The .is-gs-layout basically means disabling native temporarily during
// filtering to allow Isotope take over the layout to arrange items.
// Once a .button--reset hit, let native Grid take over its layout again.
// The best of both worlds: native Grid and dynamic filtering.
// The first requires relative positioning, the latter absolute.
if (me.isNativeGrid) {
$.addClass(me.$el, 'is-ol-native');
if ($.hasClass($btn, 'button--reset')) {
$.removeClass(me.$el, 'is-gs-layout');
}
}
}
/**
* Destroy instance on a button click.
*/
function doDestroy() {
me.destroy();
if ($.isElm(me.$input)) {
me.$input.value = '';
}
}
/**
* Layout elements.
*/
function outlay() {
// Outlayer only triggered on events, not on init.
if ($.isElm(me.$filter)) {
$.on(me.$filter, 'click', '.button--filter', doFilter);
}
if ($.isElm(me.$search)) {
var onSearch = Drupal.debounce(doSearch, 250);
$.on(me.$search, 'keyup', '.form-text--search', onSearch);
}
if ($.isElm(me.$sorter)) {
me.activeSorters = $.parse($.attr(me.$sorter, 'data-sorters')) || {};
$.on(me.$sorter, 'click', '.button--sorter', doSorter);
}
$.each([me.$filter, me.$search, me.$sorter], function (cn) {
if ($.isElm(cn)) {
$.on(cn, 'click', '.button--reset', doDestroy);
}
});
// Only initialize Isotope if not using gridstack grids.
// If not using gridstack, or using native, layout items manually.
if (me.isNativeGrid || me.isUnGridStack) {
me.forceHeight = me.isNativeGrid;
}
// Prevents extra height calc if the sizer is not removed.
if (me.isValid(me.$sizer)) {
me.$sizer.classList.add('box--stamp');
}
// Only init if really ungridstack, not using GridStack layout. But
// using the non-responsive Grid Custom option. So far Isotope is never
// initialized till a filter, sorter or search button is hit.
// If ungridstack, normal Isotope layout below is triggered.
if (me.isUnGridStack) {
me.useGridStack = false;
me.init(root);
// Runs update(), including on resize event, as nobody helps here.
me.buildOut(root);
}
me.cleanUp(root);
}
outlay();
}
/**
* Attaches Outlayer behavior to HTML element.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.outLayerIsotope = {
attach: function (context) {
$.once(process, _idOnce, _selector, context);
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
$.once.removeSafely(_idOnce, _selector, context);
}
}
};
}(dBlazy, Drupal, Isotope, this, this.document));
