diff --git a/.jshintrc b/.jshintrc index a9be14e..550e289 100644 --- a/.jshintrc +++ b/.jshintrc @@ -32,7 +32,7 @@ "indent": 2, // Prohibit use of a variable before it is defined. - "latedef": true, + "latedef": "nofunc", // Enforce line length to 100 characters "maxlen": 100, diff --git a/dist/shuffle.js b/dist/shuffle.js index 305d89c..662028f 100644 --- a/dist/shuffle.js +++ b/dist/shuffle.js @@ -64,6 +64,10 @@ return /******/ (function(modules) { // webpackBootstrap var _matchesSelector2 = _interopRequireDefault(_matchesSelector); + var _throttle = __webpack_require__(10); + + var _throttle2 = _interopRequireDefault(_throttle); + var _point = __webpack_require__(2); var _point2 = _interopRequireDefault(_point); @@ -76,15 +80,19 @@ return /******/ (function(modules) { // webpackBootstrap var _classes2 = _interopRequireDefault(_classes); - var _getNumber = __webpack_require__(3); + var _getNumberStyle = __webpack_require__(6); - var _getNumber2 = _interopRequireDefault(_getNumber); + var _getNumberStyle2 = _interopRequireDefault(_getNumberStyle); - var _sorter = __webpack_require__(6); + var _sorter = __webpack_require__(8); var _sorter2 = _interopRequireDefault(_sorter); - __webpack_require__(7); + var _assign = __webpack_require__(11); + + var _assign2 = _interopRequireDefault(_assign); + + __webpack_require__(9); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } @@ -94,52 +102,6 @@ return /******/ (function(modules) { // webpackBootstrap return Array.prototype.slice.call(arrayLike); } - // Constants - var SHUFFLE = 'shuffle'; - - // Configurable. You can change these constants to fit your application. - // The default scale and concealed scale, however, have to be different values. - var ALL_ITEMS = 'all'; - var FILTER_ATTRIBUTE_KEY = 'groups'; - var DEFAULT_SCALE = 1; - var CONCEALED_SCALE = 0.001; - - // Underscore's throttle function. - function throttle(func, wait, options) { - var context, args, result; - var timeout = null; - var previous = 0; - options = options || {}; - var later = function later() { - previous = options.leading === false ? 0 : Date.now(); - timeout = null; - result = func.apply(context, args); - context = args = null; - }; - - return function () { - var now = Date.now(); - if (!previous && options.leading === false) { - previous = now; - } - - var remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0 || remaining > wait) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - context = args = null; - } else if (!timeout && options.trailing !== false) { - timeout = setTimeout(later, remaining); - } - - return result; - }; - } - function each(obj, iterator, context) { for (var i = 0, length = obj.length; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === {}) { @@ -160,21 +122,7 @@ return /******/ (function(modules) { // webpackBootstrap return Math.min.apply(Math, array); } - var getStyles = window.getComputedStyle; - - var COMPUTED_SIZE_INCLUDES_PADDING = function () { - var parent = document.body || document.documentElement; - var e = document.createElement('div'); - e.style.cssText = 'width:10px;padding:2px;box-sizing:border-box;'; - parent.appendChild(e); - - var width = getStyles(e, null).width; - var ret = width === '10px'; - - parent.removeChild(e); - - return ret; - }(); + function noop() {} // Used for unique instance variables var id = 0; @@ -194,7 +142,18 @@ return /******/ (function(modules) { // webpackBootstrap _classCallCheck(this, Shuffle); - Object.assign(this, Shuffle.options, options, Shuffle.settings); + (0, _assign2.default)(this, Shuffle.options, options); + + this.useSizer = false; + this.revealAppendedDelay = 300; + this.lastSort = {}; + this.lastFilter = Shuffle.ALL_ITEMS; + this.isEnabled = true; + this.isDestroyed = false; + this.isInitialized = false; + this._transitions = []; + this._isMovementCanceled = false; + this._queue = []; element = this._getElementOption(element); @@ -211,7 +170,7 @@ return /******/ (function(modules) { // webpackBootstrap // Dispatch the done event asynchronously so that people can bind to it after // Shuffle has been initialized. defer(function () { - this.initialized = true; + this.isInitialized = true; this._dispatch(Shuffle.EventType.DONE); }, this, 16); } @@ -239,7 +198,7 @@ return /******/ (function(modules) { // webpackBootstrap // Get container css all in one request. Causes reflow var containerCSS = window.getComputedStyle(this.element, null); - var containerWidth = Shuffle._getOuterWidth(this.element); + var containerWidth = Shuffle.getSize(this.element).width; // Add styles to the container if it doesn't have them. this._validateStyles(containerCSS); @@ -249,16 +208,14 @@ return /******/ (function(modules) { // webpackBootstrap this._setColumns(containerWidth); // Kick off! - this.shuffle(this.group, this.initialSort); + this.filter(this.group, this.initialSort); // The shuffle items haven't had transitions set on them yet // so the user doesn't see the first layout. Set them now that the first layout is done. - if (this.supported) { - defer(function () { - this._setTransitions(); - this.element.style.transition = 'height ' + this.speed + 'ms ' + this.easing; - }, this); - } + defer(function () { + this._setTransitions(); + this.element.style.transition = 'height ' + this.speed + 'ms ' + this.easing; + }, this); } /** @@ -351,7 +308,7 @@ return /******/ (function(modules) { // webpackBootstrap this.group = category; } - return set.filtered; + return set; } /** @@ -371,7 +328,7 @@ return /******/ (function(modules) { // webpackBootstrap var concealed = []; // category === 'all', add filtered class to everything - if (category === ALL_ITEMS) { + if (category === Shuffle.ALL_ITEMS) { filtered = items; // Loop through each item and use provided function to determine @@ -411,7 +368,7 @@ return /******/ (function(modules) { // webpackBootstrap // Check each element's data-groups attribute against the given category. } else { var _ret = function () { - var attr = item.element.getAttribute('data-' + FILTER_ATTRIBUTE_KEY); + var attr = item.element.getAttribute('data-' + Shuffle.FILTER_ATTRIBUTE_KEY); var groups = JSON.parse(attr); var keys = _this3.delimeter && !Array.isArray(groups) ? groups.split(_this3.delimeter) : groups; @@ -480,18 +437,6 @@ return /******/ (function(modules) { // webpackBootstrap this.visibleItems = this._getFilteredItems().length; } - /** - * Sets css transform transition on a an element. - * @param {Element} element Element to set transition on. - * @private - */ - - }, { - key: '_setTransition', - value: function _setTransition(element) { - element.style.transition = 'transform ' + this.speed + 'ms ' + this.easing + ', opacity ' + this.speed + 'ms ' + this.easing; - } - /** * Sets css transform transition on a group of elements. * @param {ArrayLike.} $items Elements to set transitions on. @@ -503,8 +448,18 @@ return /******/ (function(modules) { // webpackBootstrap value: function _setTransitions() { var items = arguments.length <= 0 || arguments[0] === undefined ? this.items : arguments[0]; + var speed = this.speed; + var easing = this.easing; + + var str; + if (this.useTransforms) { + str = 'transform ' + speed + 'ms ' + easing + ', opacity ' + speed + 'ms ' + easing; + } else { + str = 'top ' + speed + 'ms ' + easing + ', left ' + speed + 'ms ' + easing + ', opacity ' + speed + 'ms ' + easing; + } + each(items, function (item) { - this._setTransition(item.element); + item.element.style.transition = str; }, this); } @@ -517,11 +472,9 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_setSequentialDelay', value: function _setSequentialDelay($collection) { - if (!this.supported) { - return; - } // $collection can be an array of dom elements or jquery object + // FIXME won't work for noTransforms each($collection, function (el, i) { // This works because the transition-property: transform, opacity; el.style.transitionDelay = '0ms,' + (i + 1) * this.sequentialFadeDelay + 'ms'; @@ -572,7 +525,7 @@ return /******/ (function(modules) { // webpackBootstrap // columnWidth option isn't a function, are they using a sizing element? } else if (this.useSizer) { - size = Shuffle._getOuterWidth(this.sizer); + size = Shuffle.getSize(this.sizer).width; // if not, how about the explicitly set option? } else if (this.columnWidth) { @@ -580,7 +533,7 @@ return /******/ (function(modules) { // webpackBootstrap // or use the size of the first item } else if (this.items.length > 0) { - size = Shuffle._getOuterWidth(this.items[0], true); + size = Shuffle.getSize(this.items[0], true).width; // if there's no items, use size of container } else { @@ -609,7 +562,7 @@ return /******/ (function(modules) { // webpackBootstrap if (typeof this.gutterWidth === 'function') { size = this.gutterWidth(containerWidth); } else if (this.useSizer) { - size = Shuffle._getNumberStyle(this.sizer, 'marginLeft'); + size = (0, _getNumberStyle2.default)(this.sizer, 'marginLeft'); } else { size = this.gutterWidth; } @@ -619,14 +572,15 @@ return /******/ (function(modules) { // webpackBootstrap /** * Calculate the number of columns to be used. Gets css if using sizer element. - * @param {number} [theContainerWidth] Optionally specify a container width if + * @param {number} [containerWidth] Optionally specify a container width if * it's already available. */ }, { key: '_setColumns', - value: function _setColumns(theContainerWidth) { - var containerWidth = theContainerWidth || Shuffle._getOuterWidth(this.element); + value: function _setColumns() { + var containerWidth = arguments.length <= 0 || arguments[0] === undefined ? Shuffle.getSize(this.element).width : arguments[0]; + var gutter = this._getGutterSize(containerWidth); var columnWidth = this._getColumnSize(containerWidth, gutter); var calculatedColumns = (containerWidth + gutter) / columnWidth; @@ -665,7 +619,6 @@ return /******/ (function(modules) { // webpackBootstrap } /** - * Fire events with .shuffle namespace * @return {boolean} Whether the event was prevented or not. */ @@ -702,73 +655,42 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Array.} items Array of items that will be shown/layed out in * order in their array. Because jQuery collection are always ordered in DOM * order, we can't pass a jq collection. - * @param {boolean} [isOnlyPosition=false] If true this will position the items - * with zero opacity. */ }, { key: '_layout', - value: function _layout(items, isOnlyPosition) { - each(items, function (item) { - this._layoutItem(item, !!isOnlyPosition); - }, this); - - // `_layout` always happens after `_shrink`, so it's safe to process the style - // queue here with styles from the shrink method. - this._processStyleQueue(); - - // Adjust the height of the container. - this._setContainerSize(); + value: function _layout(items) { + each(items, this._layoutItem, this); } /** * Calculates the position of the item and pushes it onto the style queue. * @param {ShuffleItem} item ShuffleItem which is being positioned. - * @param {boolean} isOnlyPosition Whether to position the item, but with zero - * opacity so that it can fade in later. * @private */ }, { key: '_layoutItem', - value: function _layoutItem(item, isOnlyPosition) { + value: function _layoutItem(item, i) { var currPos = item.point; var currScale = item.scale; - var itemSize = { - width: Shuffle._getOuterWidth(item.element, true), - height: Shuffle._getOuterHeight(item.element, true) - }; + var itemSize = Shuffle.getSize(item.element, true); var pos = this._getItemPosition(itemSize); // If the item will not change its position, do not add it to the render // queue. Transitions don't fire when setting a property to the same value. - if (_point2.default.equals(currPos, pos) && currScale === DEFAULT_SCALE) { + if (_point2.default.equals(currPos, pos) && currScale === _shuffleItem2.default.Scale.VISIBLE) { return; } - // Save data for shrink item.point = pos; - item.scale = DEFAULT_SCALE; - - this.styleQueue.push({ - $item: window.jQuery(item.element), - point: pos, - scale: DEFAULT_SCALE, - opacity: isOnlyPosition ? 0 : 1, - - // Set styles immediately if there is no transition speed. - skipTransition: isOnlyPosition || this.speed === 0, - callfront: function callfront() { - if (!isOnlyPosition) { - item.element.style.visibility = 'visible'; - } - }, + item.scale = _shuffleItem2.default.Scale.VISIBLE; - callback: function callback() { - if (isOnlyPosition) { - item.element.style.visibility = 'hidden'; - } - } + this._queue.push({ + item: item, + opacity: 1, + visibility: 'visible', + transitionDelay: Math.min(i * this.staggerAmount, this.staggerAmountMax) }); } @@ -903,37 +825,36 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hides the elements that don't match our filter. - * @param {jQuery} $collection jQuery collection to shrink. + * @param {Array.} collection Collection to shrink. * @private */ }, { key: '_shrink', - value: function _shrink(collection) { + value: function _shrink() { var _this5 = this; - collection = collection || this._getConcealedItems(); + var collection = arguments.length <= 0 || arguments[0] === undefined ? this._getConcealedItems() : arguments[0]; - collection.forEach(function (item) { + each(collection, function (item, i) { // Continuing would add a transitionend event listener to the element, but // that listener would not execute because the transform and opacity would // stay the same. - if (item.scale === CONCEALED_SCALE) { + if (item.scale === _shuffleItem2.default.Scale.FILTERED) { return; } - item.scale = CONCEALED_SCALE; + item.scale = _shuffleItem2.default.Scale.FILTERED; - _this5.styleQueue.push({ - $item: window.jQuery(item.element), - point: item.point, - scale: CONCEALED_SCALE, + _this5._queue.push({ + item: item, opacity: 0, + transitionDelay: Math.min(i * _this5.staggerAmount, _this5.staggerAmountMax), callback: function callback() { item.element.style.visibility = 'hidden'; } }); - }); + }, this); } /** @@ -945,12 +866,12 @@ return /******/ (function(modules) { // webpackBootstrap key: '_handleResize', value: function _handleResize() { // If shuffle is disabled, destroyed, don't do anything - if (!this.enabled || this.destroyed) { + if (!this.isEnabled || this.isDestroyed) { return; } // Will need to check height in the future if it's layed out horizontaly - var containerWidth = Shuffle._getOuterWidth(this.element); + var containerWidth = Shuffle.getSize(this.element).width; // containerWidth hasn't changed, don't do anything if (containerWidth === this.containerWidth) { @@ -962,157 +883,131 @@ return /******/ (function(modules) { // webpackBootstrap /** * Returns styles for either jQuery animate or transition. - * @param {Object} opts Transition options. + * @param {Object} obj Transition options. * @return {!Object} Transforms for transitions, left/top for animate. * @private */ }, { key: '_getStylesForTransition', - value: function _getStylesForTransition(opts) { + value: function _getStylesForTransition(obj) { + var item = obj.item; var styles = { - opacity: opts.opacity + opacity: obj.opacity, + visibility: obj.visibility, + transitionDelay: (obj.transitionDelay || 0) + 'ms' }; - if (this.supported) { - styles.transform = Shuffle._getItemTransformString(opts.point, opts.scale); + if (this.useTransforms) { + styles.transform = Shuffle._getItemTransformString(item.point, item.scale); } else { - styles.left = opts.point.x; - styles.top = opts.point.y; + styles.left = item.point.x + 'px'; + styles.top = item.point.y + 'px'; } return styles; } - - /** - * Transitions an item in the grid - * - * @param {Object} opts options. - * @param {jQuery} opts.$item jQuery object representing the current item. - * @param {Point} opts.point A point object with the x and y coordinates. - * @param {number} opts.scale Amount to scale the item. - * @param {number} opts.opacity Opacity of the item. - * @param {Function} opts.callback Complete function for the animation. - * @param {Function} opts.callfront Function to call before transitioning. - * @private - */ - }, { key: '_transition', value: function _transition(opts) { + var _this6 = this; + var styles = this._getStylesForTransition(opts); - this._startItemAnimation(opts.$item, styles, opts.callfront || window.jQuery.noop, opts.callback || window.jQuery.noop); - } - }, { - key: '_startItemAnimation', - value: function _startItemAnimation($item, styles, callfront, callback) { + var callfront = opts.callfront || noop; + var callback = opts.callback || noop; + var item = opts.item; var _this = this; - // Transition end handler removes its listener. - function handleTransitionEnd(evt) { - // Make sure this event handler has not bubbled up from a child. - if (evt.target === evt.currentTarget) { - window.jQuery(evt.target).off('transitionend', handleTransitionEnd); - _this._removeTransitionReference(reference); - callback(); - } - } - - var reference = { - $element: $item, - handler: handleTransitionEnd - }; - - callfront(); - - // Transitions are not set until shuffle has loaded to avoid the initial transition. - if (!this.initialized) { - $item.css(styles); - callback(); - return; - } - - // Use CSS Transforms if we have them - if (this.supported) { - $item.css(styles); - $item.on('transitionend', handleTransitionEnd); - this._transitions.push(reference); + return new Promise(function (resolve) { + var reference = { + item: item, + handler: function handler(evt) { + var element = evt.target; + + // Make sure this event handler has not bubbled up from a child. + if (element === evt.currentTarget) { + element.removeEventListener('transitionend', reference.handler); + element.style.transitionDelay = ''; + _this._removeTransitionReference(reference); + callback(); + resolve(); + } + } + }; - // Use jQuery to animate left/top - } else { - // Save the deferred object which jQuery returns. - var anim = $item.stop(true).animate(styles, this.speed, 'swing', callback); + callfront(); + item.applyCss(styles); - // Push the animation to the list of pending animations. - this._animations.push(anim.promise()); + // Transitions are not set until shuffle has loaded to avoid the initial transition. + if (_this6.isInitialized) { + item.element.addEventListener('transitionend', reference.handler); + _this6._transitions.push(reference); + } else { + callback(); + resolve(); } + }); } /** * Execute the styles gathered in the style queue. This applies styles to elements, * triggering transitions. - * @param {boolean} noLayout Whether to trigger a layout event. + * @param {boolean} withLayout Whether to trigger a layout event. * @private */ }, { - key: '_processStyleQueue', - value: function _processStyleQueue(noLayout) { + key: '_processQueue', + value: function _processQueue() { + var _this7 = this; + + var withLayout = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + if (this.isTransitioning) { this._cancelMovement(); } - var $transitions = window.jQuery(); - // Iterate over the queue and keep track of ones that use transitions. - each(this.styleQueue, function (transitionObj) { - if (transitionObj.skipTransition) { - this._styleImmediately(transitionObj); + var immediates = []; + var transitions = []; + this._queue.forEach(function (obj) { + if (!_this7.isInitialized || _this7.speed === 0) { + immediates.push(obj); } else { - $transitions = $transitions.add(transitionObj.$item); - this._transition(transitionObj); + transitions.push(obj); } - }, this); + }); - if ($transitions.length > 0 && this.initialized && this.speed > 0) { + immediates.forEach(function (obj) { + _this7._styleImmediately(obj); + }); + + var promises = transitions.map(function (obj) { + return _this7._transition(obj); + }); + + if (transitions.length > 0 && this.speed > 0) { // Set flag that shuffle is currently in motion. this.isTransitioning = true; - if (this.supported) { - this._whenCollectionDone($transitions, 'transitionend', this._movementFinished); - - // The _transition function appends a promise to the animations array. - // When they're all complete, do things. - } else { - this._whenAnimationsDone(this._movementFinished); - } + Promise.all(promises).then(this._movementFinished.bind(this)); // A call to layout happened, but none of the newly filtered items will // change position. Asynchronously fire the callback here. - } else if (!noLayout) { - defer(this._layoutEnd, this); + } else if (withLayout) { + defer(this._dispatchLayout, this); } // Remove everything in the style queue - this.styleQueue.length = 0; + this._queue.length = 0; } }, { key: '_cancelMovement', value: function _cancelMovement() { - if (this.supported) { - // Remove the transition end event for each listener. - each(this._transitions, function (transition) { - transition.$element.off('transitionend', transition.handler); - }); - } else { - // Even when `stop` is called on the jQuery animation, its promise will - // still be resolved. Since it cannot be determine from within that callback - // whether the animation was stopped or not, a flag is set here to distinguish - // between the two states. - this._isMovementCanceled = true; - this.items.stop(true); - this._isMovementCanceled = false; - } + // Remove the transition end event for each listener. + each(this._transitions, function (transition) { + transition.item.element.removeEventListener('transitionend', transition.handler); + }); // Reset the array. this._transitions.length = 0; @@ -1123,7 +1018,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_removeTransitionReference', value: function _removeTransitionReference(ref) { - var indexInArray = window.jQuery.inArray(ref, this._transitions); + var indexInArray = this._transitions.indexOf(ref); if (indexInArray > -1) { this._transitions.splice(indexInArray, 1); } @@ -1137,20 +1032,22 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_styleImmediately', - value: function _styleImmediately(opts) { - Shuffle._skipTransition(opts.$item[0], function () { - opts.$item.css(this._getStylesForTransition(opts)); - }, this); + value: function _styleImmediately(obj) { + var _this8 = this; + + Shuffle._skipTransition(obj.item.element, function () { + obj.item.applyCss(_this8._getStylesForTransition(obj)); + }); } }, { key: '_movementFinished', value: function _movementFinished() { this.isTransitioning = false; - this._layoutEnd(); + this._dispatchLayout(); } }, { - key: '_layoutEnd', - value: function _layoutEnd() { + key: '_dispatchLayout', + value: function _dispatchLayout() { this._dispatch(Shuffle.EventType.LAYOUT); } }, { @@ -1167,32 +1064,33 @@ return /******/ (function(modules) { // webpackBootstrap // Shrink all items (without transitions). this._shrink($newItems); - each(this.styleQueue, function (transitionObj) { + each(this._queue, function (transitionObj) { transitionObj.skipTransition = true; }); // Apply shrink positions, but do not cause a layout event. - this._processStyleQueue(true); + this._processQueue(false); if (addToEnd) { this._addItemsToEnd($newItems, isSequential); } else { - this.shuffle(this.lastFilter); + this.filter(this.lastFilter); } } }, { key: '_addItemsToEnd', value: function _addItemsToEnd($newItems, isSequential) { // Get ones that passed the current filter - var $passed = this._filter(null, $newItems); + var $passed = this._filter(null, $newItems).filtered; var passed = $passed.get(); // How many filtered elements? this._updateItemCount(); + // FIXME won't process queue. this._layout(passed, true); - if (isSequential && this.supported) { + if (isSequential) { this._setSequentialDelay(passed); } @@ -1215,7 +1113,7 @@ return /******/ (function(modules) { // webpackBootstrap $item: $item, opacity: 1, point: $item.data('point'), - scale: DEFAULT_SCALE + scale: _shuffleItem2.default.Scale.VISIBLE }); }, this); @@ -1226,88 +1124,32 @@ return /******/ (function(modules) { // webpackBootstrap }, this, this.revealAppendedDelay); } - /** - * Execute a function when an event has been triggered for every item in a collection. - * @param {jQuery} $collection Collection of elements. - * @param {string} eventName Event to listen for. - * @param {Function} callback Callback to execute when they're done. - * @private - */ - - }, { - key: '_whenCollectionDone', - value: function _whenCollectionDone($collection, eventName, callback) { - var done = 0; - var items = $collection.length; - var _this = this; - - function handleEventName(evt) { - if (evt.target === evt.currentTarget) { - window.jQuery(evt.target).off(eventName, handleEventName); - done++; - - // Execute callback if all items have emitted the correct event. - if (done === items) { - _this._removeTransitionReference(reference); - callback.call(_this); - } - } - } - - var reference = { - $element: $collection, - handler: handleEventName - }; - - // Bind the event to all items. - $collection.on(eventName, handleEventName); - - // Keep track of transitionend events so they can be removed. - this._transitions.push(reference); - } - - /** - * Execute a callback after jQuery `animate` for a collection has finished. - * @param {Function} callback Callback to execute when they're done. - * @private - */ - - }, { - key: '_whenAnimationsDone', - value: function _whenAnimationsDone(callback) { - window.jQuery.when.apply(null, this._animations).always(window.jQuery.proxy(function () { - this._animations.length = 0; - if (!this._isMovementCanceled) { - callback.call(this); - } - }, this)); - } - /** * The magic. This is what makes the plugin 'shuffle' - * @param {string|Function} [category] Category to filter by. Can be a function + * @param {string|Function|Array.} [category] Category to filter by. + * Can be a function, string, or array of strings. * @param {Object} [sortObj] A sort object which can sort the filtered set */ }, { - key: 'shuffle', - value: function shuffle(category, sortObj) { - if (!this.enabled) { + key: 'filter', + value: function filter(category, sortObj) { + if (!this.isEnabled) { return; } - if (!category) { - category = ALL_ITEMS; + if (!category || category && category.length === 0) { + category = Shuffle.ALL_ITEMS; } this._filter(category); - // How many filtered elements? - this._updateItemCount(); - // Shrink each concealed item this._shrink(); + // How many filtered elements? + this._updateItemCount(); + // Update transforms on .filtered elements so they will animate to their new positions this.sort(sortObj); } @@ -1319,18 +1161,28 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'sort', - value: function sort(opts) { - if (this.enabled) { - this._resetCols(); + value: function sort() { + var opts = arguments.length <= 0 || arguments[0] === undefined ? this.lastSort : arguments[0]; - var sortOptions = opts || this.lastSort; - var items = this._getFilteredItems(); - items = (0, _sorter2.default)(items, sortOptions); + if (!this.isEnabled) { + return; + } - this._layout(items); + this._resetCols(); - this.lastSort = sortOptions; - } + var items = this._getFilteredItems(); + items = (0, _sorter2.default)(items, opts); + + this._layout(items); + + // `_layout` always happens after `_shrink`, so it's safe to process the style + // queue here with styles from the shrink method. + this._processQueue(); + + // Adjust the height of the container. + this._setContainerSize(); + + this.lastSort = opts; } /** @@ -1342,7 +1194,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'update', value: function update(isOnlyLayout) { - if (this.enabled) { + if (this.isEnabled) { if (!isOnlyLayout) { // Get updated colCount @@ -1387,7 +1239,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'disable', value: function disable() { - this.enabled = false; + this.isEnabled = false; } /** @@ -1398,7 +1250,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'enable', value: function enable(isUpdateLayout) { - this.enabled = true; + this.isEnabled = true; if (isUpdateLayout !== false) { this.update(); } @@ -1439,7 +1291,7 @@ return /******/ (function(modules) { // webpackBootstrap this.sort(); - this.$el.one(Shuffle.EventType.LAYOUT + '.' + SHUFFLE, window.jQuery.proxy(handleRemoved, this)); + this.$el.one(Shuffle.EventType.LAYOUT + '.shuffle', window.jQuery.proxy(handleRemoved, this)); } /** @@ -1452,10 +1304,13 @@ return /******/ (function(modules) { // webpackBootstrap window.removeEventListener('resize', this._onResize); // Reset container styles - this.$el.removeClass(SHUFFLE).removeAttr('style').removeData(SHUFFLE); + this.element.classList.remove('shuffle'); + this.element.removeAttribute('style'); // Reset individual item styles - this.items.removeAttr('style').removeData('point').removeData('scale').removeClass([Shuffle.ClassName.CONCEALED, Shuffle.ClassName.FILTERED, Shuffle.ClassName.SHUFFLE_ITEM].join(' ')); + this.items.forEach(function (item) { + item.dispose(); + }); // Null DOM references this.items = null; @@ -1465,26 +1320,120 @@ return /******/ (function(modules) { // webpackBootstrap this._transitions = null; // Set a flag so if a debounced resize has been triggered, - // it can first check if it is actually destroyed and not doing anything + // it can first check if it is actually isDestroyed and not doing anything this.destroyed = true; } + + /** + * Get the CSS transform based on position and scale. + * @param {Point} point X and Y positions. + * @param {number} scale Scale amount. + * @return {string} A normalized string which can be used with the transform style. + * @private + */ + + }], [{ + key: '_getItemTransformString', + value: function _getItemTransformString(point, scale) { + return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')'; + } + + /** + * Returns the outer width of an element, optionally including its margins. + * + * There are a few different methods for getting the width of an element, none of + * which work perfectly for all Shuffle's use cases. + * + * 1. getBoundingClientRect() `left` and `right` properties. + * - Accounts for transform scaled elements, making it useless for Shuffle + * elements which have shrunk. + * 2. The `offsetWidth` property (or jQuery's CSS). + * - This value stays the same regardless of the elements transform property, + * however, it does not return subpixel values. + * 3. getComputedStyle() + * - This works great Chrome, Firefox, Safari, but IE<=11 does not include + * padding and border when box-sizing: border-box is set, requiring a feature + * test and extra work to add the padding back for IE and other browsers which + * follow the W3C spec here. + * + * @param {Element} element The element. + * @param {boolean} [includeMargins] Whether to include margins. Default is false. + * @return {{width: number, height: number}} The width and height. + */ + + }, { + key: 'getSize', + value: function getSize(element, includeMargins) { + // Store the styles so that they can be used by others without asking for it again. + var styles = window.getComputedStyle(element, null); + var width = (0, _getNumberStyle2.default)(element, 'width', styles); + var height = (0, _getNumberStyle2.default)(element, 'height', styles); + + // Use jQuery here because it uses getComputedStyle internally and is + // cross-browser. Using the style property of the element will only work + // if there are inline styles. + if (includeMargins) { + var marginLeft = (0, _getNumberStyle2.default)(element, 'marginLeft', styles); + var marginRight = (0, _getNumberStyle2.default)(element, 'marginRight', styles); + var marginTop = (0, _getNumberStyle2.default)(element, 'marginTop', styles); + var marginBottom = (0, _getNumberStyle2.default)(element, 'marginBottom', styles); + width += marginLeft + marginRight; + height += marginTop + marginBottom; + } + + return { + width: width, + height: height + }; + } + + /** + * Change a property or execute a function which will not have a transition + * @param {Element} element DOM element that won't be transitioned + * @param {Function} callback A function which will be called while transition + * is set to 0ms. + * @private + */ + + }, { + key: '_skipTransition', + value: function _skipTransition(element, callback) { + var style = element.style; + var duration = style.transitionDuration; + var delay = style.transitionDelay; + + // Set the duration to zero so it happens immediately + style.transitionDuration = '0ms'; + style.transitionDelay = '0ms'; + + callback(); + + // Force reflow + var reflow = element.offsetWidth; + + // Avoid jshint warnings: unused variables and expressions. + reflow = null; + + // Put the duration back + style.transitionDuration = duration; + style.transitionDelay = delay; + } }]); return Shuffle; }(); + Shuffle.ALL_ITEMS = 'all'; + Shuffle.FILTER_ATTRIBUTE_KEY = 'groups'; + /** - * Events the container element emits with the .shuffle namespace. - * For example, "done.shuffle". * @enum {string} */ - - Shuffle.EventType = { - LOADING: 'loading', - DONE: 'done', - LAYOUT: 'layout', - REMOVED: 'removed' + LOADING: 'shuffle:loading', + DONE: 'shuffle:done', + LAYOUT: 'shuffle:layout', + REMOVED: 'shuffle:removed' }; /** @enum {string} */ @@ -1492,178 +1441,70 @@ return /******/ (function(modules) { // webpackBootstrap // Overrideable options Shuffle.options = { - group: ALL_ITEMS, // Initial filter group. - speed: 250, // Transition/animation speed (milliseconds). - easing: 'ease-out', // CSS easing function to use. - itemSelector: '', // e.g. '.picture-item'. - sizer: null, // Sizer element. Use an element to determine the size of columns and gutters. - gutterWidth: 0, // A static number or function that tells the plugin how wide the gutters between columns are (in pixels). - columnWidth: 0, // A static number or function that returns a number which tells the plugin how wide the columns are (in pixels). - delimeter: null, // If your group is not json, and is comma delimeted, you could set delimeter to ','. - buffer: 0, // Useful for percentage based heights when they might not always be exactly the same (in pixels). - columnThreshold: 0.01, // Reading the width of elements isn't precise enough and can cause columns to jump between values. - initialSort: null, // Shuffle can be initialized with a sort object. It is the same object given to the sort method. - throttle: throttle, // By default, shuffle will throttle resize events. This can be changed or removed. - throttleTime: 300, // How often shuffle can be called on resize (in milliseconds). - sequentialFadeDelay: 150, // Delay between each item that fades in when adding items. - supported: true }; - - // Not overrideable - // Whether to use transforms or absolute positioning. - Shuffle.settings = { - useSizer: false, - revealAppendedDelay: 300, - lastSort: {}, - lastFilter: ALL_ITEMS, - enabled: true, - destroyed: false, - initialized: false, - _animations: [], - _transitions: [], - _isMovementCanceled: false, - styleQueue: [] - }; + // Initial filter group. + group: Shuffle.ALL_ITEMS, - // Expose for testing. - Shuffle.Point = _point2.default; - Shuffle.ShuffleItem = _shuffleItem2.default; - Shuffle.sorter = _sorter2.default; + // Transition/animation speed (milliseconds). + speed: 250, - /** - * Static methods. - */ + // CSS easing function to use. + easing: 'ease', - /** - * If the browser has 3d transforms available, build a string with those, - * otherwise use 2d transforms. - * @param {Point} point X and Y positions. - * @param {number} scale Scale amount. - * @return {string} A normalized string which can be used with the transform style. - * @private - */ - Shuffle._getItemTransformString = function (point, scale) { - return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')'; - }; + // e.g. '.picture-item'. + itemSelector: '', - /** - * Retrieve the computed style for an element, parsed as a float. - * @param {Element} element Element to get style for. - * @param {string} style Style property. - * @param {CSSStyleDeclaration} [styles] Optionally include clean styles to - * use instead of asking for them again. - * @return {number} The parsed computed value or zero if that fails because IE - * will return 'auto' when the element doesn't have margins instead of - * the computed style. - * @private - */ - Shuffle._getNumberStyle = function (element, style, styles) { - styles = styles || getStyles(element, null); - var value = Shuffle._getFloat(styles[style]); + // Sizer element. Use an element to determine the size of columns and gutters. + sizer: null, - // Support IE<=11 and W3C spec. - if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'width') { - value += Shuffle._getFloat(styles.paddingLeft) + Shuffle._getFloat(styles.paddingRight) + Shuffle._getFloat(styles.borderLeftWidth) + Shuffle._getFloat(styles.borderRightWidth); - } else if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'height') { - value += Shuffle._getFloat(styles.paddingTop) + Shuffle._getFloat(styles.paddingBottom) + Shuffle._getFloat(styles.borderTopWidth) + Shuffle._getFloat(styles.borderBottomWidth); - } + // A static number or function that tells the plugin how wide the gutters + // between columns are (in pixels). + gutterWidth: 0, - return value; - }; + // A static number or function that returns a number which tells the plugin + // how wide the columns are (in pixels). + columnWidth: 0, - /** - * Parse a string as an float. - * @param {string} value String float. - * @return {number} The string as an float or zero. - * @private - */ - Shuffle._getFloat = function (value) { - return (0, _getNumber2.default)(parseFloat(value)); - }; - - /** - * Returns the outer width of an element, optionally including its margins. - * - * There are a few different methods for getting the width of an element, none of - * which work perfectly for all Shuffle's use cases. - * - * 1. getBoundingClientRect() `left` and `right` properties. - * - Accounts for transform scaled elements, making it useless for Shuffle - * elements which have shrunk. - * 2. The `offsetWidth` property (or jQuery's CSS). - * - This value stays the same regardless of the elements transform property, - * however, it does not return subpixel values. - * 3. getComputedStyle() - * - This works great Chrome, Firefox, Safari, but IE<=11 does not include - * padding and border when box-sizing: border-box is set, requiring a feature - * test and extra work to add the padding back for IE and other browsers which - * follow the W3C spec here. - * - * @param {Element} element The element. - * @param {boolean} [includeMargins] Whether to include margins. Default is false. - * @return {number} The width. - */ - Shuffle._getOuterWidth = function (element, includeMargins) { - // Store the styles so that they can be used by others without asking for it again. - var styles = getStyles(element, null); - var width = Shuffle._getNumberStyle(element, 'width', styles); - - // Use jQuery here because it uses getComputedStyle internally and is - // cross-browser. Using the style property of the element will only work - // if there are inline styles. - if (includeMargins) { - var marginLeft = Shuffle._getNumberStyle(element, 'marginLeft', styles); - var marginRight = Shuffle._getNumberStyle(element, 'marginRight', styles); - width += marginLeft + marginRight; - } + // If your group is not json, and is comma delimeted, you could set delimeter + // to ','. + delimeter: null, - return width; - }; + // Useful for percentage based heights when they might not always be exactly + // the same (in pixels). + buffer: 0, - /** - * Returns the outer height of an element, optionally including its margins. - * @param {Element} element The element. - * @param {boolean} [includeMargins] Whether to include margins. Default is false. - * @return {number} The height. - */ - Shuffle._getOuterHeight = function (element, includeMargins) { - var styles = getStyles(element, null); - var height = Shuffle._getNumberStyle(element, 'height', styles); - - if (includeMargins) { - var marginTop = Shuffle._getNumberStyle(element, 'marginTop', styles); - var marginBottom = Shuffle._getNumberStyle(element, 'marginBottom', styles); - height += marginTop + marginBottom; - } + // Reading the width of elements isn't precise enough and can cause columns to + // jump between values. + columnThreshold: 0.01, - return height; - }; + // Shuffle can be isInitialized with a sort object. It is the same object + // given to the sort method. + initialSort: null, - /** - * Change a property or execute a function which will not have a transition - * @param {Element} element DOM element that won't be transitioned - * @param {Function} callback A function which will be called while transition - * is set to 0ms. - * @param {Object} [context] Optional context for the callback function. - * @private - */ - Shuffle._skipTransition = function (element, callback, context) { - var duration = element.style.transitionDuration; + // By default, shuffle will throttle resize events. This can be changed or + // removed. + throttle: _throttle2.default, - // Set the duration to zero so it happens immediately - element.style.transitionDuration = '0ms'; + // How often shuffle can be called on resize (in milliseconds). + throttleTime: 300, - callback.call(context); + // Delay between each item that fades in when adding items. + sequentialFadeDelay: 150, - // Force reflow - var reflow = element.offsetWidth; + // Transition delay offset for each item in milliseconds. + staggerAmount: 15, - // Avoid jshint warnings: unused variables and expressions. - reflow = null; + // It can look a little weird when the last element is in the top row + staggerAmountMax: 250, - // Put the duration back - element.style.transitionDuration = duration; + // Whether to use transforms or absolute positioning. + useTransforms: true }; + // Expose for testing. + Shuffle.Point = _point2.default; + Shuffle.ShuffleItem = _shuffleItem2.default; + Shuffle.sorter = _sorter2.default; + module.exports = Shuffle; /***/ }, @@ -1759,7 +1600,7 @@ return /******/ (function(modules) { // webpackBootstrap var str = value && value.toString(); var val = parseFloat(str); if (val + 1 >= 0) { - return value; + return val; } return 0; @@ -1815,7 +1656,7 @@ return /******/ (function(modules) { // webpackBootstrap key: 'init', value: function init() { this.addClasses([_classes2.default.SHUFFLE_ITEM, _classes2.default.FILTERED]); - this._applyCss(ShuffleItem.css); + this.applyCss(ShuffleItem.css); this.scale = ShuffleItem.Scale.VISIBLE; this.point = new _point2.default(); } @@ -1829,14 +1670,31 @@ return /******/ (function(modules) { // webpackBootstrap }); } }, { - key: '_applyCss', - value: function _applyCss(obj) { + key: 'removeClasses', + value: function removeClasses(classes) { var _this2 = this; + classes.forEach(function (className) { + _this2.element.classList.remove(className); + }); + } + }, { + key: 'applyCss', + value: function applyCss(obj) { + var _this3 = this; + Object.keys(obj).forEach(function (key) { - _this2.element.style[key] = obj[key]; + _this3.element.style[key] = obj[key]; }); } + }, { + key: 'dispose', + value: function dispose() { + this.removeClasses([_classes2.default.CONCEALED, _classes2.default.FILTERED, _classes2.default.SHUFFLE_ITEM]); + + this.element.removeAttribute('style'); + this.element = null; + } }]); return ShuffleItem; @@ -1872,16 +1730,91 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 6 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = getNumberStyle; + + var _getNumber = __webpack_require__(3); + + var _getNumber2 = _interopRequireDefault(_getNumber); + + var _computedSize = __webpack_require__(7); + + var _computedSize2 = _interopRequireDefault(_computedSize); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Retrieve the computed style for an element, parsed as a float. + * @param {Element} element Element to get style for. + * @param {string} style Style property. + * @param {CSSStyleDeclaration} [styles] Optionally include clean styles to + * use instead of asking for them again. + * @return {number} The parsed computed value or zero if that fails because IE + * will return 'auto' when the element doesn't have margins instead of + * the computed style. + * @private + */ + function getNumberStyle(element, style) { + var styles = arguments.length <= 2 || arguments[2] === undefined ? window.getComputedStyle(element, null) : arguments[2]; + + var value = (0, _getNumber2.default)(styles[style]); + + // Support IE<=11 and W3C spec. + if (!_computedSize2.default && style === 'width') { + value += (0, _getNumber2.default)(styles.paddingLeft) + (0, _getNumber2.default)(styles.paddingRight) + (0, _getNumber2.default)(styles.borderLeftWidth) + (0, _getNumber2.default)(styles.borderRightWidth); + } else if (!_computedSize2.default && style === 'height') { + value += (0, _getNumber2.default)(styles.paddingTop) + (0, _getNumber2.default)(styles.paddingBottom) + (0, _getNumber2.default)(styles.borderTopWidth) + (0, _getNumber2.default)(styles.borderBottomWidth); + } + + return value; + } + +/***/ }, +/* 7 */ /***/ function(module, exports) { 'use strict'; - // http://stackoverflow.com/a/962890/373422 + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var element = document.body || document.documentElement; + var e = document.createElement('div'); + e.style.cssText = 'width:10px;padding:2px;box-sizing:border-box;'; + element.appendChild(e); + + var width = window.getComputedStyle(e, null).width; + var ret = width === '10px'; + + element.removeChild(e); + + exports.default = ret; + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = sorter; + + var _assign = __webpack_require__(11); + + var _assign2 = _interopRequireDefault(_assign); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + // http://stackoverflow.com/a/962890/373422 function randomize(array) { var tmp; var current; @@ -1909,13 +1842,17 @@ return /******/ (function(modules) { // webpackBootstrap by: null, // If true, this will skip the sorting and return a randomized order in the array - randomize: false + randomize: false, + + // Determines which property of each item in the array is passed to the + // sorting method. + key: 'element' }; // You can return `undefined` from the `by` function to revert to DOM order. function sorter(arr, options) { - var opts = Object.assign({}, defaults, options); - var original = Array.from(arr); + var opts = (0, _assign2.default)({}, defaults, options); + var original = [].slice.call(arr); var revert = false; if (!arr.length) { @@ -1928,7 +1865,7 @@ return /******/ (function(modules) { // webpackBootstrap // Sort the elements by the opts.by function. // If we don't have opts.by, default to DOM order - if (typeof options.by === 'function') { + if (typeof opts.by === 'function') { arr.sort(function (a, b) { // Exit early if we already know we want to revert @@ -1936,8 +1873,8 @@ return /******/ (function(modules) { // webpackBootstrap return 0; } - var valA = opts.by(a); - var valB = opts.by(b); + var valA = opts.by(a[opts.key]); + var valB = opts.by(b[opts.key]); // If both values are undefined, use the DOM order if (valA === undefined && valB === undefined) { @@ -1970,7 +1907,7 @@ return /******/ (function(modules) { // webpackBootstrap } /***/ }, -/* 7 */ +/* 9 */ /***/ function(module, exports) { 'use strict'; @@ -1994,6 +1931,83 @@ return /******/ (function(modules) { // webpackBootstrap window.CustomEvent = CustomEvent; })(); +/***/ }, +/* 10 */ +/***/ function(module, exports) { + + 'use strict'; + + // Underscore's throttle method. + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (func, wait, options) { + var _this; + var args; + var result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function later() { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(_this, args); + if (!timeout) _this = args = null; + }; + + return function () { + var now = Date.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + _this = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + previous = now; + result = func.apply(_this, args); + if (!timeout) _this = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + + return result; + }; + }; + +/***/ }, +/* 11 */ +/***/ function(module, exports) { + + 'use strict'; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = assign; + function assign(target) { + var output = Object(target); + for (var i = 1, length = arguments.length; i < length; i++) { + var source = arguments[i]; + if (source !== undefined && source !== null) { + for (var key in source) { + if (source.hasOwnProperty(key)) { + output[key] = source[key]; + } + } + } + } + + return output; + } + /***/ } /******/ ]) }); diff --git a/js/demos/homepage.js b/js/demos/homepage.js index 416cde6..92617b5 100644 --- a/js/demos/homepage.js +++ b/js/demos/homepage.js @@ -1,161 +1,201 @@ 'use strict'; -var DEMO = (function ($) { - 'use strict'; - - var $grid = $('#grid'), - $filterOptions = $('.filter-options'), - $sizer = $grid.find('.shuffle__sizer'), - - init = function () { - - // None of these need to be executed synchronously - setTimeout(function () { - listen(); - setupFilters(); - setupSorting(); - setupSearching(); - }, 100); - - // You can subscribe to custom events. - // shrink, shrunk, filter, filtered, sorted, load, done - $grid.on('loading.shuffle done.shuffle layout.shuffle', function (evt, shuffle) { - // Make sure the browser has a console - if (window.console && window.console.log && typeof window.console.log === 'function') { - console.log('Shuffle:', evt.type); - } - }); - - // instantiate the plugin - $grid.shuffle({ - itemSelector: '.picture-item', - sizer: $sizer, - }); - - // Destroy it! o_O - // $grid.shuffle('destroy'); - }, - - // Set up button clicks - setupFilters = function () { - var $btns = $filterOptions.children(); - $btns.on('click', function () { - var $this = $(this), - isActive = $this.hasClass('active'), - group = isActive ? 'all' : $this.data('group'); - - // Hide current label, show current label in title - if (!isActive) { - $('.filter-options .active').removeClass('active'); - } - - $this.toggleClass('active'); - - // Filter elements - $grid.shuffle('shuffle', group); - }); - - $btns = null; - }, - - setupSorting = function () { - // Sorting options - $('.sort-options').on('change', function () { - var sort = this.value, - opts = {}; - - // We're given the element wrapped in jQuery - if (sort === 'date-created') { - opts = { - reverse: true, - by: function ($el) { - return $el.data('date-created'); - }, - }; - } else if (sort === 'title') { - opts = { - by: function ($el) { - return $el.data('title').toLowerCase(); - }, - }; - } - - // Filter elements - $grid.shuffle('sort', opts); - }); - }, - - setupSearching = function () { - // Advanced filtering - $('.js-shuffle-search').on('keyup change', function () { - var val = this.value.toLowerCase(); - $grid.shuffle('shuffle', function ($el, shuffle) { - - // Only search elements in the current group - if (shuffle.group !== 'all' && $.inArray(shuffle.group, $el.data('groups')) === -1) { - return false; - } - - var text = $.trim($el.find('.picture-item__title').text()).toLowerCase(); - return text.indexOf(val) !== -1; - }); - }); - }, - - // Re layout shuffle when images load. This is only needed - // below 768 pixels because the .picture-item height is auto and therefore - // the height of the picture-item is dependent on the image - // I recommend using imagesloaded to determine when an image is loaded - // but that doesn't support IE7 - listen = function () { - var debouncedLayout = $.throttle(300, function () { - $grid.shuffle('update'); - }); - - // Get all images inside shuffle - $grid.find('img').each(function () { - var proxyImage; - - // Image already loaded - if (this.complete && this.naturalWidth !== undefined) { - return; - } - - // If none of the checks above matched, simulate loading on detached element. - proxyImage = new Image(); - $(proxyImage).on('load', function () { - $(this).off('load'); - debouncedLayout(); - }); - - proxyImage.src = this.src; - }); - - // Because this method doesn't seem to be perfect. - setTimeout(function () { - debouncedLayout(); - }, 500); - }; - - return { - init: init, - }; -}(jQuery)); +var Shuffle = window.Shuffle; -// $(document).ready(function() { -// DEMO.init(); -// }); +var Demo = function (element) { + this.element = element; -var Shuffle = window.Shuffle; + // Log out events. + this.addShuffleEventListeners(); -var Demo = function () { - var element = document.getElementById('grid'); this.shuffle = new Shuffle(element, { itemSelector: '.picture-item', sizer: element.querySelector('.shuffle__sizer'), }); + + this._activeFilters = []; + + this.addFilterButtons(); + this.addSorting(); + this.addSearchFilter(); + this.listenForImageLoads(); + + this.mode = 'exclusive'; +}; + +Demo.prototype.toArray = function (arrayLike) { + return Array.prototype.slice.call(arrayLike); +}; + +Demo.prototype.toggleMode = function () { + if (this.mode === 'additive') { + this.mode = 'exclusive'; + } else { + this.mode = 'additive'; + } +}; + +/** + * Shuffle uses the CustomEvent constructor to dispatch events. You can listen + * for them like you normally would (with jQuery for example). The extra event + * data is in the `detail` property. + */ +Demo.prototype.addShuffleEventListeners = function () { + var handler = function (event) { + console.log('type: %s', event.type, 'detail:', event.detail); + }; + + this.element.addEventListener(Shuffle.EventType.LOADING, handler, false); + this.element.addEventListener(Shuffle.EventType.DONE, handler, false); + this.element.addEventListener(Shuffle.EventType.LAYOUT, handler, false); + this.element.addEventListener(Shuffle.EventType.REMOVED, handler, false); +}; + +Demo.prototype.addFilterButtons = function () { + var options = document.querySelector('.filter-options'); + + if (!options) { + return; + } + + var filterButtons = this.toArray( + options.children + ); + + filterButtons.forEach(function (button) { + button.addEventListener('click', this._handleFilterClick.bind(this), false); + }, this); +}; + +Demo.prototype._handleFilterClick = function (evt) { + var btn = evt.currentTarget; + var isActive = btn.classList.contains('active'); + var btnGroup = btn.getAttribute('data-group'); + + // You don't need _both_ of these modes. This is only for the demo. + + // For this custom 'additive' mode in the demo, clicking on filter buttons + // doesn't remove any other filters. + if (this.mode === 'additive') { + // If this button is already active, remove it from the list of filters. + if (isActive) { + this._activeFilters.splice(this._activeFilters.indexOf(btnGroup)); + } else { + this._activeFilters.push(btnGroup); + } + + btn.classList.toggle('active'); + + // Filter elements + this.shuffle.filter(this._activeFilters); + + // 'exclusive' mode lets only one filter button be active at a time. + } else { + this._removeActiveClassFromChildren(btn.parentNode); + + var filterGroup; + if (isActive) { + btn.classList.remove('active'); + filterGroup = Shuffle.ALL_ITEMS; + } else { + btn.classList.add('active'); + filterGroup = btnGroup; + } + + this.shuffle.filter(filterGroup); + } +}; + +Demo.prototype._removeActiveClassFromChildren = function (parent) { + var children = parent.children; + for (var i = children.length - 1; i >= 0; i--) { + children[i].classList.remove('active'); + } +}; + +Demo.prototype.addSorting = function () { + var menu = document.querySelector('.sort-options'); + + if (!menu) { + return; + } + + menu.addEventListener('change', this._handleSortChange.bind(this)); +}; + +Demo.prototype._handleSortChange = function (evt) { + var value = evt.target.value; + var options = {}; + + function sortByDate(element) { + return element.getAttribute('data-created'); + } + + function sortByTitle(element) { + return element.getAttribute('data-title').toLowerCase(); + } + + if (value === 'date-created') { + options = { + reverse: true, + by: sortByDate, + }; + } else if (value === 'title') { + options = { + by: sortByTitle, + }; + } + + this.shuffle.sort(options); +}; + +// Advanced filtering +Demo.prototype.addSearchFilter = function () { + var searchInput = document.querySelector('.js-shuffle-search'); + + if (!searchInput) { + return; + } + + searchInput.addEventListener('keyup', this._handleSearchKeyup.bind(this)); +}; + +Demo.prototype._handleSearchKeyup = function (evt) { + var searchText = evt.target.value.toLowerCase(); + + this.shuffle.filter(function (element, shuffle) { + // Get the item's groups. + var groups = JSON.parse(element.getAttribute('data-groups')); + + // Only search elements in the current group + if (shuffle.group !== 'all' && groups.indexOf(shuffle.group) === -1) { + return false; + } + + var title = element.querySelector('.picture-item__title'); + var titleText = title.textContent.toLowerCase().trim(); + + return titleText.indexOf(searchText) !== -1; + }); +}; + +/** + * Re-layout shuffle when images load. This is only needed below 768 pixels + * because the .picture-item height is auto and therefore the height of the + * picture-item is dependent on the image. I recommend using imagesloaded by + * desandro to determine when all your images have loaded. + */ +Demo.prototype.listenForImageLoads = function () { + var imgs = this.element.querySelectorAll('img'); + var handler = function () { + this.shuffle.update(); + }.bind(this); + + for (var i = imgs.length - 1; i >= 0; i--) { + imgs[i].addEventListener('load', handler, false); + } }; document.addEventListener('DOMContentLoaded', function () { - new Demo(); + window.demo = new Demo(document.getElementById('grid')); }); diff --git a/src/assign.js b/src/assign.js new file mode 100644 index 0000000..172c961 --- /dev/null +++ b/src/assign.js @@ -0,0 +1,18 @@ +'use strict'; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign +export default function assign(target) { + var output = Object(target); + for (var i = 1, length = arguments.length; i < length; i++) { + var source = arguments[i]; + if (source !== undefined && source !== null) { + for (var key in source) { + if (source.hasOwnProperty(key)) { + output[key] = source[key]; + } + } + } + } + + return output; +} diff --git a/src/computed-size.js b/src/computed-size.js new file mode 100644 index 0000000..f940a53 --- /dev/null +++ b/src/computed-size.js @@ -0,0 +1,12 @@ + +let element = document.body || document.documentElement; +let e = document.createElement('div'); +e.style.cssText = 'width:10px;padding:2px;box-sizing:border-box;'; +element.appendChild(e); + +let width = window.getComputedStyle(e, null).width; +let ret = width === '10px'; + +element.removeChild(e); + +export default ret; diff --git a/src/get-number-style.js b/src/get-number-style.js new file mode 100644 index 0000000..c456c4f --- /dev/null +++ b/src/get-number-style.js @@ -0,0 +1,35 @@ +'use strict'; + +import getNumber from './get-number'; +import COMPUTED_SIZE_INCLUDES_PADDING from './computed-size'; + +/** + * Retrieve the computed style for an element, parsed as a float. + * @param {Element} element Element to get style for. + * @param {string} style Style property. + * @param {CSSStyleDeclaration} [styles] Optionally include clean styles to + * use instead of asking for them again. + * @return {number} The parsed computed value or zero if that fails because IE + * will return 'auto' when the element doesn't have margins instead of + * the computed style. + * @private + */ +export default function getNumberStyle(element, style, + styles = window.getComputedStyle(element, null)) { + var value = getNumber(styles[style]); + + // Support IE<=11 and W3C spec. + if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'width') { + value += getNumber(styles.paddingLeft) + + getNumber(styles.paddingRight) + + getNumber(styles.borderLeftWidth) + + getNumber(styles.borderRightWidth); + } else if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'height') { + value += getNumber(styles.paddingTop) + + getNumber(styles.paddingBottom) + + getNumber(styles.borderTopWidth) + + getNumber(styles.borderBottomWidth); + } + + return value; +} diff --git a/src/get-number.js b/src/get-number.js index b0d2a22..220c7da 100644 --- a/src/get-number.js +++ b/src/get-number.js @@ -10,7 +10,7 @@ export default function getNumber(value) { let str = value && value.toString(); let val = parseFloat(str); if (val + 1 >= 0) { - return value; + return val; } return 0; diff --git a/src/shuffle-item.js b/src/shuffle-item.js index 6bb5758..27ed89f 100644 --- a/src/shuffle-item.js +++ b/src/shuffle-item.js @@ -21,7 +21,7 @@ class ShuffleItem { init() { this.addClasses([Classes.SHUFFLE_ITEM, Classes.FILTERED]); - this._applyCss(ShuffleItem.css); + this.applyCss(ShuffleItem.css); this.scale = ShuffleItem.Scale.VISIBLE; this.point = new Point(); } @@ -32,11 +32,28 @@ class ShuffleItem { }); } - _applyCss(obj) { + removeClasses(classes) { + classes.forEach((className) => { + this.element.classList.remove(className); + }); + } + + applyCss(obj) { Object.keys(obj).forEach((key) => { this.element.style[key] = obj[key]; }); } + + dispose() { + this.removeClasses([ + Classes.CONCEALED, + Classes.FILTERED, + Classes.SHUFFLE_ITEM, + ]); + + this.element.removeAttribute('style'); + this.element = null; + } } ShuffleItem.css = { diff --git a/src/shuffle.es6.js b/src/shuffle.es6.js index d189df6..7a1b5e2 100644 --- a/src/shuffle.es6.js +++ b/src/shuffle.es6.js @@ -1,63 +1,19 @@ 'use strict'; import matches from 'matches-selector'; +import throttle from './throttle'; import Point from './point'; import ShuffleItem from './shuffle-item'; import Classes from './classes'; -import getNumber from './get-number'; +import getNumberStyle from './get-number-style'; import sorter from './sorter'; +import assign from './assign'; import './custom-event'; function toArray(arrayLike) { return Array.prototype.slice.call(arrayLike); } -// Constants -const SHUFFLE = 'shuffle'; - -// Configurable. You can change these constants to fit your application. -// The default scale and concealed scale, however, have to be different values. -var ALL_ITEMS = 'all'; -var FILTER_ATTRIBUTE_KEY = 'groups'; -var DEFAULT_SCALE = 1; -var CONCEALED_SCALE = 0.001; - -// Underscore's throttle function. -function throttle(func, wait, options) { - var context, args, result; - var timeout = null; - var previous = 0; - options = options || {}; - var later = function () { - previous = options.leading === false ? 0 : Date.now(); - timeout = null; - result = func.apply(context, args); - context = args = null; - }; - - return function () { - var now = Date.now(); - if (!previous && options.leading === false) { - previous = now; - } - - var remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0 || remaining > wait) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - context = args = null; - } else if (!timeout && options.trailing !== false) { - timeout = setTimeout(later, remaining); - } - - return result; - }; -} - function each(obj, iterator, context) { for (var i = 0, length = obj.length; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === {}) { @@ -78,21 +34,7 @@ function arrayMin(array) { return Math.min.apply(Math, array); } -var getStyles = window.getComputedStyle; - -const COMPUTED_SIZE_INCLUDES_PADDING = (function () { - var parent = document.body || document.documentElement; - var e = document.createElement('div'); - e.style.cssText = 'width:10px;padding:2px;box-sizing:border-box;'; - parent.appendChild(e); - - var width = getStyles(e, null).width; - var ret = width === '10px'; - - parent.removeChild(e); - - return ret; -}()); +function noop() {} // Used for unique instance variables let id = 0; @@ -107,7 +49,18 @@ class Shuffle { * @constructor */ constructor(element, options = {}) { - Object.assign(this, Shuffle.options, options, Shuffle.settings); + assign(this, Shuffle.options, options); + + this.useSizer = false; + this.revealAppendedDelay = 300; + this.lastSort = {}; + this.lastFilter = Shuffle.ALL_ITEMS; + this.isEnabled = true; + this.isDestroyed = false; + this.isInitialized = false; + this._transitions = []; + this._isMovementCanceled = false; + this._queue = []; element = this._getElementOption(element); @@ -124,7 +77,7 @@ class Shuffle { // Dispatch the done event asynchronously so that people can bind to it after // Shuffle has been initialized. defer(function () { - this.initialized = true; + this.isInitialized = true; this._dispatch(Shuffle.EventType.DONE); }, this, 16); } @@ -150,7 +103,7 @@ class Shuffle { // Get container css all in one request. Causes reflow var containerCSS = window.getComputedStyle(this.element, null); - var containerWidth = Shuffle._getOuterWidth(this.element); + var containerWidth = Shuffle.getSize(this.element).width; // Add styles to the container if it doesn't have them. this._validateStyles(containerCSS); @@ -160,16 +113,14 @@ class Shuffle { this._setColumns(containerWidth); // Kick off! - this.shuffle(this.group, this.initialSort); + this.filter(this.group, this.initialSort); // The shuffle items haven't had transitions set on them yet // so the user doesn't see the first layout. Set them now that the first layout is done. - if (this.supported) { - defer(function () { - this._setTransitions(); - this.element.style.transition = 'height ' + this.speed + 'ms ' + this.easing; - }, this); - } + defer(function () { + this._setTransitions(); + this.element.style.transition = 'height ' + this.speed + 'ms ' + this.easing; + }, this); } /** @@ -249,7 +200,7 @@ class Shuffle { this.group = category; } - return set.filtered; + return set; } /** @@ -264,7 +215,7 @@ class Shuffle { var concealed = []; // category === 'all', add filtered class to everything - if (category === ALL_ITEMS) { + if (category === Shuffle.ALL_ITEMS) { filtered = items; // Loop through each item and use provided function to determine @@ -298,7 +249,7 @@ class Shuffle { // Check each element's data-groups attribute against the given category. } else { - let attr = item.element.getAttribute('data-' + FILTER_ATTRIBUTE_KEY); + let attr = item.element.getAttribute('data-' + Shuffle.FILTER_ATTRIBUTE_KEY); let groups = JSON.parse(attr); let keys = this.delimeter && !Array.isArray(groups) ? groups.split(this.delimeter) : @@ -348,24 +299,27 @@ class Shuffle { this.visibleItems = this._getFilteredItems().length; } - /** - * Sets css transform transition on a an element. - * @param {Element} element Element to set transition on. - * @private - */ - _setTransition(element) { - element.style.transition = 'transform ' + this.speed + 'ms ' + - this.easing + ', opacity ' + this.speed + 'ms ' + this.easing; - } - /** * Sets css transform transition on a group of elements. * @param {ArrayLike.} $items Elements to set transitions on. * @private */ _setTransitions(items = this.items) { + let speed = this.speed; + let easing = this.easing; + + var str; + if (this.useTransforms) { + str = 'transform ' + speed + 'ms ' + easing + + ', opacity ' + speed + 'ms ' + easing; + } else { + str = 'top ' + speed + 'ms ' + easing + + ', left ' + speed + 'ms ' + easing + + ', opacity ' + speed + 'ms ' + easing; + } + each(items, function (item) { - this._setTransition(item.element); + item.element.style.transition = str; }, this); } @@ -375,11 +329,9 @@ class Shuffle { * @param {ArrayLike.} $collection Array to iterate over. */ _setSequentialDelay($collection) { - if (!this.supported) { - return; - } // $collection can be an array of dom elements or jquery object + // FIXME won't work for noTransforms each($collection, function (el, i) { // This works because the transition-property: transform, opacity; el.style.transitionDelay = '0ms,' + ((i + 1) * this.sequentialFadeDelay) + 'ms'; @@ -416,7 +368,7 @@ class Shuffle { // columnWidth option isn't a function, are they using a sizing element? } else if (this.useSizer) { - size = Shuffle._getOuterWidth(this.sizer); + size = Shuffle.getSize(this.sizer).width; // if not, how about the explicitly set option? } else if (this.columnWidth) { @@ -424,7 +376,7 @@ class Shuffle { // or use the size of the first item } else if (this.items.length > 0) { - size = Shuffle._getOuterWidth(this.items[0], true); + size = Shuffle.getSize(this.items[0], true).width; // if there's no items, use size of container } else { @@ -450,7 +402,7 @@ class Shuffle { if (typeof this.gutterWidth === 'function') { size = this.gutterWidth(containerWidth); } else if (this.useSizer) { - size = Shuffle._getNumberStyle(this.sizer, 'marginLeft'); + size = getNumberStyle(this.sizer, 'marginLeft'); } else { size = this.gutterWidth; } @@ -460,11 +412,10 @@ class Shuffle { /** * Calculate the number of columns to be used. Gets css if using sizer element. - * @param {number} [theContainerWidth] Optionally specify a container width if + * @param {number} [containerWidth] Optionally specify a container width if * it's already available. */ - _setColumns(theContainerWidth) { - var containerWidth = theContainerWidth || Shuffle._getOuterWidth(this.element); + _setColumns(containerWidth = Shuffle.getSize(this.element).width) { var gutter = this._getGutterSize(containerWidth); var columnWidth = this._getColumnSize(containerWidth, gutter); var calculatedColumns = (containerWidth + gutter) / columnWidth; @@ -497,7 +448,6 @@ class Shuffle { } /** - * Fire events with .shuffle namespace * @return {boolean} Whether the event was prevented or not. */ _dispatch(name, details = {}) { @@ -526,67 +476,36 @@ class Shuffle { * @param {Array.} items Array of items that will be shown/layed out in * order in their array. Because jQuery collection are always ordered in DOM * order, we can't pass a jq collection. - * @param {boolean} [isOnlyPosition=false] If true this will position the items - * with zero opacity. */ - _layout(items, isOnlyPosition) { - each(items, function (item) { - this._layoutItem(item, !!isOnlyPosition); - }, this); - - // `_layout` always happens after `_shrink`, so it's safe to process the style - // queue here with styles from the shrink method. - this._processStyleQueue(); - - // Adjust the height of the container. - this._setContainerSize(); + _layout(items) { + each(items, this._layoutItem, this); } /** * Calculates the position of the item and pushes it onto the style queue. * @param {ShuffleItem} item ShuffleItem which is being positioned. - * @param {boolean} isOnlyPosition Whether to position the item, but with zero - * opacity so that it can fade in later. * @private */ - _layoutItem(item, isOnlyPosition) { + _layoutItem(item, i) { var currPos = item.point; var currScale = item.scale; - var itemSize = { - width: Shuffle._getOuterWidth(item.element, true), - height: Shuffle._getOuterHeight(item.element, true), - }; + var itemSize = Shuffle.getSize(item.element, true); var pos = this._getItemPosition(itemSize); // If the item will not change its position, do not add it to the render // queue. Transitions don't fire when setting a property to the same value. - if (Point.equals(currPos, pos) && currScale === DEFAULT_SCALE) { + if (Point.equals(currPos, pos) && currScale === ShuffleItem.Scale.VISIBLE) { return; } - // Save data for shrink item.point = pos; - item.scale = DEFAULT_SCALE; - - this.styleQueue.push({ - $item: window.jQuery(item.element), - point: pos, - scale: DEFAULT_SCALE, - opacity: isOnlyPosition ? 0 : 1, - - // Set styles immediately if there is no transition speed. - skipTransition: isOnlyPosition || this.speed === 0, - callfront: function () { - if (!isOnlyPosition) { - item.element.style.visibility = 'visible'; - } - }, + item.scale = ShuffleItem.Scale.VISIBLE; - callback: function () { - if (isOnlyPosition) { - item.element.style.visibility = 'hidden'; - } - }, + this._queue.push({ + item, + opacity: 1, + visibility: 'visible', + transitionDelay: Math.min(i * this.staggerAmount, this.staggerAmountMax), }); } @@ -711,32 +630,29 @@ class Shuffle { /** * Hides the elements that don't match our filter. - * @param {jQuery} $collection jQuery collection to shrink. + * @param {Array.} collection Collection to shrink. * @private */ - _shrink(collection) { - collection = collection || this._getConcealedItems(); - - collection.forEach((item) => { + _shrink(collection = this._getConcealedItems()) { + each(collection, (item, i) => { // Continuing would add a transitionend event listener to the element, but // that listener would not execute because the transform and opacity would // stay the same. - if (item.scale === CONCEALED_SCALE) { + if (item.scale === ShuffleItem.Scale.FILTERED) { return; } - item.scale = CONCEALED_SCALE; + item.scale = ShuffleItem.Scale.FILTERED; - this.styleQueue.push({ - $item: window.jQuery(item.element), - point: item.point, - scale: CONCEALED_SCALE, + this._queue.push({ + item, opacity: 0, - callback: function () { + transitionDelay: Math.min(i * this.staggerAmount, this.staggerAmountMax), + callback() { item.element.style.visibility = 'hidden'; }, }); - }); + }, this); } /** @@ -745,12 +661,12 @@ class Shuffle { */ _handleResize() { // If shuffle is disabled, destroyed, don't do anything - if (!this.enabled || this.destroyed) { + if (!this.isEnabled || this.isDestroyed) { return; } // Will need to check height in the future if it's layed out horizontaly - var containerWidth = Shuffle._getOuterWidth(this.element); + var containerWidth = Shuffle.getSize(this.element).width; // containerWidth hasn't changed, don't do anything if (containerWidth === this.containerWidth) { @@ -762,146 +678,117 @@ class Shuffle { /** * Returns styles for either jQuery animate or transition. - * @param {Object} opts Transition options. + * @param {Object} obj Transition options. * @return {!Object} Transforms for transitions, left/top for animate. * @private */ - _getStylesForTransition(opts) { - var styles = { - opacity: opts.opacity, + _getStylesForTransition(obj) { + let item = obj.item; + let styles = { + opacity: obj.opacity, + visibility: obj.visibility, + transitionDelay: (obj.transitionDelay || 0) + 'ms', }; - if (this.supported) { - styles.transform = Shuffle._getItemTransformString(opts.point, opts.scale); + if (this.useTransforms) { + styles.transform = Shuffle._getItemTransformString(item.point, item.scale); } else { - styles.left = opts.point.x; - styles.top = opts.point.y; + styles.left = item.point.x + 'px'; + styles.top = item.point.y + 'px'; } return styles; } - /** - * Transitions an item in the grid - * - * @param {Object} opts options. - * @param {jQuery} opts.$item jQuery object representing the current item. - * @param {Point} opts.point A point object with the x and y coordinates. - * @param {number} opts.scale Amount to scale the item. - * @param {number} opts.opacity Opacity of the item. - * @param {Function} opts.callback Complete function for the animation. - * @param {Function} opts.callfront Function to call before transitioning. - * @private - */ _transition(opts) { - var styles = this._getStylesForTransition(opts); - this._startItemAnimation(opts.$item, styles, opts.callfront || window.jQuery.noop, opts.callback || window.jQuery.noop); - } + let styles = this._getStylesForTransition(opts); + let callfront = opts.callfront || noop; + let callback = opts.callback || noop; + let item = opts.item; + let _this = this; + + return new Promise((resolve) => { + let reference = { + item, + handler(evt) { + let element = evt.target; + + // Make sure this event handler has not bubbled up from a child. + if (element === evt.currentTarget) { + element.removeEventListener('transitionend', reference.handler); + element.style.transitionDelay = ''; + _this._removeTransitionReference(reference); + callback(); + resolve(); + } + }, + }; - _startItemAnimation($item, styles, callfront, callback) { - var _this = this; + callfront(); + item.applyCss(styles); - // Transition end handler removes its listener. - function handleTransitionEnd(evt) { - // Make sure this event handler has not bubbled up from a child. - if (evt.target === evt.currentTarget) { - window.jQuery(evt.target).off('transitionend', handleTransitionEnd); - _this._removeTransitionReference(reference); + // Transitions are not set until shuffle has loaded to avoid the initial transition. + if (this.isInitialized) { + item.element.addEventListener('transitionend', reference.handler); + this._transitions.push(reference); + } else { callback(); + resolve(); } - } - - var reference = { - $element: $item, - handler: handleTransitionEnd, - }; - - callfront(); - - // Transitions are not set until shuffle has loaded to avoid the initial transition. - if (!this.initialized) { - $item.css(styles); - callback(); - return; - } - - // Use CSS Transforms if we have them - if (this.supported) { - $item.css(styles); - $item.on('transitionend', handleTransitionEnd); - this._transitions.push(reference); - - // Use jQuery to animate left/top - } else { - // Save the deferred object which jQuery returns. - var anim = $item.stop(true).animate(styles, this.speed, 'swing', callback); - - // Push the animation to the list of pending animations. - this._animations.push(anim.promise()); - } + }); } /** * Execute the styles gathered in the style queue. This applies styles to elements, * triggering transitions. - * @param {boolean} noLayout Whether to trigger a layout event. + * @param {boolean} withLayout Whether to trigger a layout event. * @private */ - _processStyleQueue(noLayout) { + _processQueue(withLayout = true) { if (this.isTransitioning) { this._cancelMovement(); } - var $transitions = window.jQuery(); - // Iterate over the queue and keep track of ones that use transitions. - each(this.styleQueue, function (transitionObj) { - if (transitionObj.skipTransition) { - this._styleImmediately(transitionObj); + let immediates = []; + let transitions = []; + this._queue.forEach((obj) => { + if (!this.isInitialized || this.speed === 0) { + immediates.push(obj); } else { - $transitions = $transitions.add(transitionObj.$item); - this._transition(transitionObj); + transitions.push(obj); } - }, this); + }); + + immediates.forEach((obj) => { + this._styleImmediately(obj); + }); - if ($transitions.length > 0 && this.initialized && this.speed > 0) { + let promises = transitions.map((obj) => { + return this._transition(obj); + }); + + if (transitions.length > 0 && this.speed > 0) { // Set flag that shuffle is currently in motion. this.isTransitioning = true; - if (this.supported) { - this._whenCollectionDone($transitions, 'transitionend', this._movementFinished); - - // The _transition function appends a promise to the animations array. - // When they're all complete, do things. - } else { - this._whenAnimationsDone(this._movementFinished); - } + Promise.all(promises).then(this._movementFinished.bind(this)); // A call to layout happened, but none of the newly filtered items will // change position. Asynchronously fire the callback here. - } else if (!noLayout) { - defer(this._layoutEnd, this); + } else if (withLayout) { + defer(this._dispatchLayout, this); } // Remove everything in the style queue - this.styleQueue.length = 0; + this._queue.length = 0; } _cancelMovement() { - if (this.supported) { - // Remove the transition end event for each listener. - each(this._transitions, function (transition) { - transition.$element.off('transitionend', transition.handler); - }); - } else { - // Even when `stop` is called on the jQuery animation, its promise will - // still be resolved. Since it cannot be determine from within that callback - // whether the animation was stopped or not, a flag is set here to distinguish - // between the two states. - this._isMovementCanceled = true; - this.items.stop(true); - this._isMovementCanceled = false; - } + // Remove the transition end event for each listener. + each(this._transitions, (transition) => { + transition.item.element.removeEventListener('transitionend', transition.handler); + }); // Reset the array. this._transitions.length = 0; @@ -911,7 +798,7 @@ class Shuffle { } _removeTransitionReference(ref) { - var indexInArray = window.jQuery.inArray(ref, this._transitions); + let indexInArray = this._transitions.indexOf(ref); if (indexInArray > -1) { this._transitions.splice(indexInArray, 1); } @@ -922,18 +809,18 @@ class Shuffle { * @param {Object} opts Transitions options object. * @private */ - _styleImmediately(opts) { - Shuffle._skipTransition(opts.$item[0], function () { - opts.$item.css(this._getStylesForTransition(opts)); - }, this); + _styleImmediately(obj) { + Shuffle._skipTransition(obj.item.element, () => { + obj.item.applyCss(this._getStylesForTransition(obj)); + }); } _movementFinished() { this.isTransitioning = false; - this._layoutEnd(); + this._dispatchLayout(); } - _layoutEnd() { + _dispatchLayout() { this._dispatch(Shuffle.EventType.LAYOUT); } @@ -949,31 +836,32 @@ class Shuffle { // Shrink all items (without transitions). this._shrink($newItems); - each(this.styleQueue, function (transitionObj) { + each(this._queue, function (transitionObj) { transitionObj.skipTransition = true; }); // Apply shrink positions, but do not cause a layout event. - this._processStyleQueue(true); + this._processQueue(false); if (addToEnd) { this._addItemsToEnd($newItems, isSequential); } else { - this.shuffle(this.lastFilter); + this.filter(this.lastFilter); } } _addItemsToEnd($newItems, isSequential) { // Get ones that passed the current filter - var $passed = this._filter(null, $newItems); + var $passed = this._filter(null, $newItems).filtered; var passed = $passed.get(); // How many filtered elements? this._updateItemCount(); + // FIXME won't process queue. this._layout(passed, true); - if (isSequential && this.supported) { + if (isSequential) { this._setSequentialDelay(passed); } @@ -993,7 +881,7 @@ class Shuffle { $item: $item, opacity: 1, point: $item.data('point'), - scale: DEFAULT_SCALE, + scale: ShuffleItem.Scale.VISIBLE, }); }, this); @@ -1004,79 +892,29 @@ class Shuffle { }, this, this.revealAppendedDelay); } - /** - * Execute a function when an event has been triggered for every item in a collection. - * @param {jQuery} $collection Collection of elements. - * @param {string} eventName Event to listen for. - * @param {Function} callback Callback to execute when they're done. - * @private - */ - _whenCollectionDone($collection, eventName, callback) { - var done = 0; - var items = $collection.length; - var _this = this; - - function handleEventName(evt) { - if (evt.target === evt.currentTarget) { - window.jQuery(evt.target).off(eventName, handleEventName); - done++; - - // Execute callback if all items have emitted the correct event. - if (done === items) { - _this._removeTransitionReference(reference); - callback.call(_this); - } - } - } - - var reference = { - $element: $collection, - handler: handleEventName, - }; - - // Bind the event to all items. - $collection.on(eventName, handleEventName); - - // Keep track of transitionend events so they can be removed. - this._transitions.push(reference); - } - - /** - * Execute a callback after jQuery `animate` for a collection has finished. - * @param {Function} callback Callback to execute when they're done. - * @private - */ - _whenAnimationsDone(callback) { - window.jQuery.when.apply(null, this._animations).always(window.jQuery.proxy(function () { - this._animations.length = 0; - if (!this._isMovementCanceled) { - callback.call(this); - } - }, this)); - } - /** * The magic. This is what makes the plugin 'shuffle' - * @param {string|Function} [category] Category to filter by. Can be a function + * @param {string|Function|Array.} [category] Category to filter by. + * Can be a function, string, or array of strings. * @param {Object} [sortObj] A sort object which can sort the filtered set */ - shuffle(category, sortObj) { - if (!this.enabled) { + filter(category, sortObj) { + if (!this.isEnabled) { return; } - if (!category) { - category = ALL_ITEMS; + if (!category || (category && category.length === 0)) { + category = Shuffle.ALL_ITEMS; } this._filter(category); - // How many filtered elements? - this._updateItemCount(); - // Shrink each concealed item this._shrink(); + // How many filtered elements? + this._updateItemCount(); + // Update transforms on .filtered elements so they will animate to their new positions this.sort(sortObj); } @@ -1085,18 +923,26 @@ class Shuffle { * Gets the .filtered elements, sorts them, and passes them to layout. * @param {Object} opts the options object for the sorted plugin */ - sort(opts) { - if (this.enabled) { - this._resetCols(); + sort(opts = this.lastSort) { + if (!this.isEnabled) { + return; + } - var sortOptions = opts || this.lastSort; - var items = this._getFilteredItems(); - items = sorter(items, sortOptions); + this._resetCols(); - this._layout(items); + var items = this._getFilteredItems(); + items = sorter(items, opts); - this.lastSort = sortOptions; - } + this._layout(items); + + // `_layout` always happens after `_shrink`, so it's safe to process the style + // queue here with styles from the shrink method. + this._processQueue(); + + // Adjust the height of the container. + this._setContainerSize(); + + this.lastSort = opts; } /** @@ -1105,7 +951,7 @@ class Shuffle { * recalculated. */ update(isOnlyLayout) { - if (this.enabled) { + if (this.isEnabled) { if (!isOnlyLayout) { // Get updated colCount @@ -1141,7 +987,7 @@ class Shuffle { * Disables shuffle from updating dimensions and layout on resize */ disable() { - this.enabled = false; + this.isEnabled = false; } /** @@ -1149,7 +995,7 @@ class Shuffle { * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters */ enable(isUpdateLayout) { - this.enabled = true; + this.isEnabled = true; if (isUpdateLayout !== false) { this.update(); } @@ -1187,7 +1033,7 @@ class Shuffle { this.sort(); - this.$el.one(Shuffle.EventType.LAYOUT + '.' + SHUFFLE, window.jQuery.proxy(handleRemoved, this)); + this.$el.one(Shuffle.EventType.LAYOUT + '.shuffle', window.jQuery.proxy(handleRemoved, this)); } /** @@ -1197,21 +1043,13 @@ class Shuffle { window.removeEventListener('resize', this._onResize); // Reset container styles - this.$el - .removeClass(SHUFFLE) - .removeAttr('style') - .removeData(SHUFFLE); + this.element.classList.remove('shuffle'); + this.element.removeAttribute('style'); // Reset individual item styles - this.items - .removeAttr('style') - .removeData('point') - .removeData('scale') - .removeClass([ - Shuffle.ClassName.CONCEALED, - Shuffle.ClassName.FILTERED, - Shuffle.ClassName.SHUFFLE_ITEM, - ].join(' ')); + this.items.forEach((item) => { + item.dispose(); + }); // Null DOM references this.items = null; @@ -1221,22 +1059,108 @@ class Shuffle { this._transitions = null; // Set a flag so if a debounced resize has been triggered, - // it can first check if it is actually destroyed and not doing anything + // it can first check if it is actually isDestroyed and not doing anything this.destroyed = true; } + + /** + * Get the CSS transform based on position and scale. + * @param {Point} point X and Y positions. + * @param {number} scale Scale amount. + * @return {string} A normalized string which can be used with the transform style. + * @private + */ + static _getItemTransformString(point, scale) { + return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')'; + } + + /** + * Returns the outer width of an element, optionally including its margins. + * + * There are a few different methods for getting the width of an element, none of + * which work perfectly for all Shuffle's use cases. + * + * 1. getBoundingClientRect() `left` and `right` properties. + * - Accounts for transform scaled elements, making it useless for Shuffle + * elements which have shrunk. + * 2. The `offsetWidth` property (or jQuery's CSS). + * - This value stays the same regardless of the elements transform property, + * however, it does not return subpixel values. + * 3. getComputedStyle() + * - This works great Chrome, Firefox, Safari, but IE<=11 does not include + * padding and border when box-sizing: border-box is set, requiring a feature + * test and extra work to add the padding back for IE and other browsers which + * follow the W3C spec here. + * + * @param {Element} element The element. + * @param {boolean} [includeMargins] Whether to include margins. Default is false. + * @return {{width: number, height: number}} The width and height. + */ + static getSize(element, includeMargins) { + // Store the styles so that they can be used by others without asking for it again. + var styles = window.getComputedStyle(element, null); + var width = getNumberStyle(element, 'width', styles); + var height = getNumberStyle(element, 'height', styles); + + // Use jQuery here because it uses getComputedStyle internally and is + // cross-browser. Using the style property of the element will only work + // if there are inline styles. + if (includeMargins) { + var marginLeft = getNumberStyle(element, 'marginLeft', styles); + var marginRight = getNumberStyle(element, 'marginRight', styles); + var marginTop = getNumberStyle(element, 'marginTop', styles); + var marginBottom = getNumberStyle(element, 'marginBottom', styles); + width += marginLeft + marginRight; + height += marginTop + marginBottom; + } + + return { + width, + height, + }; + } + + /** + * Change a property or execute a function which will not have a transition + * @param {Element} element DOM element that won't be transitioned + * @param {Function} callback A function which will be called while transition + * is set to 0ms. + * @private + */ + static _skipTransition(element, callback) { + let style = element.style; + var duration = style.transitionDuration; + var delay = style.transitionDelay; + + // Set the duration to zero so it happens immediately + style.transitionDuration = '0ms'; + style.transitionDelay = '0ms'; + + callback(); + + // Force reflow + var reflow = element.offsetWidth; + + // Avoid jshint warnings: unused variables and expressions. + reflow = null; + + // Put the duration back + style.transitionDuration = duration; + style.transitionDelay = delay; + } } +Shuffle.ALL_ITEMS = 'all'; +Shuffle.FILTER_ATTRIBUTE_KEY = 'groups'; /** - * Events the container element emits with the .shuffle namespace. - * For example, "done.shuffle". * @enum {string} */ Shuffle.EventType = { - LOADING: 'loading', - DONE: 'done', - LAYOUT: 'layout', - REMOVED: 'removed', + LOADING: 'shuffle:loading', + DONE: 'shuffle:done', + LAYOUT: 'shuffle:layout', + REMOVED: 'shuffle:removed', }; /** @enum {string} */ @@ -1244,182 +1168,68 @@ Shuffle.ClassName = Classes; // Overrideable options Shuffle.options = { - group: ALL_ITEMS, // Initial filter group. - speed: 250, // Transition/animation speed (milliseconds). - easing: 'ease-out', // CSS easing function to use. - itemSelector: '', // e.g. '.picture-item'. - sizer: null, // Sizer element. Use an element to determine the size of columns and gutters. - gutterWidth: 0, // A static number or function that tells the plugin how wide the gutters between columns are (in pixels). - columnWidth: 0, // A static number or function that returns a number which tells the plugin how wide the columns are (in pixels). - delimeter: null, // If your group is not json, and is comma delimeted, you could set delimeter to ','. - buffer: 0, // Useful for percentage based heights when they might not always be exactly the same (in pixels). - columnThreshold: 0.01, // Reading the width of elements isn't precise enough and can cause columns to jump between values. - initialSort: null, // Shuffle can be initialized with a sort object. It is the same object given to the sort method. - throttle: throttle, // By default, shuffle will throttle resize events. This can be changed or removed. - throttleTime: 300, // How often shuffle can be called on resize (in milliseconds). - sequentialFadeDelay: 150, // Delay between each item that fades in when adding items. - supported: true, // Whether to use transforms or absolute positioning. -}; + // Initial filter group. + group: Shuffle.ALL_ITEMS, -// Not overrideable -Shuffle.settings = { - useSizer: false, - revealAppendedDelay: 300, - lastSort: {}, - lastFilter: ALL_ITEMS, - enabled: true, - destroyed: false, - initialized: false, - _animations: [], - _transitions: [], - _isMovementCanceled: false, - styleQueue: [], -}; + // Transition/animation speed (milliseconds). + speed: 250, -// Expose for testing. -Shuffle.Point = Point; -Shuffle.ShuffleItem = ShuffleItem; -Shuffle.sorter = sorter; - -/** - * Static methods. - */ + // CSS easing function to use. + easing: 'ease', -/** - * If the browser has 3d transforms available, build a string with those, - * otherwise use 2d transforms. - * @param {Point} point X and Y positions. - * @param {number} scale Scale amount. - * @return {string} A normalized string which can be used with the transform style. - * @private - */ -Shuffle._getItemTransformString = function (point, scale) { - return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')'; -}; + // e.g. '.picture-item'. + itemSelector: '', -/** - * Retrieve the computed style for an element, parsed as a float. - * @param {Element} element Element to get style for. - * @param {string} style Style property. - * @param {CSSStyleDeclaration} [styles] Optionally include clean styles to - * use instead of asking for them again. - * @return {number} The parsed computed value or zero if that fails because IE - * will return 'auto' when the element doesn't have margins instead of - * the computed style. - * @private - */ -Shuffle._getNumberStyle = function (element, style, styles) { - styles = styles || getStyles(element, null); - var value = Shuffle._getFloat(styles[style]); - - // Support IE<=11 and W3C spec. - if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'width') { - value += Shuffle._getFloat(styles.paddingLeft) + - Shuffle._getFloat(styles.paddingRight) + - Shuffle._getFloat(styles.borderLeftWidth) + - Shuffle._getFloat(styles.borderRightWidth); - } else if (!COMPUTED_SIZE_INCLUDES_PADDING && style === 'height') { - value += Shuffle._getFloat(styles.paddingTop) + - Shuffle._getFloat(styles.paddingBottom) + - Shuffle._getFloat(styles.borderTopWidth) + - Shuffle._getFloat(styles.borderBottomWidth); - } + // Sizer element. Use an element to determine the size of columns and gutters. + sizer: null, - return value; -}; + // A static number or function that tells the plugin how wide the gutters + // between columns are (in pixels). + gutterWidth: 0, -/** - * Parse a string as an float. - * @param {string} value String float. - * @return {number} The string as an float or zero. - * @private - */ -Shuffle._getFloat = function (value) { - return getNumber(parseFloat(value)); -}; + // A static number or function that returns a number which tells the plugin + // how wide the columns are (in pixels). + columnWidth: 0, -/** - * Returns the outer width of an element, optionally including its margins. - * - * There are a few different methods for getting the width of an element, none of - * which work perfectly for all Shuffle's use cases. - * - * 1. getBoundingClientRect() `left` and `right` properties. - * - Accounts for transform scaled elements, making it useless for Shuffle - * elements which have shrunk. - * 2. The `offsetWidth` property (or jQuery's CSS). - * - This value stays the same regardless of the elements transform property, - * however, it does not return subpixel values. - * 3. getComputedStyle() - * - This works great Chrome, Firefox, Safari, but IE<=11 does not include - * padding and border when box-sizing: border-box is set, requiring a feature - * test and extra work to add the padding back for IE and other browsers which - * follow the W3C spec here. - * - * @param {Element} element The element. - * @param {boolean} [includeMargins] Whether to include margins. Default is false. - * @return {number} The width. - */ -Shuffle._getOuterWidth = function (element, includeMargins) { - // Store the styles so that they can be used by others without asking for it again. - var styles = getStyles(element, null); - var width = Shuffle._getNumberStyle(element, 'width', styles); - - // Use jQuery here because it uses getComputedStyle internally and is - // cross-browser. Using the style property of the element will only work - // if there are inline styles. - if (includeMargins) { - var marginLeft = Shuffle._getNumberStyle(element, 'marginLeft', styles); - var marginRight = Shuffle._getNumberStyle(element, 'marginRight', styles); - width += marginLeft + marginRight; - } + // If your group is not json, and is comma delimeted, you could set delimeter + // to ','. + delimeter: null, - return width; -}; + // Useful for percentage based heights when they might not always be exactly + // the same (in pixels). + buffer: 0, -/** - * Returns the outer height of an element, optionally including its margins. - * @param {Element} element The element. - * @param {boolean} [includeMargins] Whether to include margins. Default is false. - * @return {number} The height. - */ -Shuffle._getOuterHeight = function (element, includeMargins) { - var styles = getStyles(element, null); - var height = Shuffle._getNumberStyle(element, 'height', styles); - - if (includeMargins) { - var marginTop = Shuffle._getNumberStyle(element, 'marginTop', styles); - var marginBottom = Shuffle._getNumberStyle(element, 'marginBottom', styles); - height += marginTop + marginBottom; - } + // Reading the width of elements isn't precise enough and can cause columns to + // jump between values. + columnThreshold: 0.01, - return height; -}; + // Shuffle can be isInitialized with a sort object. It is the same object + // given to the sort method. + initialSort: null, -/** - * Change a property or execute a function which will not have a transition - * @param {Element} element DOM element that won't be transitioned - * @param {Function} callback A function which will be called while transition - * is set to 0ms. - * @param {Object} [context] Optional context for the callback function. - * @private - */ -Shuffle._skipTransition = function (element, callback, context) { - var duration = element.style.transitionDuration; + // By default, shuffle will throttle resize events. This can be changed or + // removed. + throttle: throttle, - // Set the duration to zero so it happens immediately - element.style.transitionDuration = '0ms'; + // How often shuffle can be called on resize (in milliseconds). + throttleTime: 300, - callback.call(context); + // Delay between each item that fades in when adding items. + sequentialFadeDelay: 150, - // Force reflow - var reflow = element.offsetWidth; + // Transition delay offset for each item in milliseconds. + staggerAmount: 15, - // Avoid jshint warnings: unused variables and expressions. - reflow = null; + // It can look a little weird when the last element is in the top row + staggerAmountMax: 250, - // Put the duration back - element.style.transitionDuration = duration; + // Whether to use transforms or absolute positioning. + useTransforms: true, }; +// Expose for testing. +Shuffle.Point = Point; +Shuffle.ShuffleItem = ShuffleItem; +Shuffle.sorter = sorter; + module.exports = Shuffle; diff --git a/src/sorter.js b/src/sorter.js index 5fe2005..fb0c658 100644 --- a/src/sorter.js +++ b/src/sorter.js @@ -1,5 +1,7 @@ 'use strict'; +import assign from './assign'; + // http://stackoverflow.com/a/962890/373422 function randomize(array) { var tmp; @@ -29,12 +31,16 @@ let defaults = { // If true, this will skip the sorting and return a randomized order in the array randomize: false, + + // Determines which property of each item in the array is passed to the + // sorting method. + key: 'element', }; // You can return `undefined` from the `by` function to revert to DOM order. export default function sorter(arr, options) { - let opts = Object.assign({}, defaults, options); - let original = Array.from(arr); + let opts = assign({}, defaults, options); + let original = [].slice.call(arr); let revert = false; if (!arr.length) { @@ -47,7 +53,7 @@ export default function sorter(arr, options) { // Sort the elements by the opts.by function. // If we don't have opts.by, default to DOM order - if (typeof options.by === 'function') { + if (typeof opts.by === 'function') { arr.sort(function (a, b) { // Exit early if we already know we want to revert @@ -55,8 +61,8 @@ export default function sorter(arr, options) { return 0; } - let valA = opts.by(a); - let valB = opts.by(b); + let valA = opts.by(a[opts.key]); + let valB = opts.by(b[opts.key]); // If both values are undefined, use the DOM order if (valA === undefined && valB === undefined) { diff --git a/src/throttle.js b/src/throttle.js new file mode 100644 index 0000000..91e8326 --- /dev/null +++ b/src/throttle.js @@ -0,0 +1,39 @@ +'use strict'; + +// Underscore's throttle method. +export default function(func, wait, options) { + var _this; + var args; + var result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function () { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(_this, args); + if (!timeout) _this = args = null; + }; + + return function () { + var now = Date.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + _this = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + previous = now; + result = func.apply(_this, args); + if (!timeout) _this = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + + return result; + }; +} diff --git a/test/specs.js b/test/specs.js index d854048..e05a96b 100644 --- a/test/specs.js +++ b/test/specs.js @@ -618,22 +618,22 @@ describe('Shuffle.js', function() { document.body.appendChild(div); - expect(window.Shuffle._getOuterWidth(div, false)).toBe(100); - expect(window.Shuffle._getOuterWidth(div, true)).toBe(100); + expect(window.Shuffle.getSize(div, false).width).toBe(100); + expect(window.Shuffle.getSize(div, true).width).toBe(100); - expect(window.Shuffle._getOuterHeight(div, false)).toBe(100); - expect(window.Shuffle._getOuterHeight(div, true)).toBe(100); + expect(window.Shuffle.getSize(div, false).height).toBe(100); + expect(window.Shuffle.getSize(div, true).height).toBe(100); div.style.marginLeft = '10px'; div.style.marginRight = '20px'; div.style.marginTop = '30px'; div.style.marginBottom = '40px'; - expect(window.Shuffle._getOuterWidth(div, false)).toBe(100); - expect(window.Shuffle._getOuterWidth(div, true)).toBe(130); + expect(window.Shuffle.getSize(div, false).width).toBe(100); + expect(window.Shuffle.getSize(div, true).width).toBe(130); - expect(window.Shuffle._getOuterHeight(div, false)).toBe(100); - expect(window.Shuffle._getOuterHeight(div, true)).toBe(170); + expect(window.Shuffle.getSize(div, false).height).toBe(100); + expect(window.Shuffle.getSize(div, true).height).toBe(170); document.body.removeChild(div); });