muuri
Version:
Responsive, sortable, filterable and draggable grid layouts.
1,453 lines (1,276 loc) • 41.9 kB
JavaScript
/**
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
*/
import {
eventSynchronize,
eventLayoutStart,
eventLayoutEnd,
eventAdd,
eventRemove,
eventShowStart,
eventShowEnd,
eventHideStart,
eventHideEnd,
eventFilter,
eventSort,
eventMove,
eventDestroy,
gridInstances,
namespace
} from '../shared.js';
import Emitter from '../Emitter/Emitter.js';
import Item from '../Item/Item.js';
import ItemAnimate from '../Item/ItemAnimate.js';
import ItemDrag from '../Item/ItemDrag.js';
import ItemLayout from '../Item/ItemLayout.js';
import ItemMigrate from '../Item/ItemMigrate.js';
import ItemRelease from '../Item/ItemRelease.js';
import ItemVisibility from '../Item/ItemVisibility.js';
import Packer from '../Packer/Packer.js';
import addClass from '../utils/addClass.js';
import arrayMove from '../utils/arrayMove.js';
import arraySwap from '../utils/arraySwap.js';
import createUid from '../utils/createUid.js';
import debounce from '../utils/debounce.js';
import elementMatches from '../utils/elementMatches.js';
import getStyle from '../utils/getStyle.js';
import getStyleAsFloat from '../utils/getStyleAsFloat.js';
import arrayInsert from '../utils/arrayInsert.js';
import isNodeList from '../utils/isNodeList.js';
import isPlainObject from '../utils/isPlainObject.js';
import removeClass from '../utils/removeClass.js';
import toArray from '../utils/toArray.js';
var packer = new Packer();
var noop = function() {};
/**
* Creates a new Grid instance.
*
* @class
* @param {(HTMLElement|String)} element
* @param {Object} [options]
* @param {(?HTMLElement[]|NodeList|String)} [options.items]
* @param {Number} [options.showDuration=300]
* @param {String} [options.showEasing="ease"]
* @param {Object} [options.visibleStyles]
* @param {Number} [options.hideDuration=300]
* @param {String} [options.hideEasing="ease"]
* @param {Object} [options.hiddenStyles]
* @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=true]
* @param {(Boolean|Number)} [options.layoutOnResize=100]
* @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 {?HtmlElement} [options.dragContainer=null]
* @param {?Function} [options.dragStartPredicate]
* @param {Number} [options.dragStartPredicate.distance=0]
* @param {Number} [options.dragStartPredicate.delay=0]
* @param {(Boolean|String)} [options.dragStartPredicate.handle=false]
* @param {?String} [options.dragAxis]
* @param {(Boolean|Function)} [options.dragSort=true]
* @param {Number} [options.dragSortInterval=100]
* @param {(Function|Object)} [options.dragSortPredicate]
* @param {Number} [options.dragSortPredicate.threshold=50]
* @param {String} [options.dragSortPredicate.action="move"]
* @param {Number} [options.dragReleaseDuration=300]
* @param {String} [options.dragReleaseEasing="ease"]
* @param {Object} [options.dragHammerSettings={touchAction: "none"}]
* @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"]
*/
function Grid(element, options) {
var inst = this;
var settings;
var items;
var layoutOnResize;
// Allow passing element as selector string. Store element for instance.
element = this._element = typeof element === 'string' ? document.querySelector(element) : element;
// Throw an error if the container element is not body element or does not
// exist within the body element.
if (!document.body.contains(element)) {
throw new Error('Container element must be an existing DOM element');
}
// Create instance settings by merging the options with default options.
settings = this._settings = mergeSettings(Grid.defaultOptions, options);
// Sanitize dragSort setting.
if (typeof settings.dragSort !== 'function') {
settings.dragSort = !!settings.dragSort;
}
// Create instance id and store it to the grid instances collection.
this._id = createUid();
gridInstances[this._id] = inst;
// Destroyed flag.
this._isDestroyed = false;
// The layout object (mutated on every layout).
this._layout = {
id: 0,
items: [],
slots: [],
setWidth: false,
setHeight: false,
width: 0,
height: 0
};
// Create private Emitter instance.
this._emitter = new Emitter();
// Add container element's class name.
addClass(element, settings.containerClass);
// Create initial items.
this._items = [];
items = settings.items;
if (typeof items === 'string') {
toArray(element.children).forEach(function(itemElement) {
if (items === '*' || elementMatches(itemElement, items)) {
inst._items.push(new Item(inst, itemElement));
}
});
} else if (Array.isArray(items) || isNodeList(items)) {
this._items = toArray(items).map(function(itemElement) {
return new Item(inst, itemElement);
});
}
// If layoutOnResize option is a valid number sanitize it and bind the resize
// handler.
layoutOnResize = settings.layoutOnResize;
if (typeof layoutOnResize !== 'number') {
layoutOnResize = layoutOnResize === true ? 0 : -1;
}
if (layoutOnResize >= 0) {
window.addEventListener(
'resize',
(inst._resizeHandler = debounce(function() {
inst.refreshItems().layout();
}, layoutOnResize))
);
}
// Layout on init if necessary.
if (settings.layoutOnInit) {
this.layout(true);
}
}
/**
* Public properties
* *****************
*/
/**
* @see Item
*/
Grid.Item = Item;
/**
* @see ItemLayout
*/
Grid.ItemLayout = ItemLayout;
/**
* @see ItemVisibility
*/
Grid.ItemVisibility = ItemVisibility;
/**
* @see ItemRelease
*/
Grid.ItemRelease = ItemRelease;
/**
* @see ItemMigrate
*/
Grid.ItemMigrate = ItemMigrate;
/**
* @see ItemAnimate
*/
Grid.ItemAnimate = ItemAnimate;
/**
* @see ItemDrag
*/
Grid.ItemDrag = ItemDrag;
/**
* @see Emitter
*/
Grid.Emitter = Emitter;
/**
* Default options for Grid instance.
*
* @public
* @memberof Grid
*/
Grid.defaultOptions = {
// 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: true
},
layoutOnResize: 100,
layoutOnInit: true,
layoutDuration: 300,
layoutEasing: 'ease',
// Sorting
sortData: null,
// Drag & Drop
dragEnabled: false,
dragContainer: null,
dragStartPredicate: {
distance: 0,
delay: 0,
handle: false
},
dragAxis: null,
dragSort: true,
dragSortInterval: 100,
dragSortPredicate: {
threshold: 50,
action: 'move'
},
dragReleaseDuration: 300,
dragReleaseEasing: 'ease',
dragHammerSettings: {
touchAction: 'none'
},
// 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'
};
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @memberof Grid.prototype
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.on = function(event, listener) {
this._emitter.on(event, listener);
return this;
};
/**
* Bind an event listener that is triggered only once.
*
* @public
* @memberof Grid.prototype
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.once = function(event, listener) {
this._emitter.once(event, listener);
return this;
};
/**
* Unbind an event listener.
*
* @public
* @memberof Grid.prototype
* @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
* @memberof Grid.prototype
* @returns {HTMLElement}
*/
Grid.prototype.getElement = function() {
return this._element;
};
/**
* Get all items. Optionally you can provide specific targets (elements and
* indices). Note that the returned array is not the same object used by the
* instance so modifying it will not affect instance's items. All items that
* are not found are omitted from the returned array.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [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 && targets !== 0)) {
return this._items.slice(0);
}
var ret = [];
var targetItems = toArray(targets);
var item;
var i;
// If target items are defined return filtered results.
for (i = 0; i < targetItems.length; i++) {
item = this._getItem(targetItems[i]);
item && ret.push(item);
}
return ret;
};
/**
* Update the cached dimensions of the instance's items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [items]
* @returns {Grid}
*/
Grid.prototype.refreshItems = function(items) {
if (this._isDestroyed) return this;
var targets = this.getItems(items);
var i;
for (i = 0; i < targets.length; i++) {
targets[i]._refreshDimensions();
}
return this;
};
/**
* Update the sort data of the instance's items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [items]
* @returns {Grid}
*/
Grid.prototype.refreshSortData = function(items) {
if (this._isDestroyed) return this;
var targetItems = this.getItems(items);
var i;
for (i = 0; i < targetItems.length; i++) {
targetItems[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
* @memberof Grid.prototype
* @returns {Grid}
*/
Grid.prototype.synchronize = function() {
if (this._isDestroyed) return this;
var container = this._element;
var items = this._items;
var fragment;
var element;
var i;
// Append all elements in order to the container element.
if (items.length) {
for (i = 0; i < items.length; i++) {
element = items[i]._element;
if (element.parentNode === container) {
fragment = fragment || document.createDocumentFragment();
fragment.appendChild(element);
}
}
if (fragment) container.appendChild(fragment);
}
// Emit synchronize event.
this._emit(eventSynchronize);
return this;
};
/**
* Calculate and apply item positions.
*
* @public
* @memberof Grid.prototype
* @param {Boolean} [instant=false]
* @param {LayoutCallback} [onFinish]
* @returns {Grid}
*/
Grid.prototype.layout = function(instant, onFinish) {
if (this._isDestroyed) return this;
var inst = this;
var element = this._element;
var layout = this._updateLayout();
var layoutId = layout.id;
var itemsLength = layout.items.length;
var counter = itemsLength;
var callback = typeof instant === 'function' ? instant : onFinish;
var isCallbackFunction = typeof callback === 'function';
var callbackItems = isCallbackFunction ? layout.items.slice(0) : null;
var isBorderBox;
var item;
var i;
// The finish function, which will be used for checking if all the items
// have laid out yet. After all items have finished their animations call
// callback and emit layoutEnd event. Only emit layoutEnd event if there
// hasn't been a new layout call during this layout.
function tryFinish() {
if (--counter > 0) return;
var hasLayoutChanged = inst._layout.id !== layoutId;
isCallbackFunction && callback(hasLayoutChanged, callbackItems);
if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) {
inst._emit(eventLayoutEnd, layout.items.slice(0));
}
}
// If grid's width or height was modified, we need to update it's cached
// dimensions. Also keep in mind that grid's cached width/height should
// always equal to what elem.getBoundingClientRect() would return, so
// therefore we need to add the grid element's borders to the dimensions if
// it's box-sizing is border-box.
if (
(layout.setHeight && typeof layout.height === 'number') ||
(layout.setWidth && typeof layout.width === 'number')
) {
isBorderBox = getStyle(element, 'box-sizing') === 'border-box';
}
if (layout.setHeight) {
if (typeof layout.height === 'number') {
element.style.height =
(isBorderBox ? layout.height + this._borderTop + this._borderBottom : layout.height) + 'px';
} else {
element.style.height = layout.height;
}
}
if (layout.setWidth) {
if (typeof layout.width === 'number') {
element.style.width =
(isBorderBox ? layout.width + this._borderLeft + this._borderRight : layout.width) + 'px';
} else {
element.style.width = layout.width;
}
}
// Emit layoutStart event. Note that this 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(eventLayoutStart)) {
this._emit(eventLayoutStart, layout.items.slice(0));
}
// If there are no items let's finish quickly.
if (!itemsLength) {
tryFinish();
return this;
}
// If there are items let's position them.
for (i = 0; i < itemsLength; i++) {
item = layout.items[i];
if (!item) continue;
// Update item's position.
item._left = layout.slots[i * 2];
item._top = layout.slots[i * 2 + 1];
// Layout item if it is not dragged.
item.isDragging() ? tryFinish() : item._layout.start(instant === true, tryFinish);
}
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
* @memberof Grid.prototype
* @param {(HTMLElement|HTMLElement[])} elements
* @param {Object} [options]
* @param {Number} [options.index=-1]
* @param {Boolean} [options.isActive]
* @param {(Boolean|LayoutCallback|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 || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var items = this._items;
var needsLayout = false;
var item;
var i;
// Map provided elements into new grid items.
for (i = 0; i < newItems.length; i++) {
item = new Item(this, newItems[i], opts.isActive);
newItems[i] = item;
// 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;
}
}
// Add the new items to the items collection to correct index.
arrayInsert(items, newItems, opts.index);
// Emit add event.
if (this._hasListeners(eventAdd)) {
this._emit(eventAdd, newItems.slice(0));
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined);
}
return newItems;
};
/**
* Remove items from the instance.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.removeElements=false]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Item[]}
*/
Grid.prototype.remove = function(items, options) {
if (this._isDestroyed) return this;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var needsLayout = false;
var allItems = this.getItems();
var targetItems = this.getItems(items);
var indices = [];
var item;
var i;
// Remove the individual items.
for (i = 0; i < targetItems.length; i++) {
item = targetItems[i];
indices.push(allItems.indexOf(item));
if (item._isActive) needsLayout = true;
item._destroy(opts.removeElements);
}
// Emit remove event.
if (this._hasListeners(eventRemove)) {
this._emit(eventRemove, targetItems.slice(0), indices);
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined);
}
return targetItems;
};
/**
* Show instance items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {ShowCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.show = function(items, options) {
if (this._isDestroyed) return this;
this._setItemsVisibility(items, true, options);
return this;
};
/**
* Hide instance items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {HideCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.hide = function(items, options) {
if (this._isDestroyed) return this;
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
* @memberof Grid.prototype
* @param {(Function|String)} predicate
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {FilterCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|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';
var isPredicateFn = typeof predicate === 'function';
var opts = options || 0;
var isInstant = opts.instant === true;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var onFinish = typeof opts.onFinish === 'function' ? 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,
onFinish: tryFinish,
layout: false
});
} else {
tryFinish();
}
// Hide items that need to be hidden.
if (itemsToHide.length) {
this.hide(itemsToHide, {
instant: isInstant,
onFinish: tryFinish,
layout: false
});
} else {
tryFinish();
}
// If there are any items to filter.
if (itemsToShow.length || itemsToHide.length) {
// Emit filter event.
if (this._hasListeners(eventFilter)) {
this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0));
}
// If layout is needed.
if (layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? 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
* @memberof Grid.prototype
* @param {(Function|Item[]|String|String[])} comparer
* @param {Object} [options]
* @param {Boolean} [options.descending=false]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.sort = (function() {
var sortComparer;
var isDescending;
var origItems;
var indexMap;
function parseCriteria(data) {
return data
.trim()
.split(' ')
.map(function(val) {
return val.split(':');
});
}
function getIndexMap(items) {
var ret = {};
for (var i = 0; i < items.length; i++) {
ret[items[i]._id] = i;
}
return ret;
}
function compareIndices(itemA, itemB) {
var indexA = indexMap[itemA._id];
var indexB = indexMap[itemB._id];
return isDescending ? indexB - indexA : indexA - indexB;
}
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.
if (!result) {
if (!indexMap) indexMap = getIndexMap(origItems);
result = compareIndices(a, b);
}
return result;
}
function customComparer(a, b) {
var result = sortComparer(a, b);
// If descending let's invert the result value.
if (isDescending && result) result = -result;
// If we have a valid result (not zero) let's return it right away.
if (result) return result;
// If result is zero let's compare the item indices to make sure we have a
// stable sort.
if (!indexMap) indexMap = getIndexMap(origItems);
return compareIndices(a, b);
}
return function(comparer, options) {
if (this._isDestroyed || this._items.length < 2) return this;
var items = this._items;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var i;
// Setup parent scope data.
sortComparer = comparer;
isDescending = !!opts.descending;
origItems = items.slice(0);
indexMap = null;
// If function is provided do a native array sort.
if (typeof sortComparer === 'function') {
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 sortComparer === 'string') {
sortComparer = parseCriteria(comparer);
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.
else if (Array.isArray(sortComparer)) {
if (sortComparer.length !== items.length) {
throw new Error('[' + namespace + '] sort reference items do not match with grid items.');
}
for (i = 0; i < items.length; i++) {
if (sortComparer.indexOf(items[i]) < 0) {
throw new Error('[' + namespace + '] sort reference items do not match with grid items.');
}
items[i] = sortComparer[i];
}
if (isDescending) items.reverse();
}
// Otherwise let's just skip it, nothing we can do here.
else {
/** @todo Maybe throw an error here? */
return this;
}
// Emit sort event.
if (this._hasListeners(eventSort)) {
this._emit(eventSort, items.slice(0), origItems);
}
// If layout is needed.
if (layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined);
}
return this;
};
})();
/**
* Move item to another index or in place of another item.
*
* @public
* @memberof Grid.prototype
* @param {GridSingleItemQuery} item
* @param {GridSingleItemQuery} 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|LayoutCallback|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 || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var isSwap = opts.action === 'swap';
var action = isSwap ? 'swap' : '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(eventMove)) {
this._emit(eventMove, {
item: fromItem,
fromIndex: fromIndex,
toIndex: toIndex,
action: action
});
}
// If layout is needed.
if (layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined);
}
}
return this;
};
/**
* Send item to another Grid instance.
*
* @public
* @memberof Grid.prototype
* @param {GridSingleItemQuery} item
* @param {Grid} grid
* @param {GridSingleItemQuery} position
* @param {Object} [options]
* @param {HTMLElement} [options.appendTo=document.body]
* @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true]
* @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true]
* @returns {Grid}
*/
Grid.prototype.send = function(item, grid, position, options) {
if (this._isDestroyed || grid._isDestroyed || this === grid) return this;
// Make sure we have a valid target item.
item = this._getItem(item);
if (!item) return this;
var opts = options || 0;
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(grid, 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',
typeof layoutSender === 'function' ? layoutSender : undefined
);
}
if (layoutReceiver) {
grid.layout(
layoutReceiver === 'instant',
typeof layoutReceiver === 'function' ? layoutReceiver : undefined
);
}
}
return this;
};
/**
* Destroy the instance.
*
* @public
* @memberof Grid.prototype
* @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 i;
// Unbind window resize event listener.
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
}
// Destroy items.
for (i = 0; i < items.length; i++) {
items[i]._destroy(removeElements);
}
// Restore container.
removeClass(container, this._settings.containerClass);
container.style.height = '';
container.style.width = '';
// Emit destroy event and unbind all events.
this._emit(eventDestroy);
this._emitter.destroy();
// Remove reference from the grid instances collection.
gridInstances[this._id] = undefined;
// Flag instance as destroyed.
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* 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
* @memberof Grid.prototype
* @param {GridSingleItemQuery} [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') {
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.
/** @todo This could be made a lot faster by using Map/WeakMap of elements. */
for (var i = 0; i < this._items.length; i++) {
if (this._items[i]._element === target) {
return this._items[i];
}
}
return null;
};
/**
* Recalculates and updates instance's layout data.
*
* @private
* @memberof Grid.prototype
* @returns {LayoutData}
*/
Grid.prototype._updateLayout = function() {
var layout = this._layout;
var settings = this._settings.layout;
var width;
var height;
var newLayout;
var i;
// Let's increment layout id.
++layout.id;
// Let's update layout items
layout.items.length = 0;
for (i = 0; i < this._items.length; i++) {
if (this._items[i]._isActive) layout.items.push(this._items[i]);
}
// Let's make sure we have the correct container dimensions.
this._refreshDimensions();
// Calculate container width and height (without borders).
width = this._width - this._borderLeft - this._borderRight;
height = this._height - this._borderTop - this._borderBottom;
// Calculate new layout.
if (typeof settings === 'function') {
newLayout = settings(layout.items, width, height);
} else {
newLayout = packer.getLayout(layout.items, width, height, layout.slots, settings);
}
// Let's update the grid's layout.
layout.slots = newLayout.slots;
layout.setWidth = Boolean(newLayout.setWidth);
layout.setHeight = Boolean(newLayout.setHeight);
layout.width = newLayout.width;
layout.height = newLayout.height;
return layout;
};
/**
* Emit a grid event.
*
* @private
* @memberof Grid.prototype
* @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
* @memberof Grid.prototype
* @param {String} event
* @returns {Boolean}
*/
Grid.prototype._hasListeners = function(event) {
var listeners = this._emitter._events[event];
return !!(listeners && listeners.length);
};
/**
* Update container's width, height and offsets.
*
* @private
* @memberof Grid.prototype
*/
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;
};
/**
* Update container's border sizes.
*
* @private
* @memberof Grid.prototype
* @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
* @memberof Grid.prototype
*/
Grid.prototype._refreshDimensions = function() {
this._updateBoundingRect();
this._updateBorders(1, 1, 1, 1);
};
/**
* Show or hide Grid instance's items.
*
* @private
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Boolean} toVisible
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {(ShowCallback|HideCallback)} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
*/
Grid.prototype._setItemsVisibility = function(items, toVisible, options) {
var grid = this;
var targetItems = this.getItems(items);
var opts = options || 0;
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 ? eventShowStart : eventHideStart;
var endEvent = toVisible ? eventShowEnd : eventHideEnd;
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 (typeof callback === 'function') callback(targetItems);
return;
}
// Emit showStart/hideStart event.
if (this._hasListeners(startEvent)) {
this._emit(startEvent, targetItems.slice(0));
}
// Show/hide 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).
if (toVisible && !item._isActive) {
item._layout._skipNextAnimation = true;
}
// If a hidden item is being shown we need to refresh the item's
// dimensions.
if (toVisible && item._visibility._isHidden) {
hiddenItems.push(item);
}
// Show/hide the item.
item._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 (typeof callback === 'function') callback(completedItems.slice(0));
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
}
});
}
// Refresh hidden items.
if (hiddenItems.length) this.refreshItems(hiddenItems);
// Layout if needed.
if (needsLayout && layout) {
this.layout(layout === 'instant', typeof layout === 'function' ? 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 ret = mergeObjects({}, defaultSettings);
// Merge user settings to default settings.
if (userSettings) {
ret = mergeObjects(ret, userSettings);
}
// Handle visible/hidden styles manually so that the whole object is
// overridden instead of the props.
ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles;
ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles;
return ret;
}
/**
* 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;
}
export default Grid;