1782 lines
50 KiB
JavaScript
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;
|