Files
ztab/app/src/vendor/muuri-src/Grid/Grid.js
yinsx bf5a3bc343 1
2026-02-02 09:07:30 +08:00

1782 lines
50 KiB
JavaScript

/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import {
ACTION_MOVE,
ACTION_SWAP,
EVENT_SYNCHRONIZE,
EVENT_LAYOUT_START,
EVENT_LAYOUT_ABORT,
EVENT_LAYOUT_END,
EVENT_ADD,
EVENT_REMOVE,
EVENT_SHOW_START,
EVENT_SHOW_END,
EVENT_HIDE_START,
EVENT_HIDE_END,
EVENT_FILTER,
EVENT_SORT,
EVENT_MOVE,
EVENT_DESTROY,
GRID_INSTANCES,
ITEM_ELEMENT_MAP,
MAX_SAFE_FLOAT32_INTEGER,
} from '../constants';
import Item from '../Item/Item';
import ItemDrag from '../Item/ItemDrag';
import ItemDragPlaceholder from '../Item/ItemDragPlaceholder';
import ItemLayout from '../Item/ItemLayout';
import ItemMigrate from '../Item/ItemMigrate';
import ItemDragRelease from '../Item/ItemDragRelease';
import ItemVisibility from '../Item/ItemVisibility';
import Emitter from '../Emitter/Emitter';
import Animator from '../Animator/Animator';
import Packer from '../Packer/Packer';
import Dragger from '../Dragger/Dragger';
import AutoScroller from '../AutoScroller/AutoScroller';
import addClass from '../utils/addClass';
import arrayInsert from '../utils/arrayInsert';
import arrayMove from '../utils/arrayMove';
import arraySwap from '../utils/arraySwap';
import createUid from '../utils/createUid';
import debounce from '../utils/debounce';
import elementMatches from '../utils/elementMatches';
import getPrefixedPropName from '../utils/getPrefixedPropName';
import getStyle from '../utils/getStyle';
import getStyleAsFloat from '../utils/getStyleAsFloat';
import isFunction from '../utils/isFunction';
import isNodeList from '../utils/isNodeList';
import isPlainObject from '../utils/isPlainObject';
import noop from '../utils/noop';
import removeClass from '../utils/removeClass';
import setStyles from '../utils/setStyles';
import toArray from '../utils/toArray';
var NUMBER_TYPE = 'number';
var STRING_TYPE = 'string';
var INSTANT_LAYOUT = 'instant';
var layoutId = 0;
/**
* Creates a new Grid instance.
*
* @class
* @param {(HTMLElement|String)} element
* @param {Object} [options]
* @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"]
* @param {Number} [options.showDuration=300]
* @param {String} [options.showEasing="ease"]
* @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}]
* @param {Number} [options.hideDuration=300]
* @param {String} [options.hideEasing="ease"]
* @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}]
* @param {(Function|Object)} [options.layout]
* @param {Boolean} [options.layout.fillGaps=false]
* @param {Boolean} [options.layout.horizontal=false]
* @param {Boolean} [options.layout.alignRight=false]
* @param {Boolean} [options.layout.alignBottom=false]
* @param {Boolean} [options.layout.rounding=false]
* @param {(Boolean|Number)} [options.layoutOnResize=150]
* @param {Boolean} [options.layoutOnInit=true]
* @param {Number} [options.layoutDuration=300]
* @param {String} [options.layoutEasing="ease"]
* @param {?Object} [options.sortData=null]
* @param {Boolean} [options.dragEnabled=false]
* @param {?String} [options.dragHandle=null]
* @param {?HtmlElement} [options.dragContainer=null]
* @param {?Function} [options.dragStartPredicate]
* @param {Number} [options.dragStartPredicate.distance=0]
* @param {Number} [options.dragStartPredicate.delay=0]
* @param {String} [options.dragAxis="xy"]
* @param {(Boolean|Function)} [options.dragSort=true]
* @param {Object} [options.dragSortHeuristics]
* @param {Number} [options.dragSortHeuristics.sortInterval=100]
* @param {Number} [options.dragSortHeuristics.minDragDistance=10]
* @param {Number} [options.dragSortHeuristics.minBounceBackAngle=1]
* @param {(Function|Object)} [options.dragSortPredicate]
* @param {Number} [options.dragSortPredicate.threshold=50]
* @param {String} [options.dragSortPredicate.action="move"]
* @param {String} [options.dragSortPredicate.migrateAction="move"]
* @param {Object} [options.dragRelease]
* @param {Number} [options.dragRelease.duration=300]
* @param {String} [options.dragRelease.easing="ease"]
* @param {Boolean} [options.dragRelease.useDragContainer=true]
* @param {Object} [options.dragCssProps]
* @param {Object} [options.dragPlaceholder]
* @param {Boolean} [options.dragPlaceholder.enabled=false]
* @param {?Function} [options.dragPlaceholder.createElement=null]
* @param {?Function} [options.dragPlaceholder.onCreate=null]
* @param {?Function} [options.dragPlaceholder.onRemove=null]
* @param {Object} [options.dragAutoScroll]
* @param {(Function|Array)} [options.dragAutoScroll.targets=[]]
* @param {?Function} [options.dragAutoScroll.handle=null]
* @param {Number} [options.dragAutoScroll.threshold=50]
* @param {Number} [options.dragAutoScroll.safeZone=0.2]
* @param {(Function|Number)} [options.dragAutoScroll.speed]
* @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true]
* @param {Boolean} [options.dragAutoScroll.smoothStop=false]
* @param {?Function} [options.dragAutoScroll.onStart=null]
* @param {?Function} [options.dragAutoScroll.onStop=null]
* @param {String} [options.containerClass="muuri"]
* @param {String} [options.itemClass="muuri-item"]
* @param {String} [options.itemVisibleClass="muuri-item-visible"]
* @param {String} [options.itemHiddenClass="muuri-item-hidden"]
* @param {String} [options.itemPositioningClass="muuri-item-positioning"]
* @param {String} [options.itemDraggingClass="muuri-item-dragging"]
* @param {String} [options.itemReleasingClass="muuri-item-releasing"]
* @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"]
*/
function Grid(element, options) {
// Allow passing element as selector string
if (typeof element === STRING_TYPE) {
element = document.querySelector(element);
}
// Throw an error if the container element is not body element or does not
// exist within the body element.
var isElementInDom = element.getRootNode
? element.getRootNode({ composed: true }) === document
: document.body.contains(element);
if (!isElementInDom || element === document.documentElement) {
throw new Error('Container element must be an existing DOM element.');
}
// Create instance settings by merging the options with default options.
var settings = mergeSettings(Grid.defaultOptions, options);
settings.visibleStyles = normalizeStyles(settings.visibleStyles);
settings.hiddenStyles = normalizeStyles(settings.hiddenStyles);
if (!isFunction(settings.dragSort)) {
settings.dragSort = !!settings.dragSort;
}
this._id = createUid();
this._element = element;
this._settings = settings;
this._isDestroyed = false;
this._items = [];
this._layout = {
id: 0,
items: [],
slots: [],
};
this._isLayoutFinished = true;
this._nextLayoutData = null;
this._emitter = new Emitter();
this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this);
// Store grid instance to the grid instances collection.
GRID_INSTANCES[this._id] = this;
// Add container element's class name.
addClass(element, settings.containerClass);
// If layoutOnResize option is a valid number sanitize it and bind the resize
// handler.
bindLayoutOnResize(this, settings.layoutOnResize);
// Add initial items.
this.add(getInitialGridElements(element, settings.items), { layout: false });
// Layout on init if necessary.
if (settings.layoutOnInit) {
this.layout(true);
}
}
/**
* Public properties
* *****************
*/
/**
* @public
* @static
* @see Item
*/
Grid.Item = Item;
/**
* @public
* @static
* @see ItemLayout
*/
Grid.ItemLayout = ItemLayout;
/**
* @public
* @static
* @see ItemVisibility
*/
Grid.ItemVisibility = ItemVisibility;
/**
* @public
* @static
* @see ItemMigrate
*/
Grid.ItemMigrate = ItemMigrate;
/**
* @public
* @static
* @see ItemDrag
*/
Grid.ItemDrag = ItemDrag;
/**
* @public
* @static
* @see ItemDragRelease
*/
Grid.ItemDragRelease = ItemDragRelease;
/**
* @public
* @static
* @see ItemDragPlaceholder
*/
Grid.ItemDragPlaceholder = ItemDragPlaceholder;
/**
* @public
* @static
* @see Emitter
*/
Grid.Emitter = Emitter;
/**
* @public
* @static
* @see Animator
*/
Grid.Animator = Animator;
/**
* @public
* @static
* @see Dragger
*/
Grid.Dragger = Dragger;
/**
* @public
* @static
* @see Packer
*/
Grid.Packer = Packer;
/**
* @public
* @static
* @see AutoScroller
*/
Grid.AutoScroller = AutoScroller;
/**
* The default Packer instance used by default for all layouts.
*
* @public
* @static
* @type {Packer}
*/
Grid.defaultPacker = new Packer(2);
/**
* Default options for Grid instance.
*
* @public
* @static
* @type {Object}
*/
Grid.defaultOptions = {
// Initial item elements
items: '*',
// Default show animation
showDuration: 300,
showEasing: 'ease',
// Default hide animation
hideDuration: 300,
hideEasing: 'ease',
// Item's visible/hidden state styles
visibleStyles: {
opacity: '1',
transform: 'scale(1)',
},
hiddenStyles: {
opacity: '0',
transform: 'scale(0.5)',
},
// Layout
layout: {
fillGaps: false,
horizontal: false,
alignRight: false,
alignBottom: false,
rounding: false,
},
layoutOnResize: 150,
layoutOnInit: true,
layoutDuration: 300,
layoutEasing: 'ease',
// Sorting
sortData: null,
// Drag & Drop
dragEnabled: false,
dragContainer: null,
dragHandle: null,
dragStartPredicate: {
distance: 0,
delay: 0,
},
dragAxis: 'xy',
dragSort: true,
dragSortLock: null,
dragSortHeuristics: {
sortInterval: 100,
minDragDistance: 10,
minBounceBackAngle: 1,
},
dragSortPredicate: {
threshold: 50,
action: ACTION_MOVE,
migrateAction: ACTION_MOVE,
},
dragRelease: {
duration: 300,
easing: 'ease',
useDragContainer: true,
},
dragCssProps: {
touchAction: 'none',
userSelect: 'none',
userDrag: 'none',
tapHighlightColor: 'rgba(0, 0, 0, 0)',
touchCallout: 'none',
contentZooming: 'none',
},
dragPlaceholder: {
enabled: false,
createElement: null,
onCreate: null,
onRemove: null,
},
dragAutoScroll: {
targets: [],
handle: null,
threshold: 50,
safeZone: 0.2,
speed: AutoScroller.smoothSpeed(1000, 2000, 2500),
sortDuringScroll: true,
smoothStop: false,
onStart: null,
onStop: null,
},
// Classnames
containerClass: 'muuri',
itemClass: 'muuri-item',
itemVisibleClass: 'muuri-item-shown',
itemHiddenClass: 'muuri-item-hidden',
itemPositioningClass: 'muuri-item-positioning',
itemDraggingClass: 'muuri-item-dragging',
itemReleasingClass: 'muuri-item-releasing',
itemPlaceholderClass: 'muuri-item-placeholder',
};
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.on = function (event, listener) {
this._emitter.on(event, listener);
return this;
};
/**
* Unbind an event listener.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.off = function (event, listener) {
this._emitter.off(event, listener);
return this;
};
/**
* Get the container element.
*
* @public
* @returns {HTMLElement}
*/
Grid.prototype.getElement = function () {
return this._element;
};
/**
* Get instance's item by element or by index. Target can also be an Item
* instance in which case the function returns the item if it exists within
* related Grid instance. If nothing is found with the provided target, null
* is returned.
*
* @private
* @param {(HtmlElement|Number|Item)} [target]
* @returns {?Item}
*/
Grid.prototype.getItem = function (target) {
// If no target is specified or the instance is destroyed, return null.
if (this._isDestroyed || (!target && target !== 0)) {
return null;
}
// If target is number return the item in that index. If the number is lower
// than zero look for the item starting from the end of the items array. For
// example -1 for the last item, -2 for the second last item, etc.
if (typeof target === NUMBER_TYPE) {
return this._items[target > -1 ? target : this._items.length + target] || null;
}
// If the target is an instance of Item return it if it is attached to this
// Grid instance, otherwise return null.
if (target instanceof Item) {
return target._gridId === this._id ? target : null;
}
// In other cases let's assume that the target is an element, so let's try
// to find an item that matches the element and return it. If item is not
// found return null.
if (ITEM_ELEMENT_MAP) {
var item = ITEM_ELEMENT_MAP.get(target);
return item && item._gridId === this._id ? item : null;
} else {
for (var i = 0; i < this._items.length; i++) {
if (this._items[i]._element === target) {
return this._items[i];
}
}
}
return null;
};
/**
* Get all items. Optionally you can provide specific targets (elements,
* indices and item instances). All items that are not found are omitted from
* the returned array.
*
* @public
* @param {(HtmlElement|Number|Item|Array)} [targets]
* @returns {Item[]}
*/
Grid.prototype.getItems = function (targets) {
// Return all items immediately if no targets were provided or if the
// instance is destroyed.
if (this._isDestroyed || targets === undefined) {
return this._items.slice(0);
}
var items = [];
var i, item;
if (Array.isArray(targets) || isNodeList(targets)) {
for (i = 0; i < targets.length; i++) {
item = this.getItem(targets[i]);
if (item) items.push(item);
}
} else {
item = this.getItem(targets);
if (item) items.push(item);
}
return items;
};
/**
* Update the cached dimensions of the instance's items. By default all the
* items are refreshed, but you can also provide an array of target items as the
* first argument if you want to refresh specific items. Note that all hidden
* items are not refreshed by default since their "display" property is "none"
* and their dimensions are therefore not readable from the DOM. However, if you
* do want to force update hidden item dimensions too you can provide `true`
* as the second argument, which makes the elements temporarily visible while
* their dimensions are being read.
*
* @public
* @param {Item[]} [items]
* @param {Boolean} [force=false]
* @returns {Grid}
*/
Grid.prototype.refreshItems = function (items, force) {
if (this._isDestroyed) return this;
var targets = items || this._items;
var i, item, style, hiddenItemStyles;
if (force === true) {
hiddenItemStyles = [];
for (i = 0; i < targets.length; i++) {
item = targets[i];
if (!item.isVisible() && !item.isHiding()) {
style = item.getElement().style;
style.visibility = 'hidden';
style.display = '';
hiddenItemStyles.push(style);
}
}
}
for (i = 0; i < targets.length; i++) {
targets[i]._refreshDimensions(force);
}
if (force === true) {
for (i = 0; i < hiddenItemStyles.length; i++) {
style = hiddenItemStyles[i];
style.visibility = '';
style.display = 'none';
}
hiddenItemStyles.length = 0;
}
return this;
};
/**
* Update the sort data of the instance's items. By default all the items are
* refreshed, but you can also provide an array of target items if you want to
* refresh specific items.
*
* @public
* @param {Item[]} [items]
* @returns {Grid}
*/
Grid.prototype.refreshSortData = function (items) {
if (this._isDestroyed) return this;
var targets = items || this._items;
for (var i = 0; i < targets.length; i++) {
targets[i]._refreshSortData();
}
return this;
};
/**
* Synchronize the item elements to match the order of the items in the DOM.
* This comes handy if you need to keep the DOM structure matched with the
* order of the items. Note that if an item's element is not currently a child
* of the container element (if it is dragged for example) it is ignored and
* left untouched.
*
* @public
* @returns {Grid}
*/
Grid.prototype.synchronize = function () {
if (this._isDestroyed) return this;
var items = this._items;
if (!items.length) return this;
var fragment;
var element;
for (var i = 0; i < items.length; i++) {
element = items[i]._element;
if (element.parentNode === this._element) {
fragment = fragment || document.createDocumentFragment();
fragment.appendChild(element);
}
}
if (!fragment) return this;
this._element.appendChild(fragment);
this._emit(EVENT_SYNCHRONIZE);
return this;
};
/**
* Calculate and apply item positions.
*
* @public
* @param {Boolean} [instant=false]
* @param {Function} [onFinish]
* @returns {Grid}
*/
Grid.prototype.layout = function (instant, onFinish) {
if (this._isDestroyed) return this;
// Cancel unfinished layout algorithm if possible.
var unfinishedLayout = this._nextLayoutData;
if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) {
unfinishedLayout.cancel();
}
// Compute layout id (let's stay in Float32 range).
layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1;
var nextLayoutId = layoutId;
// Store data for next layout.
this._nextLayoutData = {
id: nextLayoutId,
instant: instant,
onFinish: onFinish,
cancel: null,
};
// Collect layout items (all active grid items).
var items = this._items;
var layoutItems = [];
for (var i = 0; i < items.length; i++) {
if (items[i]._isActive) layoutItems.push(items[i]);
}
// Compute new layout.
this._refreshDimensions();
var gridWidth = this._width - this._borderLeft - this._borderRight;
var gridHeight = this._height - this._borderTop - this._borderBottom;
var layoutSettings = this._settings.layout;
var cancelLayout;
if (isFunction(layoutSettings)) {
cancelLayout = layoutSettings(
this,
nextLayoutId,
layoutItems,
gridWidth,
gridHeight,
this._onLayoutDataReceived
);
} else {
Grid.defaultPacker.setOptions(layoutSettings);
cancelLayout = Grid.defaultPacker.createLayout(
this,
nextLayoutId,
layoutItems,
gridWidth,
gridHeight,
this._onLayoutDataReceived
);
}
// Store layout cancel method if available.
if (
isFunction(cancelLayout) &&
this._nextLayoutData &&
this._nextLayoutData.id === nextLayoutId
) {
this._nextLayoutData.cancel = cancelLayout;
}
return this;
};
/**
* Add new items by providing the elements you wish to add to the instance and
* optionally provide the index where you want the items to be inserted into.
* All elements that are not already children of the container element will be
* automatically appended to the container element. If an element has it's CSS
* display property set to "none" it will be marked as inactive during the
* initiation process. As long as the item is inactive it will not be part of
* the layout, but it will retain it's index. You can activate items at any
* point with grid.show() method. This method will automatically call
* grid.layout() if one or more of the added elements are visible. If only
* hidden items are added no layout will be called. All the new visible items
* are positioned without animation during their first layout.
*
* @public
* @param {(HTMLElement|HTMLElement[])} elements
* @param {Object} [options]
* @param {Number} [options.index=-1]
* @param {Boolean} [options.active]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Item[]}
*/
Grid.prototype.add = function (elements, options) {
if (this._isDestroyed || !elements) return [];
var newItems = toArray(elements);
if (!newItems.length) return newItems;
var opts = options || {};
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var items = this._items;
var needsLayout = false;
var fragment;
var element;
var item;
var i;
// Collect all the elements that are not child of the grid element into a
// document fragment.
for (i = 0; i < newItems.length; i++) {
element = newItems[i];
if (element.parentNode !== this._element) {
fragment = fragment || document.createDocumentFragment();
fragment.appendChild(element);
}
}
// If we have a fragment, let's append it to the grid element. We could just
// not do this and the `new Item()` instantiation would handle this for us,
// but this way we can add the elements into the DOM a bit faster.
if (fragment) {
this._element.appendChild(fragment);
}
// Map provided elements into new grid items.
for (i = 0; i < newItems.length; i++) {
element = newItems[i];
item = newItems[i] = new Item(this, element, opts.active);
// If the item to be added is active, we need to do a layout. Also, we
// need to mark the item with the skipNextAnimation flag to make it
// position instantly (without animation) during the next layout. Without
// the hack the item would animate to it's new position from the northwest
// corner of the grid, which feels a bit buggy (imho).
if (item._isActive) {
needsLayout = true;
item._layout._skipNextAnimation = true;
}
}
// Set up the items' initial dimensions and sort data. This needs to be done
// in a separate loop to avoid layout thrashing.
for (i = 0; i < newItems.length; i++) {
item = newItems[i];
item._refreshDimensions();
item._refreshSortData();
}
// Add the new items to the items collection to correct index.
arrayInsert(items, newItems, opts.index);
// Emit add event.
if (this._hasListeners(EVENT_ADD)) {
this._emit(EVENT_ADD, newItems.slice(0));
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
return newItems;
};
/**
* Remove items from the instance.
*
* @public
* @param {Item[]} items
* @param {Object} [options]
* @param {Boolean} [options.removeElements=false]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Item[]}
*/
Grid.prototype.remove = function (items, options) {
if (this._isDestroyed || !items.length) return [];
var opts = options || {};
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var needsLayout = false;
var allItems = this.getItems();
var targetItems = [];
var indices = [];
var index;
var item;
var i;
// Remove the individual items.
for (i = 0; i < items.length; i++) {
item = items[i];
if (item._isDestroyed) continue;
index = this._items.indexOf(item);
if (index === -1) continue;
if (item._isActive) needsLayout = true;
targetItems.push(item);
indices.push(allItems.indexOf(item));
item._destroy(opts.removeElements);
this._items.splice(index, 1);
}
// Emit remove event.
if (this._hasListeners(EVENT_REMOVE)) {
this._emit(EVENT_REMOVE, targetItems.slice(0), indices);
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
return targetItems;
};
/**
* Show specific instance items.
*
* @public
* @param {Item[]} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {Boolean} [options.syncWithLayout=true]
* @param {Function} [options.onFinish]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.show = function (items, options) {
if (!this._isDestroyed && items.length) {
this._setItemsVisibility(items, true, options);
}
return this;
};
/**
* Hide specific instance items.
*
* @public
* @param {Item[]} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {Boolean} [options.syncWithLayout=true]
* @param {Function} [options.onFinish]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.hide = function (items, options) {
if (!this._isDestroyed && items.length) {
this._setItemsVisibility(items, false, options);
}
return this;
};
/**
* Filter items. Expects at least one argument, a predicate, which should be
* either a function or a string. The predicate callback is executed for every
* item in the instance. If the return value of the predicate is truthy the
* item in question will be shown and otherwise hidden. The predicate callback
* receives the item instance as it's argument. If the predicate is a string
* it is considered to be a selector and it is checked against every item
* element in the instance with the native element.matches() method. All the
* matching items will be shown and others hidden.
*
* @public
* @param {(Function|String)} predicate
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {Boolean} [options.syncWithLayout=true]
* @param {FilterCallback} [options.onFinish]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.filter = function (predicate, options) {
if (this._isDestroyed || !this._items.length) return this;
var itemsToShow = [];
var itemsToHide = [];
var isPredicateString = typeof predicate === STRING_TYPE;
var isPredicateFn = isFunction(predicate);
var opts = options || {};
var isInstant = opts.instant === true;
var syncWithLayout = opts.syncWithLayout;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null;
var tryFinishCounter = -1;
var tryFinish = noop;
var item;
var i;
// If we have onFinish callback, let's create proper tryFinish callback.
if (onFinish) {
tryFinish = function () {
++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0));
};
}
// Check which items need to be shown and which hidden.
if (isPredicateFn || isPredicateString) {
for (i = 0; i < this._items.length; i++) {
item = this._items[i];
if (isPredicateFn ? predicate(item) : elementMatches(item._element, predicate)) {
itemsToShow.push(item);
} else {
itemsToHide.push(item);
}
}
}
// Show items that need to be shown.
if (itemsToShow.length) {
this.show(itemsToShow, {
instant: isInstant,
syncWithLayout: syncWithLayout,
onFinish: tryFinish,
layout: false,
});
} else {
tryFinish();
}
// Hide items that need to be hidden.
if (itemsToHide.length) {
this.hide(itemsToHide, {
instant: isInstant,
syncWithLayout: syncWithLayout,
onFinish: tryFinish,
layout: false,
});
} else {
tryFinish();
}
// If there are any items to filter.
if (itemsToShow.length || itemsToHide.length) {
// Emit filter event.
if (this._hasListeners(EVENT_FILTER)) {
this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0));
}
// If layout is needed.
if (layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
}
return this;
};
/**
* Sort items. There are three ways to sort the items. The first is simply by
* providing a function as the comparer which works identically to native
* array sort. Alternatively you can sort by the sort data you have provided
* in the instance's options. Just provide the sort data key(s) as a string
* (separated by space) and the items will be sorted based on the provided
* sort data keys. Lastly you have the opportunity to provide a presorted
* array of items which will be used to sync the internal items array in the
* same order.
*
* @public
* @param {(Function|String|Item[])} comparer
* @param {Object} [options]
* @param {Boolean} [options.descending=false]
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.sort = (function () {
var sortComparer;
var isDescending;
var origItems;
var indexMap;
function defaultComparer(a, b) {
var result = 0;
var criteriaName;
var criteriaOrder;
var valA;
var valB;
// Loop through the list of sort criteria.
for (var i = 0; i < sortComparer.length; i++) {
// Get the criteria name, which should match an item's sort data key.
criteriaName = sortComparer[i][0];
criteriaOrder = sortComparer[i][1];
// Get items' cached sort values for the criteria. If the item has no sort
// data let's update the items sort data (this is a lazy load mechanism).
valA = (a._sortData ? a : a._refreshSortData())._sortData[criteriaName];
valB = (b._sortData ? b : b._refreshSortData())._sortData[criteriaName];
// Sort the items in descending order if defined so explicitly. Otherwise
// sort items in ascending order.
if (criteriaOrder === 'desc' || (!criteriaOrder && isDescending)) {
result = valB < valA ? -1 : valB > valA ? 1 : 0;
} else {
result = valA < valB ? -1 : valA > valB ? 1 : 0;
}
// If we have -1 or 1 as the return value, let's return it immediately.
if (result) return result;
}
// If values are equal let's compare the item indices to make sure we
// have a stable sort. Note that this is not necessary in evergreen browsers
// because Array.sort() is nowadays stable. However, in order to guarantee
// same results in older browsers we need this.
if (!result) {
if (!indexMap) indexMap = createIndexMap(origItems);
result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b);
}
return result;
}
function customComparer(a, b) {
var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b);
if (!result) {
if (!indexMap) indexMap = createIndexMap(origItems);
result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b);
}
return result;
}
return function (comparer, options) {
if (this._isDestroyed || this._items.length < 2) return this;
var items = this._items;
var opts = options || {};
var layout = opts.layout ? opts.layout : opts.layout === undefined;
// Setup parent scope data.
isDescending = !!opts.descending;
origItems = items.slice(0);
indexMap = null;
// If function is provided do a native array sort.
if (isFunction(comparer)) {
sortComparer = comparer;
items.sort(customComparer);
}
// Otherwise if we got a string, let's sort by the sort data as provided in
// the instance's options.
else if (typeof comparer === STRING_TYPE) {
sortComparer = comparer
.trim()
.split(' ')
.filter(function (val) {
return val;
})
.map(function (val) {
return val.split(':');
});
items.sort(defaultComparer);
}
// Otherwise if we got an array, let's assume it's a presorted array of the
// items and order the items based on it. Here we blindly trust that the
// presorted array consists of the same item instances as the current
// `gird._items` array.
else if (Array.isArray(comparer)) {
items.length = 0;
items.push.apply(items, comparer);
}
// Otherwise let's throw an error.
else {
sortComparer = isDescending = origItems = indexMap = null;
throw new Error('Invalid comparer argument provided.');
}
// Emit sort event.
if (this._hasListeners(EVENT_SORT)) {
this._emit(EVENT_SORT, items.slice(0), origItems);
}
// If layout is needed.
if (layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
// Reset data (to avoid mem leaks).
sortComparer = isDescending = origItems = indexMap = null;
return this;
};
})();
/**
* Move item to another index or in place of another item.
*
* @public
* @param {(HtmlElement|Number|Item)} item
* @param {(HtmlElement|Number|Item)} position
* @param {Object} [options]
* @param {String} [options.action="move"]
* - Accepts either "move" or "swap".
* - "move" moves the item in place of the other item.
* - "swap" swaps the position of the items.
* @param {(Boolean|Function|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.move = function (item, position, options) {
if (this._isDestroyed || this._items.length < 2) return this;
var items = this._items;
var opts = options || {};
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var isSwap = opts.action === ACTION_SWAP;
var action = isSwap ? ACTION_SWAP : ACTION_MOVE;
var fromItem = this.getItem(item);
var toItem = this.getItem(position);
var fromIndex;
var toIndex;
// Make sure the items exist and are not the same.
if (fromItem && toItem && fromItem !== toItem) {
// Get the indices of the items.
fromIndex = items.indexOf(fromItem);
toIndex = items.indexOf(toItem);
// Do the move/swap.
if (isSwap) {
arraySwap(items, fromIndex, toIndex);
} else {
arrayMove(items, fromIndex, toIndex);
}
// Emit move event.
if (this._hasListeners(EVENT_MOVE)) {
this._emit(EVENT_MOVE, {
item: fromItem,
fromIndex: fromIndex,
toIndex: toIndex,
action: action,
});
}
// If layout is needed.
if (layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
}
return this;
};
/**
* Send item to another Grid instance.
*
* @public
* @param {(HtmlElement|Number|Item)} item
* @param {Grid} targetGrid
* @param {(HtmlElement|Number|Item)} position
* @param {Object} [options]
* @param {HTMLElement} [options.appendTo=document.body]
* @param {(Boolean|Function|String)} [options.layoutSender=true]
* @param {(Boolean|Function|String)} [options.layoutReceiver=true]
* @returns {Grid}
*/
Grid.prototype.send = function (item, targetGrid, position, options) {
if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this;
// Make sure we have a valid target item.
item = this.getItem(item);
if (!item) return this;
var opts = options || {};
var container = opts.appendTo || document.body;
var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined;
var layoutReceiver = opts.layoutReceiver
? opts.layoutReceiver
: opts.layoutReceiver === undefined;
// Start the migration process.
item._migrate.start(targetGrid, position, container);
// If migration was started successfully and the item is active, let's layout
// the grids.
if (item._migrate._isActive && item._isActive) {
if (layoutSender) {
this.layout(
layoutSender === INSTANT_LAYOUT,
isFunction(layoutSender) ? layoutSender : undefined
);
}
if (layoutReceiver) {
targetGrid.layout(
layoutReceiver === INSTANT_LAYOUT,
isFunction(layoutReceiver) ? layoutReceiver : undefined
);
}
}
return this;
};
/**
* Destroy the instance.
*
* @public
* @param {Boolean} [removeElements=false]
* @returns {Grid}
*/
Grid.prototype.destroy = function (removeElements) {
if (this._isDestroyed) return this;
var container = this._element;
var items = this._items.slice(0);
var layoutStyles = (this._layout && this._layout.styles) || {};
var i, prop;
// Unbind window resize event listener.
unbindLayoutOnResize(this);
// Destroy items.
for (i = 0; i < items.length; i++) items[i]._destroy(removeElements);
this._items.length = 0;
// Restore container.
removeClass(container, this._settings.containerClass);
for (prop in layoutStyles) container.style[prop] = '';
// Emit destroy event and unbind all events.
this._emit(EVENT_DESTROY);
this._emitter.destroy();
// Remove reference from the grid instances collection.
delete GRID_INSTANCES[this._id];
// Flag instance as destroyed.
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Emit a grid event.
*
* @private
* @param {String} event
* @param {...*} [arg]
*/
Grid.prototype._emit = function () {
if (this._isDestroyed) return;
this._emitter.emit.apply(this._emitter, arguments);
};
/**
* Check if there are any events listeners for an event.
*
* @private
* @param {String} event
* @returns {Boolean}
*/
Grid.prototype._hasListeners = function (event) {
if (this._isDestroyed) return false;
return this._emitter.countListeners(event) > 0;
};
/**
* Update container's width, height and offsets.
*
* @private
*/
Grid.prototype._updateBoundingRect = function () {
var element = this._element;
var rect = element.getBoundingClientRect();
this._width = rect.width;
this._height = rect.height;
this._left = rect.left;
this._top = rect.top;
this._right = rect.right;
this._bottom = rect.bottom;
};
/**
* Update container's border sizes.
*
* @private
* @param {Boolean} left
* @param {Boolean} right
* @param {Boolean} top
* @param {Boolean} bottom
*/
Grid.prototype._updateBorders = function (left, right, top, bottom) {
var element = this._element;
if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width');
if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width');
if (top) this._borderTop = getStyleAsFloat(element, 'border-top-width');
if (bottom) this._borderBottom = getStyleAsFloat(element, 'border-bottom-width');
};
/**
* Refresh all of container's internal dimensions and offsets.
*
* @private
*/
Grid.prototype._refreshDimensions = function () {
this._updateBoundingRect();
this._updateBorders(1, 1, 1, 1);
this._boxSizing = getStyle(this._element, 'box-sizing');
};
/**
* Calculate and apply item positions.
*
* @private
* @param {Object} layout
*/
Grid.prototype._onLayoutDataReceived = (function () {
var itemsToLayout = [];
return function (layout) {
if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return;
var grid = this;
var instant = this._nextLayoutData.instant;
var onFinish = this._nextLayoutData.onFinish;
var numItems = layout.items.length;
var counter = numItems;
var item;
var left;
var top;
var i;
// Reset next layout data.
this._nextLayoutData = null;
if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) {
this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0));
}
// Update the layout reference.
this._layout = layout;
// Update the item positions and collect all items that need to be laid
// out. It is critical that we update the item position _before_ the
// layoutStart event as the new data might be needed in the callback.
itemsToLayout.length = 0;
for (i = 0; i < numItems; i++) {
item = layout.items[i];
// Make sure we have a matching item.
if (!item) {
--counter;
continue;
}
// Get the item's new left and top values.
left = layout.slots[i * 2];
top = layout.slots[i * 2 + 1];
// Let's skip the layout process if we can. Possibly avoids a lot of DOM
// operations which saves us some CPU cycles.
if (item._canSkipLayout(left, top)) {
--counter;
continue;
}
// Update the item's position.
item._left = left;
item._top = top;
// Only active non-dragged items need to be moved.
if (item.isActive() && !item.isDragging()) {
itemsToLayout.push(item);
} else {
--counter;
}
}
// Set layout styles to the grid element.
if (layout.styles) {
setStyles(this._element, layout.styles);
}
// layoutStart event is intentionally emitted after the container element's
// dimensions are set, because otherwise there would be no hook for reacting
// to container dimension changes.
if (this._hasListeners(EVENT_LAYOUT_START)) {
this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true);
// Let's make sure that the current layout process has not been overridden
// in the layoutStart event, and if so, let's stop processing the aborted
// layout.
if (this._layout.id !== layout.id) return;
}
var tryFinish = function () {
if (--counter > 0) return;
var hasLayoutChanged = grid._layout.id !== layout.id;
var callback = isFunction(instant) ? instant : onFinish;
if (!hasLayoutChanged) {
grid._isLayoutFinished = true;
}
if (isFunction(callback)) {
callback(layout.items.slice(0), hasLayoutChanged);
}
if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) {
grid._emit(EVENT_LAYOUT_END, layout.items.slice(0));
}
};
if (!itemsToLayout.length) {
tryFinish();
return this;
}
this._isLayoutFinished = false;
for (i = 0; i < itemsToLayout.length; i++) {
if (this._layout.id !== layout.id) break;
itemsToLayout[i]._layout.start(instant === true, tryFinish);
}
if (this._layout.id === layout.id) {
itemsToLayout.length = 0;
}
return this;
};
})();
/**
* Show or hide Grid instance's items.
*
* @private
* @param {Item[]} items
* @param {Boolean} toVisible
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {Boolean} [options.syncWithLayout=true]
* @param {Function} [options.onFinish]
* @param {(Boolean|Function|String)} [options.layout=true]
*/
Grid.prototype._setItemsVisibility = function (items, toVisible, options) {
var grid = this;
var targetItems = items.slice(0);
var opts = options || {};
var isInstant = opts.instant === true;
var callback = opts.onFinish;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var counter = targetItems.length;
var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START;
var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END;
var method = toVisible ? 'show' : 'hide';
var needsLayout = false;
var completedItems = [];
var hiddenItems = [];
var item;
var i;
// If there are no items call the callback, but don't emit any events.
if (!counter) {
if (isFunction(callback)) callback(targetItems);
return;
}
// Prepare the items.
for (i = 0; i < targetItems.length; i++) {
item = targetItems[i];
// If inactive item is shown or active item is hidden we need to do
// layout.
if ((toVisible && !item._isActive) || (!toVisible && item._isActive)) {
needsLayout = true;
}
// If inactive item is shown we also need to do a little hack to make the
// item not animate it's next positioning (layout).
item._layout._skipNextAnimation = !!(toVisible && !item._isActive);
// If a hidden item is being shown we need to refresh the item's
// dimensions.
if (toVisible && item._visibility._isHidden) {
hiddenItems.push(item);
}
// Add item to layout or remove it from layout.
if (toVisible) {
item._addToLayout();
} else {
item._removeFromLayout();
}
}
// Force refresh the dimensions of all hidden items.
if (hiddenItems.length) {
this.refreshItems(hiddenItems, true);
hiddenItems.length = 0;
}
// Show the items in sync with the next layout.
function triggerVisibilityChange() {
if (needsLayout && opts.syncWithLayout !== false) {
grid.off(EVENT_LAYOUT_START, triggerVisibilityChange);
}
if (grid._hasListeners(startEvent)) {
grid._emit(startEvent, targetItems.slice(0));
}
for (i = 0; i < targetItems.length; i++) {
// Make sure the item is still in the original grid. There is a chance
// that the item starts migrating before tiggerVisibilityChange is called.
if (targetItems[i]._gridId !== grid._id) {
if (--counter < 1) {
if (isFunction(callback)) callback(completedItems.slice(0));
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
}
continue;
}
targetItems[i]._visibility[method](isInstant, function (interrupted, item) {
// If the current item's animation was not interrupted add it to the
// completedItems array.
if (!interrupted) completedItems.push(item);
// If all items have finished their animations call the callback
// and emit showEnd/hideEnd event.
if (--counter < 1) {
if (isFunction(callback)) callback(completedItems.slice(0));
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
}
});
}
}
// Trigger the visibility change, either async with layout or instantly.
if (needsLayout && opts.syncWithLayout !== false) {
this.on(EVENT_LAYOUT_START, triggerVisibilityChange);
} else {
triggerVisibilityChange();
}
// Trigger layout if needed.
if (needsLayout && layout) {
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
}
};
/**
* Private helpers
* ***************
*/
/**
* Merge default settings with user settings. The returned object is a new
* object with merged values. The merging is a deep merge meaning that all
* objects and arrays within the provided settings objects will be also merged
* so that modifying the values of the settings object will have no effect on
* the returned object.
*
* @param {Object} defaultSettings
* @param {Object} [userSettings]
* @returns {Object} Returns a new object.
*/
function mergeSettings(defaultSettings, userSettings) {
// Create a fresh copy of default settings.
var settings = mergeObjects({}, defaultSettings);
// Merge user settings to default settings.
if (userSettings) {
settings = mergeObjects(settings, userSettings);
}
// Handle visible/hidden styles manually so that the whole object is
// overridden instead of the props.
if (userSettings && userSettings.visibleStyles) {
settings.visibleStyles = userSettings.visibleStyles;
} else if (defaultSettings && defaultSettings.visibleStyles) {
settings.visibleStyles = defaultSettings.visibleStyles;
}
if (userSettings && userSettings.hiddenStyles) {
settings.hiddenStyles = userSettings.hiddenStyles;
} else if (defaultSettings && defaultSettings.hiddenStyles) {
settings.hiddenStyles = defaultSettings.hiddenStyles;
}
return settings;
}
/**
* Merge two objects recursively (deep merge). The source object's properties
* are merged to the target object.
*
* @param {Object} target
* - The target object.
* @param {Object} source
* - The source object.
* @returns {Object} Returns the target object.
*/
function mergeObjects(target, source) {
var sourceKeys = Object.keys(source);
var length = sourceKeys.length;
var isSourceObject;
var propName;
var i;
for (i = 0; i < length; i++) {
propName = sourceKeys[i];
isSourceObject = isPlainObject(source[propName]);
// If target and source values are both objects, merge the objects and
// assign the merged value to the target property.
if (isPlainObject(target[propName]) && isSourceObject) {
target[propName] = mergeObjects(mergeObjects({}, target[propName]), source[propName]);
continue;
}
// If source's value is object and target's is not let's clone the object as
// the target's value.
if (isSourceObject) {
target[propName] = mergeObjects({}, source[propName]);
continue;
}
// If source's value is an array let's clone the array as the target's
// value.
if (Array.isArray(source[propName])) {
target[propName] = source[propName].slice(0);
continue;
}
// In all other cases let's just directly assign the source's value as the
// target's value.
target[propName] = source[propName];
}
return target;
}
/**
* Collect and return initial items for grid.
*
* @param {HTMLElement} gridElement
* @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements
* @returns {(HTMLElement[]|NodeList|HtmlCollection)}
*/
function getInitialGridElements(gridElement, elements) {
// If we have a wildcard selector let's return all the children.
if (elements === '*') {
return gridElement.children;
}
// If we have some more specific selector, let's filter the elements.
if (typeof elements === STRING_TYPE) {
var result = [];
var children = gridElement.children;
for (var i = 0; i < children.length; i++) {
if (elementMatches(children[i], elements)) {
result.push(children[i]);
}
}
return result;
}
// If we have an array of elements or a node list.
if (Array.isArray(elements) || isNodeList(elements)) {
return elements;
}
// Otherwise just return an empty array.
return [];
}
/**
* Bind grid's resize handler to window.
*
* @param {Grid} grid
* @param {(Number|Boolean)} delay
*/
function bindLayoutOnResize(grid, delay) {
if (typeof delay !== NUMBER_TYPE) {
delay = delay === true ? 0 : -1;
}
if (delay >= 0) {
grid._resizeHandler = debounce(function () {
grid.refreshItems().layout();
}, delay);
window.addEventListener('resize', grid._resizeHandler);
}
}
/**
* Unbind grid's resize handler from window.
*
* @param {Grid} grid
*/
function unbindLayoutOnResize(grid) {
if (grid._resizeHandler) {
grid._resizeHandler(true);
window.removeEventListener('resize', grid._resizeHandler);
grid._resizeHandler = null;
}
}
/**
* Normalize style declaration object, returns a normalized (new) styles object
* (prefixed properties and invalid properties removed).
*
* @param {Object} styles
* @returns {Object}
*/
function normalizeStyles(styles) {
var normalized = {};
var docElemStyle = document.documentElement.style;
var prop, prefixedProp;
// Normalize visible styles (prefix and remove invalid).
for (prop in styles) {
if (!styles[prop]) continue;
prefixedProp = getPrefixedPropName(docElemStyle, prop);
if (!prefixedProp) continue;
normalized[prefixedProp] = styles[prop];
}
return normalized;
}
/**
* Create index map from items.
*
* @param {Item[]} items
* @returns {Object}
*/
function createIndexMap(items) {
var result = {};
for (var i = 0; i < items.length; i++) {
result[items[i]._id] = i;
}
return result;
}
/**
* Sort comparer function for items' index map.
*
* @param {Object} indexMap
* @param {Item} itemA
* @param {Item} itemB
* @returns {Number}
*/
function compareIndexMap(indexMap, itemA, itemB) {
var indexA = indexMap[itemA._id];
var indexB = indexMap[itemB._id];
return indexA - indexB;
}
export default Grid;