diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index c8cbd79..0000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,163 +0,0 @@
-module.exports = function(grunt) {
- require('load-grunt-tasks')(grunt);
-
- var banner = [
- '/*!',
- ' * Shuffle.js by @Vestride',
- ' * Categorize, sort, and filter a responsive grid of items.',
- ' * Dependencies: jQuery 1.9+, Modernizr 2.6.2+',
- ' * @license MIT license',
- ' * @version <%= pkg.version %>',
- ' */\n'
- ].join('\n');
-
-
-
- // Project configuration.
- grunt.initConfig({
- pkg: grunt.file.readJSON('package.json'),
-
- shell: {
- serve: {
- options: {
- stdout: true,
- stderr: true,
- failOnError: true
- },
- command: 'jekyll serve --watch --config _config.yml,_config_dev.yml'
- }
- },
-
- watch: {
- livereload: {
- options: {
- livereload: true,
- interupt: true
- },
- files: [
- '_site/**/*.{html,css,js,png,jpg,jpeg,gif,webp,svg,json}'
- ]
- },
- css: {
- files: '_scss/*.scss',
- tasks: ['compile-css'],
- },
- src: {
- files: 'src/*.js',
- tasks: ['concat', 'test']
- },
- test: {
- files: 'test/specs.js',
- tasks: ['test']
- }
- },
-
- sass: {
- main: {
- options: {
- style: 'expanded'
- },
- files: {
- 'temp/gallery.css': '_scss/gallery.scss',
- 'temp/shuffle-styles.css': '_scss/shuffle-styles.scss',
- 'temp/style.css': '_scss/style.scss'
- }
- }
- },
-
- autoprefixer: {
- options: {
- browsers: ['> 1%', 'last 2 versions']
- },
-
- main: {
- expand: true,
- flatten: true,
- src: 'temp/*.css',
- dest: 'css/'
- }
- },
-
- concat: {
- options: {
- banner: banner
- },
- main: {
- src: ['src/intro.js', 'src/shuffle.js', 'src/outro.js'],
- dest: 'dist/jquery.shuffle.js'
- },
- modernizr: {
- src: ['src/modernizr.custom.min.js', 'src/intro.js', 'src/shuffle.js', 'src/outro.js'],
- dest: 'dist/jquery.shuffle.modernizr.js'
- }
- },
-
- uglify: {
- options: {
- preserveComments: false,
- banner: banner,
- report: 'min',
- mangle: true,
- compress: {}
- },
- main: {
- src: 'dist/jquery.shuffle.js',
- dest: 'dist/jquery.shuffle.min.js'
- },
- modernizr: {
- src: 'dist/jquery.shuffle.modernizr.js',
- dest: 'dist/jquery.shuffle.modernizr.min.js'
- }
- },
-
- jasmine: {
- main: {
- src: 'dist/jquery.shuffle.js',
- options: {
- specs: 'test/specs.js',
- vendor: [
- 'dist/modernizr.custom.min.js',
- 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js',
- 'bower_components/jasmine-jquery/lib/jasmine-jquery.js'
- ],
- outfile: 'test/_SpecRunner.html',
- keepRunner: true
- }
- }
- }
- });
-
-
- grunt.registerTask('compile-css', 'compile and prefix css', function() {
- grunt.task.run('sass:main');
- grunt.task.run('autoprefixer:main');
- });
-
- // Use Jekyll to watch and rebuild files.
- grunt.registerTask('serve', function() {
- grunt.task.run(['build', 'shell:serve']);
- });
-
-
- grunt.registerTask('build', function() {
- // Copy over custom modernizr build.
- grunt.file.copy('src/modernizr.custom.min.js', 'dist/modernizr.custom.min.js');
-
- // Run concat and minfication.
- grunt.task.run([
- 'concat:main',
- 'concat:modernizr',
- 'uglify:main',
- 'uglify:modernizr',
- 'test'
- ]);
- });
-
- grunt.registerTask('test', function() {
- grunt.task.run('jasmine:main');
- });
-
- // Default task(s).
- grunt.registerTask('default', ['serve']);
-
-};
diff --git a/_includes/scripts.html b/_includes/scripts.html
index 6362fd6..277e6a1 100644
--- a/_includes/scripts.html
+++ b/_includes/scripts.html
@@ -1,5 +1,5 @@
-{% if page.jquery != false %}
-
+{% if page.jquery %}
+
{% endif %}
{% if page.shuffle != false %}
diff --git a/_posts/2013-08-25-animated.html b/_posts/2013-08-25-animated.html
index b52c06c..dd36283 100644
--- a/_posts/2013-08-25-animated.html
+++ b/_posts/2013-08-25-animated.html
@@ -4,6 +4,7 @@ title: Animate In Demo
description: When elements enter the viewport, they transition in from zero opacity and from the bottom.
image: /demos/animated.jpg
prism: true
+jquery: true
extraJS: [ "viewport.js", "demos/animate-in.js" ]
---
diff --git a/src/shuffle.es6.js b/src/shuffle.es6.js
deleted file mode 100644
index 78034e3..0000000
--- a/src/shuffle.es6.js
+++ /dev/null
@@ -1,1199 +0,0 @@
-'use strict';
-
-import 'custom-event-polyfill';
-import matches from 'matches-selector';
-import arrayUnique from 'array-uniq';
-import xtend from 'xtend';
-import throttle from 'throttleit';
-import Point from './point';
-import ShuffleItem from './shuffle-item';
-import Classes from './classes';
-import getNumberStyle from './get-number-style';
-import sorter from './sorter';
-import { onTransitionEnd, cancelTransitionEnd } from './on-transition-end';
-
-function toArray(arrayLike) {
- return Array.prototype.slice.call(arrayLike);
-}
-
-function arrayMax(array) {
- return Math.max.apply(Math, array);
-}
-
-function arrayMin(array) {
- return Math.min.apply(Math, array);
-}
-
-function arrayIncludes(array, obj) {
- if (arguments.length === 2) {
- return arrayIncludes(array)(obj);
- }
-
- return function (obj) {
- return array.indexOf(obj) > -1;
- };
-}
-
-// Used for unique instance variables
-let id = 0;
-
-class Shuffle {
-
- /**
- * Categorize, sort, and filter a responsive grid of items.
- *
- * @param {Element} element An element which is the parent container for the grid items.
- * @param {Object} [options=Shuffle.options] Options object.
- * @constructor
- */
- constructor(element, options = {}) {
- this.options = xtend(Shuffle.options, options);
-
- this.useSizer = false;
- this.lastSort = {};
- this.lastFilter = Shuffle.ALL_ITEMS;
- this.isEnabled = true;
- this.isDestroyed = false;
- this.isInitialized = false;
- this._transitions = [];
- this.isTransitioning = false;
- this._queue = [];
-
- element = this._getElementOption(element);
-
- if (!element) {
- throw new TypeError('Shuffle needs to be initialized with an element.');
- }
-
- this.element = element;
- this.id = 'shuffle_' + id++;
-
- this._init();
- this.isInitialized = true;
- }
-
- _init() {
- this.items = this._getItems();
-
- this.options.sizer = this._getElementOption(this.options.sizer);
-
- if (this.options.sizer) {
- this.useSizer = true;
- }
-
- // Add class and invalidate styles
- this.element.classList.add(Shuffle.ClassName.BASE);
-
- // Set initial css for each item
- this._initItems();
-
- // Bind resize events
- this._onResize = this._getResizeFunction();
- window.addEventListener('resize', this._onResize);
-
- // Get container css all in one request. Causes reflow
- var containerCss = window.getComputedStyle(this.element, null);
- var containerWidth = Shuffle.getSize(this.element).width;
-
- // Add styles to the container if it doesn't have them.
- this._validateStyles(containerCss);
-
- // We already got the container's width above, no need to cause another
- // reflow getting it again... Calculate the number of columns there will be
- this._setColumns(containerWidth);
-
- // Kick off!
- this.filter(this.options.group, this.options.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.
- // First, however, a synchronous layout must be caused for the previous
- // styles to be applied without transitions.
- this.element.offsetWidth; // jshint ignore: line
- this._setTransitions();
- this.element.style.transition = 'height ' + this.options.speed + 'ms ' + this.options.easing;
- }
-
- /**
- * Returns a throttled and proxied function for the resize handler.
- * @return {Function}
- * @private
- */
- _getResizeFunction() {
- var resizeFunction = this._handleResize.bind(this);
- return this.options.throttle ?
- this.options.throttle(resizeFunction, this.options.throttleTime) :
- resizeFunction;
- }
-
- /**
- * Retrieve an element from an option.
- * @param {string|jQuery|Element} option The option to check.
- * @return {?Element} The plain element or null.
- * @private
- */
- _getElementOption(option) {
- // If column width is a string, treat is as a selector and search for the
- // sizer element within the outermost container
- if (typeof option === 'string') {
- return this.element.querySelector(option);
-
- // Check for an element
- } else if (option && option.nodeType && option.nodeType === 1) {
- return option;
-
- // Check for jQuery object
- } else if (option && option.jquery) {
- return option[0];
- }
-
- return null;
- }
-
- /**
- * Ensures the shuffle container has the css styles it needs applied to it.
- * @param {Object} styles Key value pairs for position and overflow.
- * @private
- */
- _validateStyles(styles) {
- // Position cannot be static.
- if (styles.position === 'static') {
- this.element.style.position = 'relative';
- }
-
- // Overflow has to be hidden
- if (styles.overflow !== 'hidden') {
- this.element.style.overflow = 'hidden';
- }
- }
-
- /**
- * Filter the elements by a category.
- * @param {string} [category] Category to filter by. If it's given, the last
- * category will be used to filter the items.
- * @param {Array} [collection] Optionally filter a collection. Defaults to
- * all the items.
- * @return {!{filtered: Array, concealed: Array}}
- * @private
- */
- _filter(category = this.lastFilter, collection = this.items) {
- var set = this._getFilteredSets(category, collection);
-
- // Individually add/remove concealed/filtered classes
- this._toggleFilterClasses(set);
-
- // Save the last filter in case elements are appended.
- this.lastFilter = category;
-
- // This is saved mainly because providing a filter function (like searching)
- // will overwrite the `lastFilter` property every time its called.
- if (typeof category === 'string') {
- this.options.group = category;
- }
-
- return set;
- }
-
- /**
- * Returns an object containing the filtered and concealed elements.
- * @param {string|Function} category Category or function to filter by.
- * @param {Array.} items A collection of items to filter.
- * @return {!{filtered: Array, concealed: Array}}
- * @private
- */
- _getFilteredSets(category, items) {
- let filtered = [];
- let concealed = [];
-
- // category === 'all', add filtered class to everything
- if (category === Shuffle.ALL_ITEMS) {
- filtered = items;
-
- // Loop through each item and use provided function to determine
- // whether to hide it or not.
- } else {
- items.forEach((item) => {
- if (this._doesPassFilter(category, item.element)) {
- filtered.push(item);
- } else {
- concealed.push(item);
- }
- });
- }
-
- return {
- filtered,
- concealed,
- };
- }
-
- /**
- * Test an item to see if it passes a category.
- * @param {string|Function} category Category or function to filter by.
- * @param {Element} element An element to test.
- * @return {boolean} Whether it passes the category/filter.
- * @private
- */
- _doesPassFilter(category, element) {
-
- if (typeof category === 'function') {
- return category.call(element, element, this);
-
- // Check each element's data-groups attribute against the given category.
- } else {
- let attr = element.getAttribute('data-' + Shuffle.FILTER_ATTRIBUTE_KEY);
- let groups = JSON.parse(attr);
- let keys = this.delimeter && !Array.isArray(groups) ?
- groups.split(this.delimeter) :
- groups;
-
- if (Array.isArray(category)) {
- return category.some(arrayIncludes(keys));
- }
-
- return arrayIncludes(keys, category);
- }
- }
-
- /**
- * Toggles the filtered and concealed class names.
- * @param {{filtered, concealed}} Object with filtered and concealed arrays.
- * @private
- */
- _toggleFilterClasses({ filtered, concealed }) {
- filtered.forEach((item) => {
- item.reveal();
- });
-
- concealed.forEach((item) => {
- item.conceal();
- });
- }
-
- /**
- * Set the initial css for each item
- * @param {Array.} [items] Optionally specifiy at set to initialize.
- * @private
- */
- _initItems(items = this.items) {
- items.forEach((item) => {
- item.init();
- });
- }
-
- /**
- * Remove element reference and styles.
- * @private
- */
- _disposeItems(items = this.items) {
- items.forEach((item) => {
- item.dispose();
- });
- }
-
- /**
- * Updates the filtered item count.
- * @private
- */
- _updateItemCount() {
- this.visibleItems = this._getFilteredItems().length;
- }
-
- /**
- * Sets css transform transition on a group of elements. This is not executed
- * at the same time as `item.init` so that transitions don't occur upon
- * initialization of Shuffle.
- * @param {Array.} items Shuffle items to set transitions on.
- * @private
- */
- _setTransitions(items = this.items) {
- let speed = this.options.speed;
- let easing = this.options.easing;
-
- var str;
- if (this.options.useTransforms) {
- str = 'transform ' + speed + 'ms ' + easing +
- ', opacity ' + speed + 'ms ' + easing;
- } else {
- str = 'top ' + speed + 'ms ' + easing +
- ', left ' + speed + 'ms ' + easing +
- ', opacity ' + speed + 'ms ' + easing;
- }
-
- items.forEach((item) => {
- item.element.style.transition = str;
- });
- }
-
- _getItems() {
- return toArray(this.element.children)
- .filter(el => matches(el, this.options.itemSelector))
- .map(el => new ShuffleItem(el));
- }
-
- /**
- * When new elements are added to the shuffle container, update the array of
- * items because that is the order `_layout` calls them.
- */
- _updateItemsOrder() {
- let children = this.element.children;
- this.items = sorter(this.items, {
- by(element) {
- return Array.prototype.indexOf.call(children, element);
- },
- });
- }
-
- _getFilteredItems() {
- return this.items.filter(item => item.isVisible);
- }
-
- _getConcealedItems() {
- return this.items.filter(item => !item.isVisible);
- }
-
- /**
- * Returns the column size, based on column width and sizer options.
- * @param {number} containerWidth Size of the parent container.
- * @param {number} gutterSize Size of the gutters.
- * @return {number}
- * @private
- */
- _getColumnSize(containerWidth, gutterSize) {
- var size;
-
- // If the columnWidth property is a function, then the grid is fluid
- if (typeof this.options.columnWidth === 'function') {
- size = this.options.columnWidth(containerWidth);
-
- // columnWidth option isn't a function, are they using a sizing element?
- } else if (this.useSizer) {
- size = Shuffle.getSize(this.options.sizer).width;
-
- // if not, how about the explicitly set option?
- } else if (this.options.columnWidth) {
- size = this.options.columnWidth;
-
- // or use the size of the first item
- } else if (this.items.length > 0) {
- size = Shuffle.getSize(this.items[0].element, true).width;
-
- // if there's no items, use size of container
- } else {
- size = containerWidth;
- }
-
- // Don't let them set a column width of zero.
- if (size === 0) {
- size = containerWidth;
- }
-
- return size + gutterSize;
- }
-
- /**
- * Returns the gutter size, based on gutter width and sizer options.
- * @param {number} containerWidth Size of the parent container.
- * @return {number}
- * @private
- */
- _getGutterSize(containerWidth) {
- var size;
- if (typeof this.options.gutterWidth === 'function') {
- size = this.options.gutterWidth(containerWidth);
- } else if (this.useSizer) {
- size = getNumberStyle(this.options.sizer, 'marginLeft');
- } else {
- size = this.options.gutterWidth;
- }
-
- return size;
- }
-
- /**
- * Calculate the number of columns to be used. Gets css if using sizer element.
- * @param {number} [containerWidth] Optionally specify a container width if
- * it's already available.
- */
- _setColumns(containerWidth = Shuffle.getSize(this.element).width) {
- var gutter = this._getGutterSize(containerWidth);
- var columnWidth = this._getColumnSize(containerWidth, gutter);
- var calculatedColumns = (containerWidth + gutter) / columnWidth;
-
- // Widths given from getStyles are not precise enough...
- if (Math.abs(Math.round(calculatedColumns) - calculatedColumns) <
- this.options.columnThreshold) {
- // e.g. calculatedColumns = 11.998876
- calculatedColumns = Math.round(calculatedColumns);
- }
-
- this.cols = Math.max(Math.floor(calculatedColumns), 1);
- this.containerWidth = containerWidth;
- this.colWidth = columnWidth;
- }
-
- /**
- * Adjust the height of the grid
- */
- _setContainerSize() {
- this.element.style.height = this._getContainerSize() + 'px';
- }
-
- /**
- * Based on the column heights, it returns the biggest one.
- * @return {number}
- * @private
- */
- _getContainerSize() {
- return arrayMax(this.positions);
- }
-
- /**
- * Get the clamped stagger amount.
- * @param {number} index Index of the item to be staggered.
- * @return {number}
- */
- _getStaggerAmount(index) {
- return Math.min(index * this.options.staggerAmount, this.options.staggerAmountMax);
- }
-
- /**
- * @return {boolean} Whether the event was prevented or not.
- */
- _dispatch(name, details = {}) {
- if (this.isDestroyed) {
- return;
- }
-
- details.shuffle = this;
- return !this.element.dispatchEvent(new CustomEvent(name, {
- bubbles: true,
- cancelable: false,
- detail: details,
- }));
- }
-
- /**
- * Zeros out the y columns array, which is used to determine item placement.
- * @private
- */
- _resetCols() {
- var i = this.cols;
- this.positions = [];
- while (i--) {
- this.positions.push(0);
- }
- }
-
- /**
- * Loops through each item that should be shown and calculates the x, y position.
- * @param {Array.} items Array of items that will be shown/layed
- * out in order in their array.
- */
- _layout(items) {
- let count = 0;
- items.forEach((item) => {
- var currPos = item.point;
- var currScale = item.scale;
- 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 === ShuffleItem.Scale.VISIBLE) {
- return;
- }
-
- item.point = pos;
- item.scale = ShuffleItem.Scale.VISIBLE;
-
- this._queue.push({
- item,
- opacity: 1,
- visibility: 'visible',
- transitionDelay: this._getStaggerAmount(count),
- });
-
- count++;
- });
- }
-
- /**
- * Determine the location of the next item, based on its size.
- * @param {{width: number, height: number}} itemSize Object with width and height.
- * @return {Point}
- * @private
- */
- _getItemPosition(itemSize) {
- var columnSpan = this._getColumnSpan(itemSize.width, this.colWidth, this.cols);
-
- var setY = this._getColumnSet(columnSpan, this.cols);
-
- // Finds the index of the smallest number in the set.
- var shortColumnIndex = this._getShortColumn(setY, this.options.buffer);
-
- // Position the item
- var point = new Point(
- Math.round(this.colWidth * shortColumnIndex),
- Math.round(setY[shortColumnIndex]));
-
- // Update the columns array with the new values for each column.
- // e.g. before the update the columns could be [250, 0, 0, 0] for an item
- // which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
- var setHeight = setY[shortColumnIndex] + itemSize.height;
- var setSpan = this.cols + 1 - setY.length;
- for (var i = 0; i < setSpan; i++) {
- this.positions[shortColumnIndex + i] = setHeight;
- }
-
- return point;
- }
-
- /**
- * Determine the number of columns an items spans.
- * @param {number} itemWidth Width of the item.
- * @param {number} columnWidth Width of the column (includes gutter).
- * @param {number} columns Total number of columns
- * @return {number}
- * @private
- */
- _getColumnSpan(itemWidth, columnWidth, columns) {
- var columnSpan = itemWidth / columnWidth;
-
- // If the difference between the rounded column span number and the
- // calculated column span number is really small, round the number to
- // make it fit.
- if (Math.abs(Math.round(columnSpan) - columnSpan) < this.options.columnThreshold) {
- // e.g. columnSpan = 4.0089945390298745
- columnSpan = Math.round(columnSpan);
- }
-
- // Ensure the column span is not more than the amount of columns in the whole layout.
- return Math.min(Math.ceil(columnSpan), columns);
- }
-
- /**
- * Retrieves the column set to use for placement.
- * @param {number} columnSpan The number of columns this current item spans.
- * @param {number} columns The total columns in the grid.
- * @return {Array.} An array of numbers represeting the column set.
- * @private
- */
- _getColumnSet(columnSpan, columns) {
- // The item spans only one column.
- if (columnSpan === 1) {
- return this.positions;
-
- // The item spans more than one column, figure out how many different
- // places it could fit horizontally.
- // The group count is the number of places within the positions this block
- // could fit, ignoring the current positions of items.
- // Imagine a 2 column brick as the second item in a 4 column grid with
- // 10px height each. Find the places it would fit:
- // [10, 0, 0, 0]
- // | | |
- // * * *
- //
- // Then take the places which fit and get the bigger of the two:
- // max([10, 0]), max([0, 0]), max([0, 0]) = [10, 0, 0]
- //
- // Next, find the first smallest number (the short column).
- // [10, 0, 0]
- // |
- // *
- //
- // And that's where it should be placed!
- } else {
- var groupCount = columns + 1 - columnSpan;
- var groupY = [];
-
- // For how many possible positions for this item there are.
- for (var i = 0; i < groupCount; i++) {
- // Find the bigger value for each place it could fit.
- groupY[i] = arrayMax(this.positions.slice(i, i + columnSpan));
- }
-
- return groupY;
- }
- }
-
- /**
- * Find index of short column, the first from the left where this item will go.
- *
- * @param {Array.} positions The array to search for the smallest number.
- * @param {number} buffer Optional buffer which is very useful when the height
- * is a percentage of the width.
- * @return {number} Index of the short column.
- * @private
- */
- _getShortColumn(positions, buffer) {
- var minPosition = arrayMin(positions);
- for (var i = 0, len = positions.length; i < len; i++) {
- if (positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer) {
- return i;
- }
- }
-
- return 0;
- }
-
- /**
- * Hides the elements that don't match our filter.
- * @param {Array.} collection Collection to shrink.
- * @private
- */
- _shrink(collection = this._getConcealedItems()) {
- let count = 0;
- collection.forEach((item) => {
- // 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 === ShuffleItem.Scale.FILTERED) {
- return;
- }
-
- item.scale = ShuffleItem.Scale.FILTERED;
-
- this._queue.push({
- item,
- opacity: 0,
- transitionDelay: this._getStaggerAmount(count),
- callback() {
- item.element.style.visibility = 'hidden';
- },
- });
-
- count++;
- });
- }
-
- /**
- * Resize handler.
- * @private
- */
- _handleResize() {
- // If shuffle is disabled, destroyed, don't do anything
- if (!this.isEnabled || this.isDestroyed) {
- return;
- }
-
- // Will need to check height in the future if it's layed out horizontaly
- var containerWidth = Shuffle.getSize(this.element).width;
-
- // containerWidth hasn't changed, don't do anything
- if (containerWidth === this.containerWidth) {
- return;
- }
-
- this.update();
- }
-
- /**
- * Returns styles which will be applied to the an item for a transition.
- * @param {Object} obj Transition options.
- * @return {!Object} Transforms for transitions, left/top for animate.
- * @private
- */
- _getStylesForTransition(obj) {
- let item = obj.item;
- let styles = {
- opacity: obj.opacity,
- visibility: obj.visibility,
- transitionDelay: (obj.transitionDelay || 0) + 'ms',
- };
-
- let x = item.point.x;
- let y = item.point.y;
-
- if (this.options.useTransforms) {
- styles.transform = `translate(${x}px, ${y}px) scale(${item.scale})`;
- } else {
- styles.left = x + 'px';
- styles.top = y + 'px';
- }
-
- return styles;
- }
-
- _whenTransitionDone(element, itemCallback) {
- // TODO what happens when the transition is canceled and the promise never resolves?
- return new Promise((resolve) => {
- let id = onTransitionEnd(element, (evt) => {
- evt.currentTarget.style.transitionDelay = '';
-
- if (itemCallback) {
- itemCallback();
- }
-
- resolve();
- });
- this._transitions.push(id);
- });
- }
-
- _transition(opts) {
- opts.item.applyCss(this._getStylesForTransition(opts));
- return this._whenTransitionDone(opts.item.element, opts.callback);
- }
-
- /**
- * Execute the styles gathered in the style queue. This applies styles to elements,
- * triggering transitions.
- * @private
- */
- _processQueue() {
- if (this.isTransitioning) {
- this._cancelMovement();
- }
-
- // Iterate over the queue and keep track of ones that use transitions.
- let immediates = [];
- let transitions = [];
- this._queue.forEach((obj) => {
- if (!this.isInitialized || this.options.speed === 0) {
- immediates.push(obj);
- } else {
- transitions.push(obj);
- }
- });
-
- this._styleImmediately(immediates);
-
- if (transitions.length > 0 && this.options.speed > 0) {
- this._startTransitions(transitions);
-
- // A call to layout happened, but none of the newly filtered items will
- // change position. Asynchronously fire the callback here.
- } else {
- setTimeout(this._dispatchLayout.bind(this), 0);
- }
-
- // Remove everything in the style queue
- this._queue.length = 0;
- }
-
- /**
- * Create a promise for each transition and wait for all of them to complete,
- * then emit the layout event.
- * @param {Array.} transitions Array of transition objects.
- */
- _startTransitions(transitions) {
- // Set flag that shuffle is currently in motion.
- this.isTransitioning = true;
-
- let promises = transitions.map(obj => this._transition(obj));
- Promise.all(promises).then(this._movementFinished.bind(this));
- }
-
- _cancelMovement() {
- // Remove the transition end event for each listener.
- this._transitions.forEach(cancelTransitionEnd);
-
- // Reset the array.
- this._transitions.length = 0;
-
- // Show it's no longer active.
- this.isTransitioning = false;
- }
-
- /**
- * Apply styles without a transition.
- * @param {Array.} objects Array of transition objects.
- * @private
- */
- _styleImmediately(objects) {
- if (objects.length) {
- let elements = objects.map(obj => obj.item.element);
-
- Shuffle._skipTransitions(elements, () => {
- objects.forEach((obj) => {
- obj.item.applyCss(this._getStylesForTransition(obj));
-
- if (obj.callback) {
- obj.callback();
- }
- });
- });
- }
- }
-
- _movementFinished() {
- this._transitions.length = 0;
- this.isTransitioning = false;
- this._dispatchLayout();
- }
-
- _dispatchLayout() {
- this._dispatch(Shuffle.EventType.LAYOUT);
- }
-
- /**
- * The magic. This is what makes the plugin 'shuffle'
- * @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
- */
- filter(category, sortObj) {
- if (!this.isEnabled) {
- return;
- }
-
- if (!category || (category && category.length === 0)) {
- category = Shuffle.ALL_ITEMS;
- }
-
- this._filter(category);
-
- // 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);
- }
-
- /**
- * Gets the .filtered elements, sorts them, and passes them to layout.
- * @param {Object} opts the options object for the sorted plugin
- */
- sort(opts = this.lastSort) {
- if (!this.isEnabled) {
- return;
- }
-
- this._resetCols();
-
- var items = this._getFilteredItems();
- items = sorter(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;
- }
-
- /**
- * Reposition everything.
- * @param {boolean} isOnlyLayout If true, column and gutter widths won't be
- * recalculated.
- */
- update(isOnlyLayout) {
- if (this.isEnabled) {
-
- if (!isOnlyLayout) {
- // Get updated colCount
- this._setColumns();
- }
-
- // Layout items
- this.sort();
- }
- }
-
- /**
- * Use this instead of `update()` if you don't need the columns and gutters updated
- * Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
- * could be off.
- */
- layout() {
- this.update(true);
- }
-
- /**
- * New items have been appended to shuffle. Mix them in with the current
- * filter or sort status.
- * @param {Array.} newItems Collection of new items.
- */
- add(newItems) {
- newItems = arrayUnique(newItems).map(el => new ShuffleItem(el));
-
- // Add classes and set initial positions.
- this._initItems(newItems);
-
- // Add transition to each item.
- this._setTransitions(newItems);
-
- // Update the list of items.
- this.items = this.items.concat(newItems);
- this._updateItemsOrder();
- this.filter(this.lastFilter);
- }
-
- /**
- * Disables shuffle from updating dimensions and layout on resize
- */
- disable() {
- this.isEnabled = false;
- }
-
- /**
- * Enables shuffle again
- * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
- */
- enable(isUpdateLayout) {
- this.isEnabled = true;
- if (isUpdateLayout !== false) {
- this.update();
- }
- }
-
- /**
- * Remove 1 or more shuffle items
- * @param {Array.} collection An array containing one or more
- * elements in shuffle
- * @return {Shuffle} The shuffle object
- */
- remove(collection) {
- if (!collection.length) {
- return;
- }
-
- collection = arrayUnique(collection);
-
- let oldItems = collection
- .map(element => this.getItemByElement(element))
- .filter(item => !!item);
-
- let handleLayout = () => {
- this.element.removeEventListener(Shuffle.EventType.LAYOUT, handleLayout);
- this._disposeItems(oldItems);
-
- // Remove the collection in the callback
- collection.forEach((element) => {
- element.parentNode.removeChild(element);
- });
-
- this._dispatch(Shuffle.EventType.REMOVED, { collection });
-
- // Let it get garbage collected
- collection = null;
- oldItems = null;
- };
-
- // Hide collection first.
- this._toggleFilterClasses({
- filtered: [],
- concealed: oldItems,
- });
-
- this._shrink(oldItems);
-
- this.sort();
-
- // Update the list of items here because `remove` could be called again
- // with an item that is in the process of being removed.
- this.items = this.items.filter(item => !arrayIncludes(oldItems, item));
- this._updateItemCount();
-
- this.element.addEventListener(Shuffle.EventType.LAYOUT, handleLayout);
- }
-
- /**
- * Retrieve a shuffle item by its element.
- * @param {Element} element Element to look for.
- * @return {?ShuffleItem} A shuffle item or null if it's not found.
- */
- getItemByElement(element) {
- for (var i = this.items.length - 1; i >= 0; i--) {
- if (this.items[i].element === element) {
- return this.items[i];
- }
- }
-
- return null;
- }
-
- /**
- * Destroys shuffle, removes events, styles, and classes
- */
- destroy() {
- this._cancelMovement();
- window.removeEventListener('resize', this._onResize);
-
- // Reset container styles
- this.element.classList.remove('shuffle');
- this.element.removeAttribute('style');
-
- // Reset individual item styles
- this._disposeItems();
-
- // Null DOM references
- this.items = null;
- this.options.sizer = null;
- this.element = null;
- this._transitions = null;
-
- // Set a flag so if a debounced resize has been triggered,
- // it can first check if it is actually isDestroyed and not doing anything
- this.isDestroyed = true;
- }
-
- /**
- * 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.
- * - 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);
-
- 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 {Array.} elements DOM elements that won't be transitioned.
- * @param {Function} callback A function which will be called while transition
- * is set to 0ms.
- * @private
- */
- static _skipTransitions(elements, callback) {
- let zero = '0ms';
-
- // Save current duration and delay.
- let data = elements.map((element) => {
- let style = element.style;
- let duration = style.transitionDuration;
- let delay = style.transitionDelay;
-
- // Set the duration to zero so it happens immediately
- style.transitionDuration = zero;
- style.transitionDelay = zero;
-
- return {
- duration,
- delay,
- };
- });
-
- callback();
-
- // Cause reflow.
- elements[0].offsetWidth; // jshint ignore:line
-
- // Put the duration back
- elements.forEach((element, i) => {
- element.style.transitionDuration = data[i].duration;
- element.style.transitionDelay = data[i].delay;
- });
- }
-}
-
-Shuffle.ALL_ITEMS = 'all';
-Shuffle.FILTER_ATTRIBUTE_KEY = 'groups';
-
-/**
- * @enum {string}
- */
-Shuffle.EventType = {
- LAYOUT: 'shuffle:layout',
- REMOVED: 'shuffle:removed',
-};
-
-/** @enum {string} */
-Shuffle.ClassName = Classes;
-
-// Overrideable options
-Shuffle.options = {
- // Initial filter group.
- group: Shuffle.ALL_ITEMS,
-
- // Transition/animation speed (milliseconds).
- speed: 250,
-
- // CSS easing function to use.
- easing: 'ease',
-
- // e.g. '.picture-item'.
- itemSelector: '*',
-
- // Element or selector string. Use an element to determine the size of columns
- // and gutters.
- sizer: null,
-
- // A static number or function that tells the plugin how wide the gutters
- // between columns are (in pixels).
- gutterWidth: 0,
-
- // A static number or function that returns a number which tells the plugin
- // how wide the columns are (in pixels).
- columnWidth: 0,
-
- // If your group is not json, and is comma delimeted, you could set delimeter
- // to ','.
- delimeter: null,
-
- // Useful for percentage based heights when they might not always be exactly
- // the same (in pixels).
- buffer: 0,
-
- // Reading the width of elements isn't precise enough and can cause columns to
- // jump between values.
- columnThreshold: 0.01,
-
- // Shuffle can be isInitialized with a sort object. It is the same object
- // given to the sort method.
- initialSort: null,
-
- // By default, shuffle will throttle resize events. This can be changed or
- // removed.
- throttle: throttle,
-
- // How often shuffle can be called on resize (in milliseconds).
- throttleTime: 300,
-
- // Transition delay offset for each item in milliseconds.
- staggerAmount: 15,
-
- // Maximum stagger delay in milliseconds.
- staggerAmountMax: 250,
-
- // 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/shuffle.js b/src/shuffle.js
index e2feeb4..78034e3 100644
--- a/src/shuffle.js
+++ b/src/shuffle.js
@@ -1,1674 +1,1199 @@
-
-// Validate Modernizr exists.
-// Shuffle requires `csstransitions`, `csstransforms`, `csstransforms3d`,
-// and `prefixed` to exist on the Modernizr object.
-if (typeof Modernizr !== 'object') {
- throw new Error('Shuffle.js requires Modernizr.\n' +
- 'http://vestride.github.io/Shuffle/#dependencies');
+'use strict';
+
+import 'custom-event-polyfill';
+import matches from 'matches-selector';
+import arrayUnique from 'array-uniq';
+import xtend from 'xtend';
+import throttle from 'throttleit';
+import Point from './point';
+import ShuffleItem from './shuffle-item';
+import Classes from './classes';
+import getNumberStyle from './get-number-style';
+import sorter from './sorter';
+import { onTransitionEnd, cancelTransitionEnd } from './on-transition-end';
+
+function toArray(arrayLike) {
+ return Array.prototype.slice.call(arrayLike);
}
-
-/**
- * Returns css prefixed properties like `-webkit-transition` or `box-sizing`
- * from `transition` or `boxSizing`, respectively.
- * @param {(string|boolean)} prop Property to be prefixed.
- * @return {string} The prefixed css property.
- */
-function dashify( prop ) {
- if (!prop) {
- return '';
- }
-
- // Replace upper case with dash-lowercase,
- // then fix ms- prefixes because they're not capitalized.
- return prop.replace(/([A-Z])/g, function( str, m1 ) {
- return '-' + m1.toLowerCase();
- }).replace(/^ms-/,'-ms-');
+function arrayMax(array) {
+ return Math.max.apply(Math, array);
}
-// Constant, prefixed variables.
-var TRANSITION = Modernizr.prefixed('transition');
-var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
-var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
-
-// Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
-// says it supports non-prefixed transitions.
-// https://github.com/Modernizr/Modernizr/issues/897
-var TRANSITIONEND = {
- 'WebkitTransition' : 'webkitTransitionEnd',
- 'transition' : 'transitionend'
-}[ TRANSITION ];
-
-var TRANSFORM = Modernizr.prefixed('transform');
-var CSS_TRANSFORM = dashify(TRANSFORM);
-
-// Constants
-var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
-var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
-var HAS_COMPUTED_STYLE = !!window.getComputedStyle;
-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() {
- previous = options.leading === false ? 0 : $.now();
- timeout = null;
- result = func.apply(context, args);
- context = args = null;
- };
- return function() {
- var now = $.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 arrayMin(array) {
+ return Math.min.apply(Math, array);
}
-function each(obj, iterator, context) {
- for (var i = 0, length = obj.length; i < length; i++) {
- if (iterator.call(context, obj[i], i, obj) === {}) {
- return;
- }
+function arrayIncludes(array, obj) {
+ if (arguments.length === 2) {
+ return arrayIncludes(array)(obj);
}
-}
-
-function defer(fn, context, wait) {
- return setTimeout( $.proxy( fn, context ), wait );
-}
-function arrayMax( array ) {
- return Math.max.apply( Math, array );
-}
-
-function arrayMin( array ) {
- return Math.min.apply( Math, array );
-}
-
-
-/**
- * Always returns a numeric value, given a value.
- * @param {*} value Possibly numeric value.
- * @return {number} `value` or zero if `value` isn't numeric.
- * @private
- */
-function getNumber(value) {
- return $.isNumeric(value) ? value : 0;
+ return function (obj) {
+ return array.indexOf(obj) > -1;
+ };
}
-var getStyles = window.getComputedStyle || function() {};
-
-/**
- * Represents a coordinate pair.
- * @param {number} [x=0] X.
- * @param {number} [y=0] Y.
- */
-var Point = function(x, y) {
- this.x = getNumber( x );
- this.y = getNumber( y );
-};
-
+// Used for unique instance variables
+let id = 0;
+
+class Shuffle {
+
+ /**
+ * Categorize, sort, and filter a responsive grid of items.
+ *
+ * @param {Element} element An element which is the parent container for the grid items.
+ * @param {Object} [options=Shuffle.options] Options object.
+ * @constructor
+ */
+ constructor(element, options = {}) {
+ this.options = xtend(Shuffle.options, options);
+
+ this.useSizer = false;
+ this.lastSort = {};
+ this.lastFilter = Shuffle.ALL_ITEMS;
+ this.isEnabled = true;
+ this.isDestroyed = false;
+ this.isInitialized = false;
+ this._transitions = [];
+ this.isTransitioning = false;
+ this._queue = [];
+
+ element = this._getElementOption(element);
+
+ if (!element) {
+ throw new TypeError('Shuffle needs to be initialized with an element.');
+ }
-/**
- * Whether two points are equal.
- * @param {Point} a Point A.
- * @param {Point} b Point B.
- * @return {boolean}
- */
-Point.equals = function(a, b) {
- return a.x === b.x && a.y === b.y;
-};
+ this.element = element;
+ this.id = 'shuffle_' + id++;
-var COMPUTED_SIZE_INCLUDES_PADDING = (function() {
- if (!HAS_COMPUTED_STYLE) {
- return false;
+ this._init();
+ this.isInitialized = true;
}
- var parent = document.body || document.documentElement;
- var e = document.createElement('div');
- e.style.cssText = 'width:10px;padding:2px;' +
- '-webkit-box-sizing:border-box;box-sizing:border-box;';
- parent.appendChild(e);
-
- var width = getStyles(e, null).width;
- var ret = width === '10px';
-
- parent.removeChild(e);
-
- return ret;
-}());
-
-
-// Used for unique instance variables
-var id = 0;
-var $window = $( window );
-
-
-/**
- * Categorize, sort, and filter a responsive grid of items.
- *
- * @param {Element} element An element which is the parent container for the grid items.
- * @param {Object} [options=Shuffle.options] Options object.
- * @constructor
- */
-var Shuffle = function( element, options ) {
- options = options || {};
- $.extend( this, Shuffle.options, options, Shuffle.settings );
-
- this.$el = $(element);
- this.element = element;
- this.unique = 'shuffle_' + id++;
-
- this._fire( Shuffle.EventType.LOADING );
- this._init();
-
- // Dispatch the done event asynchronously so that people can bind to it after
- // Shuffle has been initialized.
- defer(function() {
- this.initialized = true;
- this._fire( Shuffle.EventType.DONE );
- }, this, 16);
-};
+ _init() {
+ this.items = this._getItems();
+ this.options.sizer = this._getElementOption(this.options.sizer);
-/**
- * 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'
-};
+ if (this.options.sizer) {
+ this.useSizer = true;
+ }
+ // Add class and invalidate styles
+ this.element.classList.add(Shuffle.ClassName.BASE);
-/** @enum {string} */
-Shuffle.ClassName = {
- BASE: SHUFFLE,
- SHUFFLE_ITEM: 'shuffle-item',
- FILTERED: 'filtered',
- CONCEALED: 'concealed'
-};
+ // Set initial css for each item
+ this._initItems();
+ // Bind resize events
+ this._onResize = this._getResizeFunction();
+ window.addEventListener('resize', this._onResize);
-// 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: HAS_COMPUTED_STYLE ? 0.01 : 0.1, // 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: CAN_TRANSITION_TRANSFORMS // Whether to use transforms or absolute positioning.
-};
+ // Get container css all in one request. Causes reflow
+ var containerCss = window.getComputedStyle(this.element, null);
+ var containerWidth = Shuffle.getSize(this.element).width;
+ // Add styles to the container if it doesn't have them.
+ this._validateStyles(containerCss);
-// Not overrideable
-Shuffle.settings = {
- useSizer: false,
- itemCss : { // default CSS for each item
- position: 'absolute',
- top: 0,
- left: 0,
- visibility: 'visible'
- },
- revealAppendedDelay: 300,
- lastSort: {},
- lastFilter: ALL_ITEMS,
- enabled: true,
- destroyed: false,
- initialized: false,
- _animations: [],
- _transitions: [],
- _isMovementCanceled: false,
- styleQueue: []
-};
+ // We already got the container's width above, no need to cause another
+ // reflow getting it again... Calculate the number of columns there will be
+ this._setColumns(containerWidth);
+ // Kick off!
+ this.filter(this.options.group, this.options.initialSort);
-// Expose for testing.
-Shuffle.Point = Point;
-
-
-/**
- * Static methods.
- */
-
-/**
- * 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) {
- if ( HAS_TRANSFORMS_3D ) {
- return 'translate3d(' + point.x + 'px, ' + point.y + 'px, 0) scale3d(' + scale + ', ' + scale + ', 1)';
- } else {
- return 'translate(' + point.x + 'px, ' + point.y + 'px) scale(' + scale + ')';
+ // 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.
+ // First, however, a synchronous layout must be caused for the previous
+ // styles to be applied without transitions.
+ this.element.offsetWidth; // jshint ignore: line
+ this._setTransitions();
+ this.element.style.transition = 'height ' + this.options.speed + 'ms ' + this.options.easing;
}
-};
+ /**
+ * Returns a throttled and proxied function for the resize handler.
+ * @return {Function}
+ * @private
+ */
+ _getResizeFunction() {
+ var resizeFunction = this._handleResize.bind(this);
+ return this.options.throttle ?
+ this.options.throttle(resizeFunction, this.options.throttleTime) :
+ resizeFunction;
+ }
-/**
- * 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 ) {
- if ( HAS_COMPUTED_STYLE ) {
- 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 );
+ /**
+ * Retrieve an element from an option.
+ * @param {string|jQuery|Element} option The option to check.
+ * @return {?Element} The plain element or null.
+ * @private
+ */
+ _getElementOption(option) {
+ // If column width is a string, treat is as a selector and search for the
+ // sizer element within the outermost container
+ if (typeof option === 'string') {
+ return this.element.querySelector(option);
+
+ // Check for an element
+ } else if (option && option.nodeType && option.nodeType === 1) {
+ return option;
+
+ // Check for jQuery object
+ } else if (option && option.jquery) {
+ return option[0];
}
- return value;
- } else {
- return Shuffle._getFloat( $( element ).css( style ) );
+ return null;
}
-};
-
-
-/**
- * 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 ) );
-};
+ /**
+ * Ensures the shuffle container has the css styles it needs applied to it.
+ * @param {Object} styles Key value pairs for position and overflow.
+ * @private
+ */
+ _validateStyles(styles) {
+ // Position cannot be static.
+ if (styles.position === 'static') {
+ this.element.style.position = 'relative';
+ }
-/**
- * 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;
+ // Overflow has to be hidden
+ if (styles.overflow !== 'hidden') {
+ this.element.style.overflow = 'hidden';
+ }
}
- return width;
-};
-
+ /**
+ * Filter the elements by a category.
+ * @param {string} [category] Category to filter by. If it's given, the last
+ * category will be used to filter the items.
+ * @param {Array} [collection] Optionally filter a collection. Defaults to
+ * all the items.
+ * @return {!{filtered: Array, concealed: Array}}
+ * @private
+ */
+ _filter(category = this.lastFilter, collection = this.items) {
+ var set = this._getFilteredSets(category, collection);
+
+ // Individually add/remove concealed/filtered classes
+ this._toggleFilterClasses(set);
+
+ // Save the last filter in case elements are appended.
+ this.lastFilter = category;
+
+ // This is saved mainly because providing a filter function (like searching)
+ // will overwrite the `lastFilter` property every time its called.
+ if (typeof category === 'string') {
+ this.options.group = category;
+ }
-/**
- * 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;
+ return set;
}
- return 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.
- * @param {Object} [context] Optional context for the callback function.
- * @private
- */
-Shuffle._skipTransition = function( element, callback, context ) {
- var duration = element.style[ TRANSITION_DURATION ];
-
- // Set the duration to zero so it happens immediately
- element.style[ TRANSITION_DURATION ] = '0ms'; // ms needed for firefox!
-
- callback.call( context );
-
- // Force reflow
- var reflow = element.offsetWidth;
- // Avoid jshint warnings: unused variables and expressions.
- reflow = null;
-
- // Put the duration back
- element.style[ TRANSITION_DURATION ] = duration;
-};
-
-
-/**
- * Instance methods.
- */
-
-Shuffle.prototype._init = function() {
- this.$items = this._getItems();
-
- this.sizer = this._getElementOption( this.sizer );
+ /**
+ * Returns an object containing the filtered and concealed elements.
+ * @param {string|Function} category Category or function to filter by.
+ * @param {Array.} items A collection of items to filter.
+ * @return {!{filtered: Array, concealed: Array}}
+ * @private
+ */
+ _getFilteredSets(category, items) {
+ let filtered = [];
+ let concealed = [];
+
+ // category === 'all', add filtered class to everything
+ if (category === Shuffle.ALL_ITEMS) {
+ filtered = items;
+
+ // Loop through each item and use provided function to determine
+ // whether to hide it or not.
+ } else {
+ items.forEach((item) => {
+ if (this._doesPassFilter(category, item.element)) {
+ filtered.push(item);
+ } else {
+ concealed.push(item);
+ }
+ });
+ }
- if ( this.sizer ) {
- this.useSizer = true;
+ return {
+ filtered,
+ concealed,
+ };
}
- // Add class and invalidate styles
- this.$el.addClass( Shuffle.ClassName.BASE );
-
- // Set initial css for each item
- this._initItems();
-
- // Bind resize events
- // http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
- $window.on('resize.' + SHUFFLE + '.' + this.unique, this._getResizeFunction());
+ /**
+ * Test an item to see if it passes a category.
+ * @param {string|Function} category Category or function to filter by.
+ * @param {Element} element An element to test.
+ * @return {boolean} Whether it passes the category/filter.
+ * @private
+ */
+ _doesPassFilter(category, element) {
- // Get container css all in one request. Causes reflow
- var containerCSS = this.$el.css(['position', 'overflow']);
- var containerWidth = Shuffle._getOuterWidth( this.element );
+ if (typeof category === 'function') {
+ return category.call(element, element, this);
- // Add styles to the container if it doesn't have them.
- this._validateStyles( containerCSS );
-
- // We already got the container's width above, no need to cause another reflow getting it again...
- // Calculate the number of columns there will be
- this._setColumns( containerWidth );
-
- // Kick off!
- this.shuffle( this.group, this.initialSort );
+ // Check each element's data-groups attribute against the given category.
+ } else {
+ let attr = element.getAttribute('data-' + Shuffle.FILTER_ATTRIBUTE_KEY);
+ let groups = JSON.parse(attr);
+ let keys = this.delimeter && !Array.isArray(groups) ?
+ groups.split(this.delimeter) :
+ groups;
+
+ if (Array.isArray(category)) {
+ return category.some(arrayIncludes(keys));
+ }
- // 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);
+ return arrayIncludes(keys, category);
+ }
}
-};
-
-
-/**
- * Returns a throttled and proxied function for the resize handler.
- * @return {Function}
- * @private
- */
-Shuffle.prototype._getResizeFunction = function() {
- var resizeFunction = $.proxy( this._onResize, this );
- return this.throttle ?
- this.throttle( resizeFunction, this.throttleTime ) :
- resizeFunction;
-};
+ /**
+ * Toggles the filtered and concealed class names.
+ * @param {{filtered, concealed}} Object with filtered and concealed arrays.
+ * @private
+ */
+ _toggleFilterClasses({ filtered, concealed }) {
+ filtered.forEach((item) => {
+ item.reveal();
+ });
-/**
- * Retrieve an element from an option.
- * @param {string|jQuery|Element} option The option to check.
- * @return {?Element} The plain element or null.
- * @private
- */
-Shuffle.prototype._getElementOption = function( option ) {
- // If column width is a string, treat is as a selector and search for the
- // sizer element within the outermost container
- if ( typeof option === 'string' ) {
- return this.$el.find( option )[0] || null;
-
- // Check for an element
- } else if ( option && option.nodeType && option.nodeType === 1 ) {
- return option;
-
- // Check for jQuery object
- } else if ( option && option.jquery ) {
- return option[0];
+ concealed.forEach((item) => {
+ item.conceal();
+ });
}
- return null;
-};
-
-
-/**
- * Ensures the shuffle container has the css styles it needs applied to it.
- * @param {Object} styles Key value pairs for position and overflow.
- * @private
- */
-Shuffle.prototype._validateStyles = function(styles) {
- // Position cannot be static.
- if ( styles.position === 'static' ) {
- this.element.style.position = 'relative';
+ /**
+ * Set the initial css for each item
+ * @param {Array.} [items] Optionally specifiy at set to initialize.
+ * @private
+ */
+ _initItems(items = this.items) {
+ items.forEach((item) => {
+ item.init();
+ });
}
- // Overflow has to be hidden
- if ( styles.overflow !== 'hidden' ) {
- this.element.style.overflow = 'hidden';
+ /**
+ * Remove element reference and styles.
+ * @private
+ */
+ _disposeItems(items = this.items) {
+ items.forEach((item) => {
+ item.dispose();
+ });
}
-};
-
-/**
- * Filter the elements by a category.
- * @param {string} [category] Category to filter by. If it's given, the last
- * category will be used to filter the items.
- * @param {ArrayLike} [$collection] Optionally filter a collection. Defaults to
- * all the items.
- * @return {jQuery} Filtered items.
- * @private
- */
-Shuffle.prototype._filter = function( category, $collection ) {
- category = category || this.lastFilter;
- $collection = $collection || this.$items;
-
- var set = this._getFilteredSets( category, $collection );
-
- // Individually add/remove concealed/filtered classes
- this._toggleFilterClasses( set.filtered, set.concealed );
-
- // Save the last filter in case elements are appended.
- this.lastFilter = category;
-
- // This is saved mainly because providing a filter function (like searching)
- // will overwrite the `lastFilter` property every time its called.
- if ( typeof category === 'string' ) {
- this.group = category;
+ /**
+ * Updates the filtered item count.
+ * @private
+ */
+ _updateItemCount() {
+ this.visibleItems = this._getFilteredItems().length;
}
- return set.filtered;
-};
-
+ /**
+ * Sets css transform transition on a group of elements. This is not executed
+ * at the same time as `item.init` so that transitions don't occur upon
+ * initialization of Shuffle.
+ * @param {Array.} items Shuffle items to set transitions on.
+ * @private
+ */
+ _setTransitions(items = this.items) {
+ let speed = this.options.speed;
+ let easing = this.options.easing;
+
+ var str;
+ if (this.options.useTransforms) {
+ str = 'transform ' + speed + 'ms ' + easing +
+ ', opacity ' + speed + 'ms ' + easing;
+ } else {
+ str = 'top ' + speed + 'ms ' + easing +
+ ', left ' + speed + 'ms ' + easing +
+ ', opacity ' + speed + 'ms ' + easing;
+ }
-/**
- * Returns an object containing the filtered and concealed elements.
- * @param {string|Function} category Category or function to filter by.
- * @param {ArrayLike.} $items A collection of items to filter.
- * @return {!{filtered: jQuery, concealed: jQuery}}
- * @private
- */
-Shuffle.prototype._getFilteredSets = function( category, $items ) {
- var $filtered = $();
- var $concealed = $();
-
- // category === 'all', add filtered class to everything
- if ( category === ALL_ITEMS ) {
- $filtered = $items;
-
- // Loop through each item and use provided function to determine
- // whether to hide it or not.
- } else {
- each($items, function( el ) {
- var $item = $(el);
- if ( this._doesPassFilter( category, $item ) ) {
- $filtered = $filtered.add( $item );
- } else {
- $concealed = $concealed.add( $item );
- }
- }, this);
+ items.forEach((item) => {
+ item.element.style.transition = str;
+ });
}
- return {
- filtered: $filtered,
- concealed: $concealed
- };
-};
-
-
-/**
- * Test an item to see if it passes a category.
- * @param {string|Function} category Category or function to filter by.
- * @param {jQuery} $item A single item, wrapped with jQuery.
- * @return {boolean} Whether it passes the category/filter.
- * @private
- */
-Shuffle.prototype._doesPassFilter = function( category, $item ) {
- if ( $.isFunction( category ) ) {
- return category.call( $item[0], $item, this );
-
- // Check each element's data-groups attribute against the given category.
- } else {
- var groups = $item.data( FILTER_ATTRIBUTE_KEY );
- var keys = this.delimeter && !$.isArray( groups ) ?
- groups.split( this.delimeter ) :
- groups;
- var categories = [];
- categories = categories.concat(category);
- for (var i = 0; i < categories.length; i++) {
- var categoryIsInKeys = $.inArray(categories[i], keys) > -1;
- if(!categoryIsInKeys) {
- return false;
- }
- }
- return true;
+ _getItems() {
+ return toArray(this.element.children)
+ .filter(el => matches(el, this.options.itemSelector))
+ .map(el => new ShuffleItem(el));
}
-};
-
-
-/**
- * Toggles the filtered and concealed class names.
- * @param {jQuery} $filtered Filtered set.
- * @param {jQuery} $concealed Concealed set.
- * @private
- */
-Shuffle.prototype._toggleFilterClasses = function( $filtered, $concealed ) {
- $filtered
- .removeClass( Shuffle.ClassName.CONCEALED )
- .addClass( Shuffle.ClassName.FILTERED );
- $concealed
- .removeClass( Shuffle.ClassName.FILTERED )
- .addClass( Shuffle.ClassName.CONCEALED );
-};
-
-
-/**
- * Set the initial css for each item
- * @param {jQuery} [$items] Optionally specifiy at set to initialize
- */
-Shuffle.prototype._initItems = function( $items ) {
- $items = $items || this.$items;
- $items.addClass([
- Shuffle.ClassName.SHUFFLE_ITEM,
- Shuffle.ClassName.FILTERED
- ].join(' '));
- $items.css( this.itemCss ).data('point', new Point()).data('scale', DEFAULT_SCALE);
-};
-
-
-/**
- * Updates the filtered item count.
- * @private
- */
-Shuffle.prototype._updateItemCount = function() {
- this.visibleItems = this._getFilteredItems().length;
-};
-
-
-/**
- * Sets css transform transition on a an element.
- * @param {Element} element Element to set transition on.
- * @private
- */
-Shuffle.prototype._setTransition = function( element ) {
- element.style[ TRANSITION ] = CSS_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
- */
-Shuffle.prototype._setTransitions = function( $items ) {
- $items = $items || this.$items;
- each($items, function( el ) {
- this._setTransition( el );
- }, this);
-};
-
-
-/**
- * Sets a transition delay on a collection of elements, making each delay
- * greater than the last.
- * @param {ArrayLike.} $collection Array to iterate over.
- */
-Shuffle.prototype._setSequentialDelay = function( $collection ) {
- if ( !this.supported ) {
- return;
+ /**
+ * When new elements are added to the shuffle container, update the array of
+ * items because that is the order `_layout` calls them.
+ */
+ _updateItemsOrder() {
+ let children = this.element.children;
+ this.items = sorter(this.items, {
+ by(element) {
+ return Array.prototype.indexOf.call(children, element);
+ },
+ });
}
- // $collection can be an array of dom elements or jquery object
- each($collection, function( el, i ) {
- // This works because the transition-property: transform, opacity;
- el.style[ TRANSITION_DELAY ] = '0ms,' + ((i + 1) * this.sequentialFadeDelay) + 'ms';
- }, this);
-};
-
-
-Shuffle.prototype._getItems = function() {
- return this.$el.children( this.itemSelector );
-};
-
-
-Shuffle.prototype._getFilteredItems = function() {
- return this.$items.filter('.' + Shuffle.ClassName.FILTERED);
-};
-
+ _getFilteredItems() {
+ return this.items.filter(item => item.isVisible);
+ }
-Shuffle.prototype._getConcealedItems = function() {
- return this.$items.filter('.' + Shuffle.ClassName.CONCEALED);
-};
+ _getConcealedItems() {
+ return this.items.filter(item => !item.isVisible);
+ }
+ /**
+ * Returns the column size, based on column width and sizer options.
+ * @param {number} containerWidth Size of the parent container.
+ * @param {number} gutterSize Size of the gutters.
+ * @return {number}
+ * @private
+ */
+ _getColumnSize(containerWidth, gutterSize) {
+ var size;
+
+ // If the columnWidth property is a function, then the grid is fluid
+ if (typeof this.options.columnWidth === 'function') {
+ size = this.options.columnWidth(containerWidth);
+
+ // columnWidth option isn't a function, are they using a sizing element?
+ } else if (this.useSizer) {
+ size = Shuffle.getSize(this.options.sizer).width;
+
+ // if not, how about the explicitly set option?
+ } else if (this.options.columnWidth) {
+ size = this.options.columnWidth;
+
+ // or use the size of the first item
+ } else if (this.items.length > 0) {
+ size = Shuffle.getSize(this.items[0].element, true).width;
+
+ // if there's no items, use size of container
+ } else {
+ size = containerWidth;
+ }
-/**
- * Returns the column size, based on column width and sizer options.
- * @param {number} containerWidth Size of the parent container.
- * @param {number} gutterSize Size of the gutters.
- * @return {number}
- * @private
- */
-Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
- var size;
+ // Don't let them set a column width of zero.
+ if (size === 0) {
+ size = containerWidth;
+ }
- // If the columnWidth property is a function, then the grid is fluid
- if ( $.isFunction( this.columnWidth ) ) {
- size = this.columnWidth(containerWidth);
+ return size + gutterSize;
+ }
- // columnWidth option isn't a function, are they using a sizing element?
- } else if ( this.useSizer ) {
- size = Shuffle._getOuterWidth(this.sizer);
+ /**
+ * Returns the gutter size, based on gutter width and sizer options.
+ * @param {number} containerWidth Size of the parent container.
+ * @return {number}
+ * @private
+ */
+ _getGutterSize(containerWidth) {
+ var size;
+ if (typeof this.options.gutterWidth === 'function') {
+ size = this.options.gutterWidth(containerWidth);
+ } else if (this.useSizer) {
+ size = getNumberStyle(this.options.sizer, 'marginLeft');
+ } else {
+ size = this.options.gutterWidth;
+ }
- // if not, how about the explicitly set option?
- } else if ( this.columnWidth ) {
- size = this.columnWidth;
+ return size;
+ }
- // or use the size of the first item
- } else if ( this.$items.length > 0 ) {
- size = Shuffle._getOuterWidth(this.$items[0], true);
+ /**
+ * Calculate the number of columns to be used. Gets css if using sizer element.
+ * @param {number} [containerWidth] Optionally specify a container width if
+ * it's already available.
+ */
+ _setColumns(containerWidth = Shuffle.getSize(this.element).width) {
+ var gutter = this._getGutterSize(containerWidth);
+ var columnWidth = this._getColumnSize(containerWidth, gutter);
+ var calculatedColumns = (containerWidth + gutter) / columnWidth;
+
+ // Widths given from getStyles are not precise enough...
+ if (Math.abs(Math.round(calculatedColumns) - calculatedColumns) <
+ this.options.columnThreshold) {
+ // e.g. calculatedColumns = 11.998876
+ calculatedColumns = Math.round(calculatedColumns);
+ }
- // if there's no items, use size of container
- } else {
- size = containerWidth;
+ this.cols = Math.max(Math.floor(calculatedColumns), 1);
+ this.containerWidth = containerWidth;
+ this.colWidth = columnWidth;
}
- // Don't let them set a column width of zero.
- if ( size === 0 ) {
- size = containerWidth;
+ /**
+ * Adjust the height of the grid
+ */
+ _setContainerSize() {
+ this.element.style.height = this._getContainerSize() + 'px';
}
- return size + gutterSize;
-};
-
-
-/**
- * Returns the gutter size, based on gutter width and sizer options.
- * @param {number} containerWidth Size of the parent container.
- * @return {number}
- * @private
- */
-Shuffle.prototype._getGutterSize = function( containerWidth ) {
- var size;
- if ( $.isFunction( this.gutterWidth ) ) {
- size = this.gutterWidth(containerWidth);
- } else if ( this.useSizer ) {
- size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
- } else {
- size = this.gutterWidth;
+ /**
+ * Based on the column heights, it returns the biggest one.
+ * @return {number}
+ * @private
+ */
+ _getContainerSize() {
+ return arrayMax(this.positions);
}
- return size;
-};
-
-
-/**
- * Calculate the number of columns to be used. Gets css if using sizer element.
- * @param {number} [theContainerWidth] Optionally specify a container width if it's already available.
- */
-Shuffle.prototype._setColumns = function( theContainerWidth ) {
- var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
- var gutter = this._getGutterSize( containerWidth );
- var columnWidth = this._getColumnSize( containerWidth, gutter );
- var calculatedColumns = (containerWidth + gutter) / columnWidth;
-
- // Widths given from getStyles are not precise enough...
- if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < this.columnThreshold ) {
- // e.g. calculatedColumns = 11.998876
- calculatedColumns = Math.round( calculatedColumns );
+ /**
+ * Get the clamped stagger amount.
+ * @param {number} index Index of the item to be staggered.
+ * @return {number}
+ */
+ _getStaggerAmount(index) {
+ return Math.min(index * this.options.staggerAmount, this.options.staggerAmountMax);
}
- this.cols = Math.max( Math.floor(calculatedColumns), 1 );
- this.containerWidth = containerWidth;
- this.colWidth = columnWidth;
-};
-
-/**
- * Adjust the height of the grid
- */
-Shuffle.prototype._setContainerSize = function() {
- this.$el.css( 'height', this._getContainerSize() );
-};
+ /**
+ * @return {boolean} Whether the event was prevented or not.
+ */
+ _dispatch(name, details = {}) {
+ if (this.isDestroyed) {
+ return;
+ }
+ details.shuffle = this;
+ return !this.element.dispatchEvent(new CustomEvent(name, {
+ bubbles: true,
+ cancelable: false,
+ detail: details,
+ }));
+ }
-/**
- * Based on the column heights, it returns the biggest one.
- * @return {number}
- * @private
- */
-Shuffle.prototype._getContainerSize = function() {
- return arrayMax( this.positions );
-};
+ /**
+ * Zeros out the y columns array, which is used to determine item placement.
+ * @private
+ */
+ _resetCols() {
+ var i = this.cols;
+ this.positions = [];
+ while (i--) {
+ this.positions.push(0);
+ }
+ }
+ /**
+ * Loops through each item that should be shown and calculates the x, y position.
+ * @param {Array.} items Array of items that will be shown/layed
+ * out in order in their array.
+ */
+ _layout(items) {
+ let count = 0;
+ items.forEach((item) => {
+ var currPos = item.point;
+ var currScale = item.scale;
+ 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 === ShuffleItem.Scale.VISIBLE) {
+ return;
+ }
-/**
- * Fire events with .shuffle namespace
- */
-Shuffle.prototype._fire = function( name, args ) {
- this.$el.trigger( name + '.' + SHUFFLE, args && args.length ? args : [ this ] );
-};
+ item.point = pos;
+ item.scale = ShuffleItem.Scale.VISIBLE;
+ this._queue.push({
+ item,
+ opacity: 1,
+ visibility: 'visible',
+ transitionDelay: this._getStaggerAmount(count),
+ });
-/**
- * Zeros out the y columns array, which is used to determine item placement.
- * @private
- */
-Shuffle.prototype._resetCols = function() {
- var i = this.cols;
- this.positions = [];
- while (i--) {
- this.positions.push( 0 );
+ count++;
+ });
}
-};
+ /**
+ * Determine the location of the next item, based on its size.
+ * @param {{width: number, height: number}} itemSize Object with width and height.
+ * @return {Point}
+ * @private
+ */
+ _getItemPosition(itemSize) {
+ var columnSpan = this._getColumnSpan(itemSize.width, this.colWidth, this.cols);
+
+ var setY = this._getColumnSet(columnSpan, this.cols);
+
+ // Finds the index of the smallest number in the set.
+ var shortColumnIndex = this._getShortColumn(setY, this.options.buffer);
+
+ // Position the item
+ var point = new Point(
+ Math.round(this.colWidth * shortColumnIndex),
+ Math.round(setY[shortColumnIndex]));
+
+ // Update the columns array with the new values for each column.
+ // e.g. before the update the columns could be [250, 0, 0, 0] for an item
+ // which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
+ var setHeight = setY[shortColumnIndex] + itemSize.height;
+ var setSpan = this.cols + 1 - setY.length;
+ for (var i = 0; i < setSpan; i++) {
+ this.positions[shortColumnIndex + i] = setHeight;
+ }
-/**
- * Loops through each item that should be shown and calculates the x, y position.
- * @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.
- */
-Shuffle.prototype._layout = function( items, isOnlyPosition ) {
- each(items, function( item ) {
- this._layoutItem( item, !!isOnlyPosition );
- }, this);
+ return point;
+ }
- // `_layout` always happens after `_shrink`, so it's safe to process the style
- // queue here with styles from the shrink method.
- this._processStyleQueue();
+ /**
+ * Determine the number of columns an items spans.
+ * @param {number} itemWidth Width of the item.
+ * @param {number} columnWidth Width of the column (includes gutter).
+ * @param {number} columns Total number of columns
+ * @return {number}
+ * @private
+ */
+ _getColumnSpan(itemWidth, columnWidth, columns) {
+ var columnSpan = itemWidth / columnWidth;
+
+ // If the difference between the rounded column span number and the
+ // calculated column span number is really small, round the number to
+ // make it fit.
+ if (Math.abs(Math.round(columnSpan) - columnSpan) < this.options.columnThreshold) {
+ // e.g. columnSpan = 4.0089945390298745
+ columnSpan = Math.round(columnSpan);
+ }
- // Adjust the height of the container.
- this._setContainerSize();
-};
+ // Ensure the column span is not more than the amount of columns in the whole layout.
+ return Math.min(Math.ceil(columnSpan), columns);
+ }
+ /**
+ * Retrieves the column set to use for placement.
+ * @param {number} columnSpan The number of columns this current item spans.
+ * @param {number} columns The total columns in the grid.
+ * @return {Array.} An array of numbers represeting the column set.
+ * @private
+ */
+ _getColumnSet(columnSpan, columns) {
+ // The item spans only one column.
+ if (columnSpan === 1) {
+ return this.positions;
+
+ // The item spans more than one column, figure out how many different
+ // places it could fit horizontally.
+ // The group count is the number of places within the positions this block
+ // could fit, ignoring the current positions of items.
+ // Imagine a 2 column brick as the second item in a 4 column grid with
+ // 10px height each. Find the places it would fit:
+ // [10, 0, 0, 0]
+ // | | |
+ // * * *
+ //
+ // Then take the places which fit and get the bigger of the two:
+ // max([10, 0]), max([0, 0]), max([0, 0]) = [10, 0, 0]
+ //
+ // Next, find the first smallest number (the short column).
+ // [10, 0, 0]
+ // |
+ // *
+ //
+ // And that's where it should be placed!
+ } else {
+ var groupCount = columns + 1 - columnSpan;
+ var groupY = [];
-/**
- * Calculates the position of the item and pushes it onto the style queue.
- * @param {Element} item Element which is being positioned.
- * @param {boolean} isOnlyPosition Whether to position the item, but with zero
- * opacity so that it can fade in later.
- * @private
- */
-Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
- var $item = $(item);
- var itemData = $item.data();
- var currPos = itemData.point;
- var currScale = itemData.scale;
- var itemSize = {
- width: Shuffle._getOuterWidth( item, true ),
- height: Shuffle._getOuterHeight( item, true )
- };
- var pos = this._getItemPosition( itemSize );
+ // For how many possible positions for this item there are.
+ for (var i = 0; i < groupCount; i++) {
+ // Find the bigger value for each place it could fit.
+ groupY[i] = arrayMax(this.positions.slice(i, i + columnSpan));
+ }
- // 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 ) {
- return;
+ return groupY;
+ }
}
- // Save data for shrink
- itemData.point = pos;
- itemData.scale = DEFAULT_SCALE;
-
- this.styleQueue.push({
- $item: $item,
- 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.css( 'visibility', 'visible' );
- }
- },
- callback: function() {
- if ( isOnlyPosition ) {
- $item.css( 'visibility', 'hidden' );
+ /**
+ * Find index of short column, the first from the left where this item will go.
+ *
+ * @param {Array.} positions The array to search for the smallest number.
+ * @param {number} buffer Optional buffer which is very useful when the height
+ * is a percentage of the width.
+ * @return {number} Index of the short column.
+ * @private
+ */
+ _getShortColumn(positions, buffer) {
+ var minPosition = arrayMin(positions);
+ for (var i = 0, len = positions.length; i < len; i++) {
+ if (positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer) {
+ return i;
}
}
- });
-};
-
-/**
- * Determine the location of the next item, based on its size.
- * @param {{width: number, height: number}} itemSize Object with width and height.
- * @return {Point}
- * @private
- */
-Shuffle.prototype._getItemPosition = function( itemSize ) {
- var columnSpan = this._getColumnSpan( itemSize.width, this.colWidth, this.cols );
-
- var setY = this._getColumnSet( columnSpan, this.cols );
-
- // Finds the index of the smallest number in the set.
- var shortColumnIndex = this._getShortColumn( setY, this.buffer );
-
- // Position the item
- var point = new Point(
- Math.round( this.colWidth * shortColumnIndex ),
- Math.round( setY[shortColumnIndex] ));
-
- // Update the columns array with the new values for each column.
- // e.g. before the update the columns could be [250, 0, 0, 0] for an item
- // which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
- var setHeight = setY[shortColumnIndex] + itemSize.height;
- var setSpan = this.cols + 1 - setY.length;
- for ( var i = 0; i < setSpan; i++ ) {
- this.positions[ shortColumnIndex + i ] = setHeight;
+ return 0;
}
- return point;
-};
+ /**
+ * Hides the elements that don't match our filter.
+ * @param {Array.} collection Collection to shrink.
+ * @private
+ */
+ _shrink(collection = this._getConcealedItems()) {
+ let count = 0;
+ collection.forEach((item) => {
+ // 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 === ShuffleItem.Scale.FILTERED) {
+ return;
+ }
+ item.scale = ShuffleItem.Scale.FILTERED;
-/**
- * Determine the number of columns an items spans.
- * @param {number} itemWidth Width of the item.
- * @param {number} columnWidth Width of the column (includes gutter).
- * @param {number} columns Total number of columns
- * @return {number}
- * @private
- */
-Shuffle.prototype._getColumnSpan = function( itemWidth, columnWidth, columns ) {
- var columnSpan = itemWidth / columnWidth;
-
- // If the difference between the rounded column span number and the
- // calculated column span number is really small, round the number to
- // make it fit.
- if ( Math.abs(Math.round( columnSpan ) - columnSpan ) < this.columnThreshold ) {
- // e.g. columnSpan = 4.0089945390298745
- columnSpan = Math.round( columnSpan );
+ this._queue.push({
+ item,
+ opacity: 0,
+ transitionDelay: this._getStaggerAmount(count),
+ callback() {
+ item.element.style.visibility = 'hidden';
+ },
+ });
+
+ count++;
+ });
}
- // Ensure the column span is not more than the amount of columns in the whole layout.
- return Math.min( Math.ceil( columnSpan ), columns );
-};
+ /**
+ * Resize handler.
+ * @private
+ */
+ _handleResize() {
+ // If shuffle is disabled, destroyed, don't do anything
+ if (!this.isEnabled || this.isDestroyed) {
+ return;
+ }
+ // Will need to check height in the future if it's layed out horizontaly
+ var containerWidth = Shuffle.getSize(this.element).width;
-/**
- * Retrieves the column set to use for placement.
- * @param {number} columnSpan The number of columns this current item spans.
- * @param {number} columns The total columns in the grid.
- * @return {Array.} An array of numbers represeting the column set.
- * @private
- */
-Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
- // The item spans only one column.
- if ( columnSpan === 1 ) {
- return this.positions;
-
- // The item spans more than one column, figure out how many different
- // places it could fit horizontally.
- // The group count is the number of places within the positions this block
- // could fit, ignoring the current positions of items.
- // Imagine a 2 column brick as the second item in a 4 column grid with
- // 10px height each. Find the places it would fit:
- // [10, 0, 0, 0]
- // | | |
- // * * *
- //
- // Then take the places which fit and get the bigger of the two:
- // max([10, 0]), max([0, 0]), max([0, 0]) = [10, 0, 0]
- //
- // Next, find the first smallest number (the short column).
- // [10, 0, 0]
- // |
- // *
- //
- // And that's where it should be placed!
- } else {
- var groupCount = columns + 1 - columnSpan;
- var groupY = [];
-
- // For how many possible positions for this item there are.
- for ( var i = 0; i < groupCount; i++ ) {
- // Find the bigger value for each place it could fit.
- groupY[i] = arrayMax( this.positions.slice( i, i + columnSpan ) );
+ // containerWidth hasn't changed, don't do anything
+ if (containerWidth === this.containerWidth) {
+ return;
}
- return groupY;
+ this.update();
}
-};
-
-/**
- * Find index of short column, the first from the left where this item will go.
- *
- * @param {Array.} positions The array to search for the smallest number.
- * @param {number} buffer Optional buffer which is very useful when the height
- * is a percentage of the width.
- * @return {number} Index of the short column.
- * @private
- */
-Shuffle.prototype._getShortColumn = function( positions, buffer ) {
- var minPosition = arrayMin( positions );
- for (var i = 0, len = positions.length; i < len; i++) {
- if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
- return i;
+ /**
+ * Returns styles which will be applied to the an item for a transition.
+ * @param {Object} obj Transition options.
+ * @return {!Object} Transforms for transitions, left/top for animate.
+ * @private
+ */
+ _getStylesForTransition(obj) {
+ let item = obj.item;
+ let styles = {
+ opacity: obj.opacity,
+ visibility: obj.visibility,
+ transitionDelay: (obj.transitionDelay || 0) + 'ms',
+ };
+
+ let x = item.point.x;
+ let y = item.point.y;
+
+ if (this.options.useTransforms) {
+ styles.transform = `translate(${x}px, ${y}px) scale(${item.scale})`;
+ } else {
+ styles.left = x + 'px';
+ styles.top = y + 'px';
}
+
+ return styles;
}
- return 0;
-};
+ _whenTransitionDone(element, itemCallback) {
+ // TODO what happens when the transition is canceled and the promise never resolves?
+ return new Promise((resolve) => {
+ let id = onTransitionEnd(element, (evt) => {
+ evt.currentTarget.style.transitionDelay = '';
-/**
- * Hides the elements that don't match our filter.
- * @param {jQuery} $collection jQuery collection to shrink.
- * @private
- */
-Shuffle.prototype._shrink = function( $collection ) {
- var $concealed = $collection || this._getConcealedItems();
+ if (itemCallback) {
+ itemCallback();
+ }
- each($concealed, function( item ) {
- var $item = $(item);
- var itemData = $item.data();
+ resolve();
+ });
+ this._transitions.push(id);
+ });
+ }
- // 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 ( itemData.scale === CONCEALED_SCALE ) {
- return;
- }
+ _transition(opts) {
+ opts.item.applyCss(this._getStylesForTransition(opts));
+ return this._whenTransitionDone(opts.item.element, opts.callback);
+ }
- itemData.scale = CONCEALED_SCALE;
+ /**
+ * Execute the styles gathered in the style queue. This applies styles to elements,
+ * triggering transitions.
+ * @private
+ */
+ _processQueue() {
+ if (this.isTransitioning) {
+ this._cancelMovement();
+ }
- this.styleQueue.push({
- $item: $item,
- point: itemData.point,
- scale : CONCEALED_SCALE,
- opacity: 0,
- callback: function() {
- $item.css( 'visibility', 'hidden' );
+ // Iterate over the queue and keep track of ones that use transitions.
+ let immediates = [];
+ let transitions = [];
+ this._queue.forEach((obj) => {
+ if (!this.isInitialized || this.options.speed === 0) {
+ immediates.push(obj);
+ } else {
+ transitions.push(obj);
}
});
- }, this);
-};
+ this._styleImmediately(immediates);
-/**
- * Resize handler.
- * @private
- */
-Shuffle.prototype._onResize = function() {
- // If shuffle is disabled, destroyed, don't do anything
- if ( !this.enabled || this.destroyed ) {
- return;
- }
+ if (transitions.length > 0 && this.options.speed > 0) {
+ this._startTransitions(transitions);
- // Will need to check height in the future if it's layed out horizontaly
- var containerWidth = Shuffle._getOuterWidth( this.element );
+ // A call to layout happened, but none of the newly filtered items will
+ // change position. Asynchronously fire the callback here.
+ } else {
+ setTimeout(this._dispatchLayout.bind(this), 0);
+ }
- // containerWidth hasn't changed, don't do anything
- if ( containerWidth === this.containerWidth ) {
- return;
+ // Remove everything in the style queue
+ this._queue.length = 0;
}
- this.update();
-};
-
-
-/**
- * Returns styles for either jQuery animate or transition.
- * @param {Object} opts Transition options.
- * @return {!Object} Transforms for transitions, left/top for animate.
- * @private
- */
-Shuffle.prototype._getStylesForTransition = function( opts ) {
- var styles = {
- opacity: opts.opacity
- };
+ /**
+ * Create a promise for each transition and wait for all of them to complete,
+ * then emit the layout event.
+ * @param {Array.} transitions Array of transition objects.
+ */
+ _startTransitions(transitions) {
+ // Set flag that shuffle is currently in motion.
+ this.isTransitioning = true;
- if ( this.supported ) {
- styles[ TRANSFORM ] = Shuffle._getItemTransformString( opts.point, opts.scale );
- } else {
- styles.left = opts.point.x;
- styles.top = opts.point.y;
+ let promises = transitions.map(obj => this._transition(obj));
+ Promise.all(promises).then(this._movementFinished.bind(this));
}
- return styles;
-};
+ _cancelMovement() {
+ // Remove the transition end event for each listener.
+ this._transitions.forEach(cancelTransitionEnd);
+ // Reset the array.
+ this._transitions.length = 0;
-/**
- * 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
- */
-Shuffle.prototype._transition = function( opts ) {
- var styles = this._getStylesForTransition( opts );
- this._startItemAnimation( opts.$item, styles, opts.callfront || $.noop, opts.callback || $.noop );
-};
-
+ // Show it's no longer active.
+ this.isTransitioning = false;
+ }
-Shuffle.prototype._startItemAnimation = function( $item, styles, callfront, callback ) {
- 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 ) {
- $( evt.target ).off( TRANSITIONEND, handleTransitionEnd );
- _this._removeTransitionReference(reference);
- callback();
+ /**
+ * Apply styles without a transition.
+ * @param {Array.} objects Array of transition objects.
+ * @private
+ */
+ _styleImmediately(objects) {
+ if (objects.length) {
+ let elements = objects.map(obj => obj.item.element);
+
+ Shuffle._skipTransitions(elements, () => {
+ objects.forEach((obj) => {
+ obj.item.applyCss(this._getStylesForTransition(obj));
+
+ if (obj.callback) {
+ obj.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;
+ _movementFinished() {
+ this._transitions.length = 0;
+ this.isTransitioning = false;
+ this._dispatchLayout();
}
- // 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() );
+ _dispatchLayout() {
+ this._dispatch(Shuffle.EventType.LAYOUT);
}
-};
-
-/**
- * Execute the styles gathered in the style queue. This applies styles to elements,
- * triggering transitions.
- * @param {boolean} noLayout Whether to trigger a layout event.
- * @private
- */
-Shuffle.prototype._processStyleQueue = function( noLayout ) {
- if ( this.isTransitioning ) {
- this._cancelMovement();
- }
-
- var $transitions = $();
-
- // Iterate over the queue and keep track of ones that use transitions.
- each(this.styleQueue, function( transitionObj ) {
- if ( transitionObj.skipTransition ) {
- this._styleImmediately( transitionObj );
- } else {
- $transitions = $transitions.add( transitionObj.$item );
- this._transition( transitionObj );
+ /**
+ * The magic. This is what makes the plugin 'shuffle'
+ * @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
+ */
+ filter(category, sortObj) {
+ if (!this.isEnabled) {
+ return;
}
- }, this);
-
-
- if ( $transitions.length > 0 && this.initialized && 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 );
+ if (!category || (category && category.length === 0)) {
+ category = Shuffle.ALL_ITEMS;
}
- // 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 );
- }
-
- // Remove everything in the style queue
- this.styleQueue.length = 0;
-};
-
-Shuffle.prototype._cancelMovement = function() {
- 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;
- }
+ this._filter(category);
- // Reset the array.
- this._transitions.length = 0;
+ // Shrink each concealed item
+ this._shrink();
- // Show it's no longer active.
- this.isTransitioning = false;
-};
+ // How many filtered elements?
+ this._updateItemCount();
-Shuffle.prototype._removeTransitionReference = function(ref) {
- var indexInArray = $.inArray(ref, this._transitions);
- if (indexInArray > -1) {
- this._transitions.splice(indexInArray, 1);
+ // Update transforms on .filtered elements so they will animate to their new positions
+ this.sort(sortObj);
}
-};
-
-/**
- * Apply styles without a transition.
- * @param {Object} opts Transitions options object.
- * @private
- */
-Shuffle.prototype._styleImmediately = function( opts ) {
- Shuffle._skipTransition(opts.$item[0], function() {
- opts.$item.css( this._getStylesForTransition( opts ) );
- }, this);
-};
-
-Shuffle.prototype._movementFinished = function() {
- this.isTransitioning = false;
- this._layoutEnd();
-};
-
-Shuffle.prototype._layoutEnd = function() {
- this._fire( Shuffle.EventType.LAYOUT );
-};
+ /**
+ * Gets the .filtered elements, sorts them, and passes them to layout.
+ * @param {Object} opts the options object for the sorted plugin
+ */
+ sort(opts = this.lastSort) {
+ if (!this.isEnabled) {
+ return;
+ }
-Shuffle.prototype._addItems = function( $newItems, addToEnd, isSequential ) {
- // Add classes and set initial positions.
- this._initItems( $newItems );
+ this._resetCols();
- // Add transition to each item.
- this._setTransitions( $newItems );
+ var items = this._getFilteredItems();
+ items = sorter(items, opts);
- // Update the list of
- this.$items = this._getItems();
+ this._layout(items);
- // Shrink all items (without transitions).
- this._shrink( $newItems );
- each(this.styleQueue, function( transitionObj ) {
- transitionObj.skipTransition = true;
- });
+ // `_layout` always happens after `_shrink`, so it's safe to process the style
+ // queue here with styles from the shrink method.
+ this._processQueue();
- // Apply shrink positions, but do not cause a layout event.
- this._processStyleQueue( true );
+ // Adjust the height of the container.
+ this._setContainerSize();
- if ( addToEnd ) {
- this._addItemsToEnd( $newItems, isSequential );
- } else {
- this.shuffle( this.lastFilter );
+ this.lastSort = opts;
}
-};
+ /**
+ * Reposition everything.
+ * @param {boolean} isOnlyLayout If true, column and gutter widths won't be
+ * recalculated.
+ */
+ update(isOnlyLayout) {
+ if (this.isEnabled) {
+
+ if (!isOnlyLayout) {
+ // Get updated colCount
+ this._setColumns();
+ }
-Shuffle.prototype._addItemsToEnd = function( $newItems, isSequential ) {
- // Get ones that passed the current filter
- var $passed = this._filter( null, $newItems );
- var passed = $passed.get();
-
- // How many filtered elements?
- this._updateItemCount();
-
- this._layout( passed, true );
+ // Layout items
+ this.sort();
+ }
+ }
- if ( isSequential && this.supported ) {
- this._setSequentialDelay( passed );
+ /**
+ * Use this instead of `update()` if you don't need the columns and gutters updated
+ * Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
+ * could be off.
+ */
+ layout() {
+ this.update(true);
}
- this._revealAppended( passed );
-};
+ /**
+ * New items have been appended to shuffle. Mix them in with the current
+ * filter or sort status.
+ * @param {Array.} newItems Collection of new items.
+ */
+ add(newItems) {
+ newItems = arrayUnique(newItems).map(el => new ShuffleItem(el));
+ // Add classes and set initial positions.
+ this._initItems(newItems);
-/**
- * Triggers appended elements to fade in.
- * @param {ArrayLike.} $newFilteredItems Collection of elements.
- * @private
- */
-Shuffle.prototype._revealAppended = function( newFilteredItems ) {
- defer(function() {
- each(newFilteredItems, function( el ) {
- var $item = $( el );
- this._transition({
- $item: $item,
- opacity: 1,
- point: $item.data('point'),
- scale: DEFAULT_SCALE
- });
- }, this);
+ // Add transition to each item.
+ this._setTransitions(newItems);
- this._whenCollectionDone($(newFilteredItems), TRANSITIONEND, function() {
- $(newFilteredItems).css( TRANSITION_DELAY, '0ms' );
- this._movementFinished();
- });
- }, this, this.revealAppendedDelay);
-};
+ // Update the list of items.
+ this.items = this.items.concat(newItems);
+ this._updateItemsOrder();
+ this.filter(this.lastFilter);
+ }
+ /**
+ * Disables shuffle from updating dimensions and layout on resize
+ */
+ disable() {
+ this.isEnabled = false;
+ }
-/**
- * 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
- */
-Shuffle.prototype._whenCollectionDone = function( $collection, eventName, callback ) {
- var done = 0;
- var items = $collection.length;
- var self = this;
-
- function handleEventName( evt ) {
- if ( evt.target === evt.currentTarget ) {
- $( evt.target ).off( eventName, handleEventName );
- done++;
-
- // Execute callback if all items have emitted the correct event.
- if ( done === items ) {
- self._removeTransitionReference(reference);
- callback.call( self );
- }
+ /**
+ * Enables shuffle again
+ * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
+ */
+ enable(isUpdateLayout) {
+ this.isEnabled = true;
+ if (isUpdateLayout !== false) {
+ this.update();
}
}
- 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
- */
-Shuffle.prototype._whenAnimationsDone = function( callback ) {
- $.when.apply( null, this._animations ).always( $.proxy( function() {
- this._animations.length = 0;
- if (!this._isMovementCanceled) {
- callback.call( this );
+ /**
+ * Remove 1 or more shuffle items
+ * @param {Array.} collection An array containing one or more
+ * elements in shuffle
+ * @return {Shuffle} The shuffle object
+ */
+ remove(collection) {
+ if (!collection.length) {
+ return;
}
- }, this ));
-};
+ collection = arrayUnique(collection);
-/**
- * Public Methods
- */
-
-/**
- * The magic. This is what makes the plugin 'shuffle'
- * @param {string|Function} [category] Category to filter by. Can be a function
- * @param {Object} [sortObj] A sort object which can sort the filtered set
- */
-Shuffle.prototype.shuffle = function( category, sortObj ) {
- if ( !this.enabled ) {
- return;
- }
-
- if ( !category ) {
- category = ALL_ITEMS;
- }
+ let oldItems = collection
+ .map(element => this.getItemByElement(element))
+ .filter(item => !!item);
- this._filter( category );
+ let handleLayout = () => {
+ this.element.removeEventListener(Shuffle.EventType.LAYOUT, handleLayout);
+ this._disposeItems(oldItems);
- // How many filtered elements?
- this._updateItemCount();
+ // Remove the collection in the callback
+ collection.forEach((element) => {
+ element.parentNode.removeChild(element);
+ });
- // Shrink each concealed item
- this._shrink();
+ this._dispatch(Shuffle.EventType.REMOVED, { collection });
- // Update transforms on .filtered elements so they will animate to their new positions
- this.sort( sortObj );
-};
+ // Let it get garbage collected
+ collection = null;
+ oldItems = null;
+ };
+ // Hide collection first.
+ this._toggleFilterClasses({
+ filtered: [],
+ concealed: oldItems,
+ });
-/**
- * Gets the .filtered elements, sorts them, and passes them to layout.
- * @param {Object} opts the options object for the sorted plugin
- */
-Shuffle.prototype.sort = function( opts ) {
- if ( this.enabled ) {
- this._resetCols();
+ this._shrink(oldItems);
- var sortOptions = opts || this.lastSort;
- var items = this._getFilteredItems().sorted( sortOptions );
+ this.sort();
- this._layout( items );
+ // Update the list of items here because `remove` could be called again
+ // with an item that is in the process of being removed.
+ this.items = this.items.filter(item => !arrayIncludes(oldItems, item));
+ this._updateItemCount();
- this.lastSort = sortOptions;
+ this.element.addEventListener(Shuffle.EventType.LAYOUT, handleLayout);
}
-};
-
-
-/**
- * Reposition everything.
- * @param {boolean} isOnlyLayout If true, column and gutter widths won't be
- * recalculated.
- */
-Shuffle.prototype.update = function( isOnlyLayout ) {
- if ( this.enabled ) {
- if ( !isOnlyLayout ) {
- // Get updated colCount
- this._setColumns();
+ /**
+ * Retrieve a shuffle item by its element.
+ * @param {Element} element Element to look for.
+ * @return {?ShuffleItem} A shuffle item or null if it's not found.
+ */
+ getItemByElement(element) {
+ for (var i = this.items.length - 1; i >= 0; i--) {
+ if (this.items[i].element === element) {
+ return this.items[i];
+ }
}
- // Layout items
- this.sort();
+ return null;
}
-};
-
-
-/**
- * Use this instead of `update()` if you don't need the columns and gutters updated
- * Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
- * could be off.
- */
-Shuffle.prototype.layout = function() {
- this.update( true );
-};
+ /**
+ * Destroys shuffle, removes events, styles, and classes
+ */
+ destroy() {
+ this._cancelMovement();
+ window.removeEventListener('resize', this._onResize);
-/**
- * New items have been appended to shuffle. Fade them in sequentially
- * @param {jQuery} $newItems jQuery collection of new items
- * @param {boolean} [addToEnd=false] If true, new items will be added to the end / bottom
- * of the items. If not true, items will be mixed in with the current sort order.
- * @param {boolean} [isSequential=true] If false, new items won't sequentially fade in
- */
-Shuffle.prototype.appended = function( $newItems, addToEnd, isSequential ) {
- this._addItems( $newItems, addToEnd === true, isSequential !== false );
-};
+ // Reset container styles
+ this.element.classList.remove('shuffle');
+ this.element.removeAttribute('style');
+ // Reset individual item styles
+ this._disposeItems();
-/**
- * Disables shuffle from updating dimensions and layout on resize
- */
-Shuffle.prototype.disable = function() {
- this.enabled = false;
-};
+ // Null DOM references
+ this.items = null;
+ this.options.sizer = null;
+ this.element = null;
+ this._transitions = null;
-
-/**
- * Enables shuffle again
- * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
- */
-Shuffle.prototype.enable = function( isUpdateLayout ) {
- this.enabled = true;
- if ( isUpdateLayout !== false ) {
- this.update();
+ // Set a flag so if a debounced resize has been triggered,
+ // it can first check if it is actually isDestroyed and not doing anything
+ this.isDestroyed = true;
}
-};
+ /**
+ * 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.
+ * - 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);
+
+ 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;
+ }
-/**
- * Remove 1 or more shuffle items
- * @param {jQuery} $collection A jQuery object containing one or more element in shuffle
- * @return {Shuffle} The shuffle object
- */
-Shuffle.prototype.remove = function( $collection ) {
-
- // If this isn't a jquery object, exit
- if ( !$collection.length || !$collection.jquery ) {
- return;
+ return {
+ width,
+ height,
+ };
}
- function handleRemoved() {
- // Remove the collection in the callback
- $collection.remove();
+ /**
+ * Change a property or execute a function which will not have a transition
+ * @param {Array.} elements DOM elements that won't be transitioned.
+ * @param {Function} callback A function which will be called while transition
+ * is set to 0ms.
+ * @private
+ */
+ static _skipTransitions(elements, callback) {
+ let zero = '0ms';
+
+ // Save current duration and delay.
+ let data = elements.map((element) => {
+ let style = element.style;
+ let duration = style.transitionDuration;
+ let delay = style.transitionDelay;
+
+ // Set the duration to zero so it happens immediately
+ style.transitionDuration = zero;
+ style.transitionDelay = zero;
+
+ return {
+ duration,
+ delay,
+ };
+ });
- // Update things now that elements have been removed.
- this.$items = this._getItems();
- this._updateItemCount();
+ callback();
- this._fire( Shuffle.EventType.REMOVED, [ $collection, this ] );
+ // Cause reflow.
+ elements[0].offsetWidth; // jshint ignore:line
- // Let it get garbage collected
- $collection = null;
+ // Put the duration back
+ elements.forEach((element, i) => {
+ element.style.transitionDuration = data[i].duration;
+ element.style.transitionDelay = data[i].delay;
+ });
}
+}
- // Hide collection first.
- this._toggleFilterClasses( $(), $collection );
- this._shrink( $collection );
-
- this.sort();
-
- this.$el.one( Shuffle.EventType.LAYOUT + '.' + SHUFFLE, $.proxy( handleRemoved, this ) );
-};
-
+Shuffle.ALL_ITEMS = 'all';
+Shuffle.FILTER_ATTRIBUTE_KEY = 'groups';
/**
- * Destroys shuffle, removes events, styles, and classes
+ * @enum {string}
*/
-Shuffle.prototype.destroy = function() {
- // If there is more than one shuffle instance on the page,
- // removing the resize handler from the window would remove them
- // all. This is why a unique value is needed.
- $window.off('.' + this.unique);
-
- // Reset container styles
- this.$el
- .removeClass( SHUFFLE )
- .removeAttr('style')
- .removeData( SHUFFLE );
-
- // Reset individual item styles
- this.$items
- .removeAttr('style')
- .removeData('point')
- .removeData('scale')
- .removeClass([
- Shuffle.ClassName.CONCEALED,
- Shuffle.ClassName.FILTERED,
- Shuffle.ClassName.SHUFFLE_ITEM
- ].join(' '));
-
- // Null DOM references
- this.$items = null;
- this.$el = null;
- this.sizer = null;
- this.element = null;
- 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
- this.destroyed = true;
-};
-
-
-// Plugin definition
-$.fn.shuffle = function( opts ) {
- var args = Array.prototype.slice.call( arguments, 1 );
- return this.each(function() {
- var $this = $( this );
- var shuffle = $this.data( SHUFFLE );
-
- // If we don't have a stored shuffle, make a new one and save it
- if ( !shuffle ) {
- shuffle = new Shuffle( this, opts );
- $this.data( SHUFFLE, shuffle );
- } else if ( typeof opts === 'string' && shuffle[ opts ] ) {
- shuffle[ opts ].apply( shuffle, args );
- }
- });
+Shuffle.EventType = {
+ LAYOUT: 'shuffle:layout',
+ REMOVED: 'shuffle:removed',
};
+/** @enum {string} */
+Shuffle.ClassName = Classes;
-// http://stackoverflow.com/a/962890/373422
-function randomize( array ) {
- var tmp, current;
- var top = array.length;
-
- if ( !top ) {
- return array;
- }
-
- while ( --top ) {
- current = Math.floor( Math.random() * (top + 1) );
- tmp = array[ current ];
- array[ current ] = array[ top ];
- array[ top ] = tmp;
- }
+// Overrideable options
+Shuffle.options = {
+ // Initial filter group.
+ group: Shuffle.ALL_ITEMS,
- return array;
-}
+ // Transition/animation speed (milliseconds).
+ speed: 250,
+ // CSS easing function to use.
+ easing: 'ease',
-// You can return `undefined` from the `by` function to revert to DOM order
-// This plugin does NOT return a jQuery object. It returns a plain array because
-// jQuery sorts everything in DOM order.
-$.fn.sorted = function(options) {
- var opts = $.extend({}, $.fn.sorted.defaults, options);
- var arr = this.get();
- var revert = false;
+ // e.g. '.picture-item'.
+ itemSelector: '*',
- if ( !arr.length ) {
- return [];
- }
+ // Element or selector string. Use an element to determine the size of columns
+ // and gutters.
+ sizer: null,
- if ( opts.randomize ) {
- return randomize( arr );
- }
+ // A static number or function that tells the plugin how wide the gutters
+ // between columns are (in pixels).
+ gutterWidth: 0,
- // Sort the elements by the opts.by function.
- // If we don't have opts.by, default to DOM order
- if ( $.isFunction( opts.by ) ) {
- arr.sort(function(a, b) {
+ // A static number or function that returns a number which tells the plugin
+ // how wide the columns are (in pixels).
+ columnWidth: 0,
- // Exit early if we already know we want to revert
- if ( revert ) {
- return 0;
- }
+ // If your group is not json, and is comma delimeted, you could set delimeter
+ // to ','.
+ delimeter: null,
- var valA = opts.by($(a));
- var valB = opts.by($(b));
+ // Useful for percentage based heights when they might not always be exactly
+ // the same (in pixels).
+ buffer: 0,
- // If both values are undefined, use the DOM order
- if ( valA === undefined && valB === undefined ) {
- revert = true;
- return 0;
- }
+ // Reading the width of elements isn't precise enough and can cause columns to
+ // jump between values.
+ columnThreshold: 0.01,
- if ( valA < valB || valA === 'sortFirst' || valB === 'sortLast' ) {
- return -1;
- }
+ // Shuffle can be isInitialized with a sort object. It is the same object
+ // given to the sort method.
+ initialSort: null,
- if ( valA > valB || valA === 'sortLast' || valB === 'sortFirst' ) {
- return 1;
- }
+ // By default, shuffle will throttle resize events. This can be changed or
+ // removed.
+ throttle: throttle,
- return 0;
- });
- }
+ // How often shuffle can be called on resize (in milliseconds).
+ throttleTime: 300,
- // Revert to the original array if necessary
- if ( revert ) {
- return this.get();
- }
+ // Transition delay offset for each item in milliseconds.
+ staggerAmount: 15,
- if ( opts.reverse ) {
- arr.reverse();
- }
+ // Maximum stagger delay in milliseconds.
+ staggerAmountMax: 250,
- return arr;
+ // Whether to use transforms or absolute positioning.
+ useTransforms: true,
};
+// Expose for testing.
+Shuffle.Point = Point;
+Shuffle.ShuffleItem = ShuffleItem;
+Shuffle.sorter = sorter;
-$.fn.sorted.defaults = {
- reverse: false, // Use array.reverse() to reverse the results
- by: null, // Sorting function
- randomize: false // If true, this will skip the sorting and return a randomized order in the array
-};
-
-return Shuffle;
+module.exports = Shuffle;
diff --git a/webpack.config.js b/webpack.config.js
index 0e9388d..9ee9efc 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = {
name: 'build',
devtool: 'source-map',
- entry: './src/shuffle.es6.js',
+ entry: './src/shuffle.js',
output: {
filename: 'shuffle.js',
path: './dist',
diff --git a/webpack.config.min.js b/webpack.config.min.js
index f4515b6..ea5232a 100644
--- a/webpack.config.min.js
+++ b/webpack.config.min.js
@@ -4,7 +4,7 @@ const webpack = require('webpack');
module.exports = {
name: 'minified',
devtool: 'source-map',
- entry: './src/shuffle.es6.js',
+ entry: './src/shuffle.js',
output: {
filename: 'shuffle.min.js',
path: './dist',