Files
Figma_1/mixitup-3.3.1/dist/mixitup.js
2025-09-16 01:40:08 +03:00

10682 lines
345 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**!
* MixItUp v3.3.1
* A high-performance, dependency-free library for animated filtering, sorting and more
* Build 94e0fbf6-cd0b-4987-b3c0-14b59b67b8a0
*
* @copyright Copyright 2014-2018 KunkaLabs Limited.
* @author KunkaLabs Limited.
* @link https://www.kunkalabs.com/mixitup/
*
* @license Commercial use requires a commercial license.
* https://www.kunkalabs.com/mixitup/licenses/
*
* Non-commercial use permitted under same terms as CC BY-NC 3.0 license.
* http://creativecommons.org/licenses/by-nc/3.0/
*/
(function(window) {
'use strict';
var mixitup = null,
h = null;
(function() {
var VENDORS = ['webkit', 'moz', 'o', 'ms'],
canary = window.document.createElement('div'),
i = -1;
// window.requestAnimationFrame
for (i = 0; i < VENDORS.length && !window.requestAnimationFrame; i++) {
window.requestAnimationFrame = window[VENDORS[i] + 'RequestAnimationFrame'];
}
// Element.nextElementSibling
if (typeof canary.nextElementSibling === 'undefined') {
Object.defineProperty(window.Element.prototype, 'nextElementSibling', {
get: function() {
var el = this.nextSibling;
while (el) {
if (el.nodeType === 1) {
return el;
}
el = el.nextSibling;
}
return null;
}
});
}
// Element.matches
(function(ElementPrototype) {
ElementPrototype.matches =
ElementPrototype.matches ||
ElementPrototype.machesSelector ||
ElementPrototype.mozMatchesSelector ||
ElementPrototype.msMatchesSelector ||
ElementPrototype.oMatchesSelector ||
ElementPrototype.webkitMatchesSelector ||
function (selector) {
return Array.prototype.indexOf.call(this.parentElement.querySelectorAll(selector), this) > -1;
};
})(window.Element.prototype);
// Object.keys
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function() {
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = false,
dontEnums = [],
dontEnumsLength = -1;
hasDontEnumBug = !({
toString: null
})
.propertyIsEnumerable('toString');
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
];
dontEnumsLength = dontEnums.length;
return function(obj) {
var result = [],
prop = '',
i = -1;
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}
// Array.isArray
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
// Object.create
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create
if (typeof Object.create !== 'function') {
Object.create = (function(undefined) {
var Temp = function() {};
return function (prototype, propertiesObject) {
if (prototype !== Object(prototype) && prototype !== null) {
throw TypeError('Argument must be an object, or null');
}
Temp.prototype = prototype || {};
var result = new Temp();
Temp.prototype = null;
if (propertiesObject !== undefined) {
Object.defineProperties(result, propertiesObject);
}
if (prototype === null) {
/* jshint ignore:start */
result.__proto__ = null;
/* jshint ignore:end */
}
return result;
};
})();
}
// String.prototyoe.trim
if (!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
// Array.prototype.indexOf
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(searchElement) {
var n, k, t, len;
if (this === null) {
throw new TypeError();
}
t = Object(this);
len = t.length >>> 0;
if (len === 0) {
return -1;
}
n = 0;
if (arguments.length > 1) {
n = Number(arguments[1]);
if (n !== n) {
n = 0;
} else if (n !== 0 && n !== Infinity && n !== -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
for (k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
// Function.prototype.bind
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
var aArgs, self, FNOP, fBound;
if (typeof this !== 'function') {
throw new TypeError();
}
aArgs = Array.prototype.slice.call(arguments, 1);
self = this;
FNOP = function() {};
fBound = function() {
return self.apply(this instanceof FNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
FNOP.prototype = this.prototype;
}
fBound.prototype = new FNOP();
return fBound;
};
}
// Element.prototype.dispatchEvent
if (!window.Element.prototype.dispatchEvent) {
window.Element.prototype.dispatchEvent = function(event) {
try {
return this.fireEvent('on' + event.type, event);
} catch (err) {}
};
}
})();
/**
* The `mixitup()` "factory" function creates and returns individual instances
* of MixItUp, known as "mixers", on which API methods can be called.
*
* When loading MixItUp via a script tag, the factory function is accessed
* via the global variable `mixitup`. When using a module loading
* system (e.g. ES2015, CommonJS, RequireJS), the factory function is
* exported into your module when you require the MixItUp library.
*
* @example
* mixitup(container [,config] [,foreignDoc])
*
* @example <caption>Example 1: Creating a mixer instance with an element reference</caption>
* var containerEl = document.querySelector('.container');
*
* var mixer = mixitup(containerEl);
*
* @example <caption>Example 2: Creating a mixer instance with a selector string</caption>
* var mixer = mixitup('.container');
*
* @example <caption>Example 3: Passing a configuration object</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade scale(0.5)'
* }
* });
*
* @example <caption>Example 4: Passing an iframe reference</caption>
* var mixer = mixitup(containerEl, config, foreignDocument);
*
* @global
* @namespace
* @public
* @kind function
* @since 3.0.0
* @param {(Element|string)} container
* A DOM element or selector string representing the container(s) on which to instantiate MixItUp.
* @param {object} [config]
* An optional "configuration object" used to customize the behavior of the MixItUp instance.
* @param {object} [foreignDoc]
* An optional reference to a `document`, which can be used to control a MixItUp instance in an iframe.
* @return {mixitup.Mixer}
* A "mixer" object holding the MixItUp instance.
*/
mixitup = function(container, config, foreignDoc) {
var el = null,
returnCollection = false,
instance = null,
facade = null,
doc = null,
output = null,
instances = [],
id = '',
elements = [],
i = -1;
doc = foreignDoc || window.document;
if (returnCollection = arguments[3]) {
// A non-documented 4th paramater enabling control of multiple instances
returnCollection = typeof returnCollection === 'boolean';
}
if (typeof container === 'string') {
elements = doc.querySelectorAll(container);
} else if (container && typeof container === 'object' && h.isElement(container, doc)) {
elements = [container];
} else if (container && typeof container === 'object' && container.length) {
// Although not documented, the container may also be an array-like list of
// elements such as a NodeList or jQuery collection, is returnCollection is true
elements = container;
} else {
throw new Error(mixitup.messages.errorFactoryInvalidContainer());
}
if (elements.length < 1) {
throw new Error(mixitup.messages.errorFactoryContainerNotFound());
}
for (i = 0; el = elements[i]; i++) {
if (i > 0 && !returnCollection) break;
if (!el.id) {
id = 'MixItUp' + h.randomHex();
el.id = id;
} else {
id = el.id;
}
if (mixitup.instances[id] instanceof mixitup.Mixer) {
instance = mixitup.instances[id];
if (!config || (config && config.debug && config.debug.showWarnings !== false)) {
console.warn(mixitup.messages.warningFactoryPreexistingInstance());
}
} else {
instance = new mixitup.Mixer();
instance.attach(el, doc, id, config);
mixitup.instances[id] = instance;
}
facade = new mixitup.Facade(instance);
if (config && config.debug && config.debug.enable) {
instances.push(instance);
} else {
instances.push(facade);
}
}
if (returnCollection) {
output = new mixitup.Collection(instances);
} else {
// Return the first instance regardless
output = instances[0];
}
return output;
};
/**
* The `.use()` static method is used to extend the functionality of mixitup with compatible
* extensions and libraries in an environment with modular scoping e.g. ES2015, CommonJS, or RequireJS.
*
* You need only call the `.use()` function once per project, per extension, as module loaders
* will cache a single reference to MixItUp inclusive of all changes made.
*
* @example
* mixitup.use(extension)
*
* @example <caption>Example 1: Extending MixItUp with the Pagination Extension</caption>
*
* import mixitup from 'mixitup';
* import mixitupPagination from 'mixitup-pagination';
*
* mixitup.use(mixitupPagination);
*
* // All mixers created by the factory function in all modules will now
* // have pagination functionality
*
* var mixer = mixitup('.container');
*
* @public
* @name use
* @memberof mixitup
* @kind function
* @static
* @since 3.0.0
* @param {*} extension A reference to the extension or library to be used.
* @return {void}
*/
mixitup.use = function(extension) {
mixitup.Base.prototype.callActions.call(mixitup, 'beforeUse', arguments);
// Call the extension's factory function, passing
// the mixitup factory as a paramater
if (typeof extension === 'function' && extension.TYPE === 'mixitup-extension') {
// Mixitup extension
if (typeof mixitup.extensions[extension.NAME] === 'undefined') {
extension(mixitup);
mixitup.extensions[extension.NAME] = extension;
}
} else if (extension.fn && extension.fn.jquery) {
// jQuery
mixitup.libraries.$ = extension;
}
mixitup.Base.prototype.callActions.call(mixitup, 'afterUse', arguments);
};
mixitup.instances = {};
mixitup.extensions = {};
mixitup.libraries = {};
/**
* @private
*/
h = {
/**
* @private
* @param {HTMLElement} el
* @param {string} cls
* @return {boolean}
*/
hasClass: function(el, cls) {
return !!el.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
},
/**
* @private
* @param {HTMLElement} el
* @param {string} cls
* @return {void}
*/
addClass: function(el, cls) {
if (!this.hasClass(el, cls)) el.className += el.className ? ' ' + cls : cls;
},
/**
* @private
* @param {HTMLElement} el
* @param {string} cls
* @return {void}
*/
removeClass: function(el, cls) {
if (this.hasClass(el, cls)) {
var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
el.className = el.className.replace(reg, ' ').trim();
}
},
/**
* Merges the properties of the source object onto the
* target object. Alters the target object.
*
* @private
* @param {object} destination
* @param {object} source
* @param {boolean} [deep=false]
* @param {boolean} [handleErrors=false]
* @return {void}
*/
extend: function(destination, source, deep, handleErrors) {
var sourceKeys = [],
key = '',
i = -1;
deep = deep || false;
handleErrors = handleErrors || false;
try {
if (Array.isArray(source)) {
for (i = 0; i < source.length; i++) {
sourceKeys.push(i);
}
} else if (source) {
sourceKeys = Object.keys(source);
}
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (!deep || typeof source[key] !== 'object' || this.isElement(source[key])) {
// All non-object properties, or all properties if shallow extend
destination[key] = source[key];
} else if (Array.isArray(source[key])) {
// Arrays
if (!destination[key]) {
destination[key] = [];
}
this.extend(destination[key], source[key], deep, handleErrors);
} else {
// Objects
if (!destination[key]) {
destination[key] = {};
}
this.extend(destination[key], source[key], deep, handleErrors);
}
}
} catch(err) {
if (handleErrors) {
this.handleExtendError(err, destination);
} else {
throw err;
}
}
return destination;
},
/**
* @private
* @param {Error} err
* @param {object} destination
* @return {void}
*/
handleExtendError: function(err, destination) {
var re = /property "?(\w*)"?[,:] object/i,
matches = null,
erroneous = '',
message = '',
suggestion = '',
probableMatch = '',
key = '',
mostMatchingChars = -1,
i = -1;
if (err instanceof TypeError && (matches = re.exec(err.message))) {
erroneous = matches[1];
for (key in destination) {
i = 0;
while (i < erroneous.length && erroneous.charAt(i) === key.charAt(i)) {
i++;
}
if (i > mostMatchingChars) {
mostMatchingChars = i;
probableMatch = key;
}
}
if (mostMatchingChars > 1) {
suggestion = mixitup.messages.errorConfigInvalidPropertySuggestion({
probableMatch: probableMatch
});
}
message = mixitup.messages.errorConfigInvalidProperty({
erroneous: erroneous,
suggestion: suggestion
});
throw new TypeError(message);
}
throw err;
},
/**
* @private
* @param {string} str
* @return {function}
*/
template: function(str) {
var re = /\${([\w]*)}/g,
dynamics = {},
matches = null;
while ((matches = re.exec(str))) {
dynamics[matches[1]] = new RegExp('\\${' + matches[1] + '}', 'g');
}
return function(data) {
var key = '',
output = str;
data = data || {};
for (key in dynamics) {
output = output.replace(dynamics[key], typeof data[key] !== 'undefined' ? data[key] : '');
}
return output;
};
},
/**
* @private
* @param {HTMLElement} el
* @param {string} type
* @param {function} fn
* @param {boolean} useCapture
* @return {void}
*/
on: function(el, type, fn, useCapture) {
if (!el) return;
if (el.addEventListener) {
el.addEventListener(type, fn, useCapture);
} else if (el.attachEvent) {
el['e' + type + fn] = fn;
el[type + fn] = function() {
el['e' + type + fn](window.event);
};
el.attachEvent('on' + type, el[type + fn]);
}
},
/**
* @private
* @param {HTMLElement} el
* @param {string} type
* @param {function} fn
* @return {void}
*/
off: function(el, type, fn) {
if (!el) return;
if (el.removeEventListener) {
el.removeEventListener(type, fn, false);
} else if (el.detachEvent) {
el.detachEvent('on' + type, el[type + fn]);
el[type + fn] = null;
}
},
/**
* @private
* @param {string} eventType
* @param {object} detail
* @param {Document} [doc]
* @return {CustomEvent}
*/
getCustomEvent: function(eventType, detail, doc) {
var event = null;
doc = doc || window.document;
if (typeof window.CustomEvent === 'function') {
event = new window.CustomEvent(eventType, {
detail: detail,
bubbles: true,
cancelable: true
});
} else if (typeof doc.createEvent === 'function') {
event = doc.createEvent('CustomEvent');
event.initCustomEvent(eventType, true, true, detail);
} else {
event = doc.createEventObject(),
event.type = eventType;
event.returnValue = false;
event.cancelBubble = false;
event.detail = detail;
}
return event;
},
/**
* @private
* @param {Event} e
* @return {Event}
*/
getOriginalEvent: function(e) {
if (e.touches && e.touches.length) {
return e.touches[0];
} else if (e.changedTouches && e.changedTouches.length) {
return e.changedTouches[0];
} else {
return e;
}
},
/**
* @private
* @param {HTMLElement} el
* @param {string} selector
* @return {Number}
*/
index: function(el, selector) {
var i = 0;
while ((el = el.previousElementSibling) !== null) {
if (!selector || el.matches(selector)) {
++i;
}
}
return i;
},
/**
* Converts a dash or snake-case string to camel case.
*
* @private
* @param {string} str
* @param {boolean} [isPascal]
* @return {string}
*/
camelCase: function(str) {
return str.toLowerCase().replace(/([_-][a-z])/g, function($1) {
return $1.toUpperCase().replace(/[_-]/, '');
});
},
/**
* Converts a dash or snake-case string to pascal case.
*
* @private
* @param {string} str
* @param {boolean} [isPascal]
* @return {string}
*/
pascalCase: function(str) {
return (str = this.camelCase(str)).charAt(0).toUpperCase() + str.slice(1);
},
/**
* Converts a camel or pascal-case string to dash case.
*
* @private
* @param {string} str
* @return {string}
*/
dashCase: function(str) {
return str.replace(/([A-Z])/g, '-$1').replace(/^-/, '').toLowerCase();
},
/**
* @private
* @param {HTMLElement} el
* @param {HTMLHtmlElement} [doc]
* @return {boolean}
*/
isElement: function(el, doc) {
doc = doc || window.document;
if (
window.HTMLElement &&
el instanceof window.HTMLElement
) {
return true;
} else if (
doc.defaultView &&
doc.defaultView.HTMLElement &&
el instanceof doc.defaultView.HTMLElement
) {
return true;
} else {
return (
el !== null &&
el.nodeType === 1 &&
typeof el.nodeName === 'string'
);
}
},
/**
* @private
* @param {string} htmlString
* @param {HTMLHtmlElement} [doc]
* @return {DocumentFragment}
*/
createElement: function(htmlString, doc) {
var frag = null,
temp = null;
doc = doc || window.document;
frag = doc.createDocumentFragment();
temp = doc.createElement('div');
temp.innerHTML = htmlString.trim();
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
return frag;
},
/**
* @private
* @param {Node} node
* @return {void}
*/
removeWhitespace: function(node) {
var deleting;
while (node && node.nodeName === '#text') {
deleting = node;
node = node.previousSibling;
deleting.parentElement && deleting.parentElement.removeChild(deleting);
}
},
/**
* @private
* @param {Array<*>} a
* @param {Array<*>} b
* @return {boolean}
*/
isEqualArray: function(a, b) {
var i = a.length;
if (i !== b.length) return false;
while (i--) {
if (a[i] !== b[i]) return false;
}
return true;
},
/**
* @private
* @param {object} a
* @param {object} b
* @return {boolean}
*/
deepEquals: function(a, b) {
var key;
if (typeof a === 'object' && a && typeof b === 'object' && b) {
if (Object.keys(a).length !== Object.keys(b).length) return false;
for (key in a) {
if (!b.hasOwnProperty(key) || !this.deepEquals(a[key], b[key])) return false;
}
} else if (a !== b) {
return false;
}
return true;
},
/**
* @private
* @param {Array<*>} oldArray
* @return {Array<*>}
*/
arrayShuffle: function(oldArray) {
var newArray = oldArray.slice(),
len = newArray.length,
i = len,
p = -1,
t = [];
while (i--) {
p = ~~(Math.random() * len);
t = newArray[i];
newArray[i] = newArray[p];
newArray[p] = t;
}
return newArray;
},
/**
* @private
* @param {object} list
*/
arrayFromList: function(list) {
var output, i;
try {
return Array.prototype.slice.call(list);
} catch(err) {
output = [];
for (i = 0; i < list.length; i++) {
output.push(list[i]);
}
return output;
}
},
/**
* @private
* @param {function} func
* @param {Number} wait
* @param {boolean} immediate
* @return {function}
*/
debounce: function(func, wait, immediate) {
var timeout;
return function() {
var self = this,
args = arguments,
callNow = immediate && !timeout,
later = null;
later = function() {
timeout = null;
if (!immediate) {
func.apply(self, args);
}
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(self, args);
};
},
/**
* @private
* @param {HTMLElement} element
* @return {object}
*/
position: function(element) {
var xPosition = 0,
yPosition = 0,
offsetParent = element;
while (element) {
xPosition -= element.scrollLeft;
yPosition -= element.scrollTop;
if (element === offsetParent) {
xPosition += element.offsetLeft;
yPosition += element.offsetTop;
offsetParent = element.offsetParent;
}
element = element.parentElement;
}
return {
x: xPosition,
y: yPosition
};
},
/**
* @private
* @param {object} node1
* @param {object} node2
* @return {Number}
*/
getHypotenuse: function(node1, node2) {
var distanceX = node1.x - node2.x,
distanceY = node1.y - node2.y;
distanceX = distanceX < 0 ? distanceX * -1 : distanceX,
distanceY = distanceY < 0 ? distanceY * -1 : distanceY;
return Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
},
/**
* Calcuates the area of intersection between two rectangles and expresses it as
* a ratio in comparison to the area of the first rectangle.
*
* @private
* @param {Rect} box1
* @param {Rect} box2
* @return {number}
*/
getIntersectionRatio: function(box1, box2) {
var controlArea = box1.width * box1.height,
intersectionX = -1,
intersectionY = -1,
intersectionArea = -1,
ratio = -1;
intersectionX =
Math.max(0, Math.min(box1.left + box1.width, box2.left + box2.width) - Math.max(box1.left, box2.left));
intersectionY =
Math.max(0, Math.min(box1.top + box1.height, box2.top + box2.height) - Math.max(box1.top, box2.top));
intersectionArea = intersectionY * intersectionX;
ratio = intersectionArea / controlArea;
return ratio;
},
/**
* @private
* @param {object} el
* @param {string} selector
* @param {boolean} [includeSelf]
* @param {HTMLHtmlElement} [doc]
* @return {Element|null}
*/
closestParent: function(el, selector, includeSelf, doc) {
var parent = el.parentNode;
doc = doc || window.document;
if (includeSelf && el.matches(selector)) {
return el;
}
while (parent && parent != doc.body) {
if (parent.matches && parent.matches(selector)) {
return parent;
} else if (parent.parentNode) {
parent = parent.parentNode;
} else {
return null;
}
}
return null;
},
/**
* @private
* @param {HTMLElement} el
* @param {string} selector
* @param {HTMLHtmlElement} [doc]
* @return {NodeList}
*/
children: function(el, selector, doc) {
var children = [],
tempId = '';
doc = doc || window.doc;
if (el) {
if (!el.id) {
tempId = 'Temp' + this.randomHexKey();
el.id = tempId;
}
children = doc.querySelectorAll('#' + el.id + ' > ' + selector);
if (tempId) {
el.removeAttribute('id');
}
}
return children;
},
/**
* Creates a clone of a provided array, with any empty strings removed.
*
* @private
* @param {Array<*>} originalArray
* @return {Array<*>}
*/
clean: function(originalArray) {
var cleanArray = [],
i = -1;
for (i = 0; i < originalArray.length; i++) {
if (originalArray[i] !== '') {
cleanArray.push(originalArray[i]);
}
}
return cleanArray;
},
/**
* Abstracts an ES6 promise into a q-like deferred interface for storage and deferred resolution.
*
* @private
* @param {object} libraries
* @return {h.Deferred}
*/
defer: function(libraries) {
var deferred = null,
promiseWrapper = null,
$ = null;
promiseWrapper = new this.Deferred();
if (mixitup.features.has.promises) {
// ES6 native promise or polyfill
promiseWrapper.promise = new Promise(function(resolve, reject) {
promiseWrapper.resolve = resolve;
promiseWrapper.reject = reject;
});
} else if (($ = (window.jQuery || libraries.$)) && typeof $.Deferred === 'function') {
// jQuery
deferred = $.Deferred();
promiseWrapper.promise = deferred.promise();
promiseWrapper.resolve = deferred.resolve;
promiseWrapper.reject = deferred.reject;
} else if (window.console) {
// No implementation
console.warn(mixitup.messages.warningNoPromiseImplementation());
}
return promiseWrapper;
},
/**
* @private
* @param {Array<Promise>} tasks
* @param {object} libraries
* @return {Promise<Array>}
*/
all: function(tasks, libraries) {
var $ = null;
if (mixitup.features.has.promises) {
return Promise.all(tasks);
} else if (($ = (window.jQuery || libraries.$)) && typeof $.when === 'function') {
return $.when.apply($, tasks)
.done(function() {
// jQuery when returns spread arguments rather than an array or resolutions
return arguments;
});
}
// No implementation
if (window.console) {
console.warn(mixitup.messages.warningNoPromiseImplementation());
}
return [];
},
/**
* @private
* @param {HTMLElement} el
* @param {string} property
* @param {Array<string>} vendors
* @return {string}
*/
getPrefix: function(el, property, vendors) {
var i = -1,
prefix = '';
if (h.dashCase(property) in el.style) return '';
for (i = 0; prefix = vendors[i]; i++) {
if (prefix + property in el.style) {
return prefix.toLowerCase();
}
}
return 'unsupported';
},
/**
* @private
* @return {string}
*/
randomHex: function() {
return ('00000' + (Math.random() * 16777216 << 0).toString(16)).substr(-6).toUpperCase();
},
/**
* @private
* @param {HTMLDocument} [doc]
* @return {object}
*/
getDocumentState: function(doc) {
doc = typeof doc.body === 'object' ? doc : window.document;
return {
scrollTop: window.pageYOffset,
scrollLeft: window.pageXOffset,
docHeight: doc.documentElement.scrollHeight,
docWidth: doc.documentElement.scrollWidth,
viewportHeight: doc.documentElement.clientHeight,
viewportWidth: doc.documentElement.clientWidth
};
},
/**
* @private
* @param {object} obj
* @param {function} fn
* @return {function}
*/
bind: function(obj, fn) {
return function() {
return fn.apply(obj, arguments);
};
},
/**
* @private
* @param {HTMLElement} el
* @return {boolean}
*/
isVisible: function(el) {
var styles = null;
if (el.offsetParent) return true;
styles = window.getComputedStyle(el);
if (
styles.position === 'fixed' &&
styles.visibility !== 'hidden' &&
styles.opacity !== '0'
) {
// Fixed elements report no offsetParent,
// but may still be invisible
return true;
}
return false;
},
/**
* @private
* @param {object} obj
*/
seal: function(obj) {
if (typeof Object.seal === 'function') {
Object.seal(obj);
}
},
/**
* @private
* @param {object} obj
*/
freeze: function(obj) {
if (typeof Object.freeze === 'function') {
Object.freeze(obj);
}
},
/**
* @private
* @param {string} control
* @param {string} specimen
* @return {boolean}
*/
compareVersions: function(control, specimen) {
var controlParts = control.split('.'),
specimenParts = specimen.split('.'),
controlPart = -1,
specimenPart = -1,
i = -1;
for (i = 0; i < controlParts.length; i++) {
controlPart = parseInt(controlParts[i].replace(/[^\d.]/g, ''));
specimenPart = parseInt(specimenParts[i].replace(/[^\d.]/g, '') || 0);
if (specimenPart < controlPart) {
return false;
} else if (specimenPart > controlPart) {
return true;
}
}
return true;
},
/**
* @private
* @constructor
*/
Deferred: function() {
this.promise = null;
this.resolve = null;
this.reject = null;
this.id = h.randomHex();
},
/**
* @private
* @param {object} obj
* @return {boolean}
*/
isEmptyObject: function(obj) {
var key = '';
if (typeof Object.keys === 'function') {
return Object.keys(obj).length === 0;
}
for (key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
},
/**
* @param {mixitup.Config.ClassNames} classNames
* @param {string} elementName
* @param {string} [modifier]
* @return {string}
*/
getClassname: function(classNames, elementName, modifier) {
var classname = '';
classname += classNames.block;
if (classname.length) {
classname += classNames.delineatorElement;
}
classname += classNames['element' + this.pascalCase(elementName)];
if (!modifier) return classname;
if (classname.length) {
classname += classNames.delineatorModifier;
}
classname += modifier;
return classname;
},
/**
* Returns the value of a property on a given object via its string key.
*
* @param {object} obj
* @param {string} stringKey
* @return {*} value
*/
getProperty: function(obj, stringKey) {
var parts = stringKey.split('.'),
returnCurrent = null,
current = '',
i = 0;
if (!stringKey) {
return obj;
}
returnCurrent = function(obj) {
if (!obj) {
return null;
} else {
return obj[current];
}
};
while (i < parts.length) {
current = parts[i];
obj = returnCurrent(obj);
i++;
}
if (typeof obj !== 'undefined') {
return obj;
} else {
return null;
}
}
};
mixitup.h = h;
/**
* The Base class adds instance methods to all other extensible MixItUp classes,
* enabling the calling of any registered hooks.
*
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Base = function() {};
mixitup.Base.prototype = {
constructor: mixitup.Base,
/**
* Calls any registered hooks for the provided action.
*
* @memberof mixitup.Base
* @private
* @instance
* @since 2.0.0
* @param {string} actionName
* @param {Array<*>} args
* @return {void}
*/
callActions: function(actionName, args) {
var self = this,
hooks = self.constructor.actions[actionName],
extensionName = '';
if (!hooks || h.isEmptyObject(hooks)) return;
for (extensionName in hooks) {
hooks[extensionName].apply(self, args);
}
},
/**
* Calls any registered hooks for the provided filter.
*
* @memberof mixitup.Base
* @private
* @instance
* @since 2.0.0
* @param {string} filterName
* @param {*} input
* @param {Array<*>} args
* @return {*}
*/
callFilters: function(filterName, input, args) {
var self = this,
hooks = self.constructor.filters[filterName],
output = input,
extensionName = '';
if (!hooks || h.isEmptyObject(hooks)) return output;
args = args || [];
for (extensionName in hooks) {
args = h.arrayFromList(args);
args.unshift(output);
output = hooks[extensionName].apply(self, args);
}
return output;
}
};
/**
* The BaseStatic class holds a set of static methods which are then added to all other
* extensible MixItUp classes as a means of integrating extensions via the addition of new
* methods and/or actions and hooks.
*
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.BaseStatic = function() {
this.actions = {};
this.filters = {};
/**
* Performs a shallow extend on the class's prototype, adding one or more new members to
* the class in a single operation.
*
* @memberof mixitup.BaseStatic
* @public
* @static
* @since 2.1.0
* @param {object} extension
* @return {void}
*/
this.extend = function(extension) {
h.extend(this.prototype, extension);
};
/**
* Registers a function to be called on the action hook of the provided name.
*
* @memberof mixitup.BaseStatic
* @public
* @static
* @since 2.1.0
* @param {string} hookName
* @param {string} extensionName
* @param {function} func
* @return {void}
*/
this.registerAction = function(hookName, extensionName, func) {
(this.actions[hookName] = this.actions[hookName] || {})[extensionName] = func;
};
/**
* Registers a function to be called on the filter of the provided name.
*
* @memberof mixitup.BaseStatic
* @public
* @static
* @since 2.1.0
* @param {string} hookName
* @param {string} extensionName
* @param {function} func
* @return {void}
*/
this.registerFilter = function(hookName, extensionName, func) {
(this.filters[hookName] = this.filters[hookName] || {})[extensionName] = func;
};
};
/**
* The `mixitup.Features` class performs all feature and CSS prefix detection
* neccessary for MixItUp to function correctly, as well as storing various
* string and array constants. All feature decection is on evaluation of the
* library and stored in a singleton instance for use by other internal classes.
*
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Features = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.boxSizingPrefix = '';
this.transformPrefix = '';
this.transitionPrefix = '';
this.boxSizingPrefix = '';
this.transformProp = '';
this.transformRule = '';
this.transitionProp = '';
this.perspectiveProp = '';
this.perspectiveOriginProp = '';
this.has = new mixitup.Has();
this.canary = null;
this.BOX_SIZING_PROP = 'boxSizing';
this.TRANSITION_PROP = 'transition';
this.TRANSFORM_PROP = 'transform';
this.PERSPECTIVE_PROP = 'perspective';
this.PERSPECTIVE_ORIGIN_PROP = 'perspectiveOrigin';
this.VENDORS = ['Webkit', 'moz', 'O', 'ms'];
this.TWEENABLE = [
'opacity',
'width', 'height',
'marginRight', 'marginBottom',
'x', 'y',
'scale',
'translateX', 'translateY', 'translateZ',
'rotateX', 'rotateY', 'rotateZ'
];
this.callActions('afterConstruct');
};
mixitup.BaseStatic.call(mixitup.Features);
mixitup.Features.prototype = Object.create(mixitup.Base.prototype);
h.extend(mixitup.Features.prototype,
/** @lends mixitup.Features */
{
constructor: mixitup.Features,
/**
* @private
* @return {void}
*/
init: function() {
var self = this;
self.callActions('beforeInit', arguments);
self.canary = document.createElement('div');
self.setPrefixes();
self.runTests();
self.callActions('beforeInit', arguments);
},
/**
* @private
* @return {void}
*/
runTests: function() {
var self = this;
self.callActions('beforeRunTests', arguments);
self.has.promises = typeof window.Promise === 'function';
self.has.transitions = self.transitionPrefix !== 'unsupported';
self.callActions('afterRunTests', arguments);
h.freeze(self.has);
},
/**
* @private
* @return {void}
*/
setPrefixes: function() {
var self = this;
self.callActions('beforeSetPrefixes', arguments);
self.transitionPrefix = h.getPrefix(self.canary, 'Transition', self.VENDORS);
self.transformPrefix = h.getPrefix(self.canary, 'Transform', self.VENDORS);
self.boxSizingPrefix = h.getPrefix(self.canary, 'BoxSizing', self.VENDORS);
self.boxSizingProp = self.boxSizingPrefix ?
self.boxSizingPrefix + h.pascalCase(self.BOX_SIZING_PROP) : self.BOX_SIZING_PROP;
self.transitionProp = self.transitionPrefix ?
self.transitionPrefix + h.pascalCase(self.TRANSITION_PROP) : self.TRANSITION_PROP;
self.transformProp = self.transformPrefix ?
self.transformPrefix + h.pascalCase(self.TRANSFORM_PROP) : self.TRANSFORM_PROP;
self.transformRule = self.transformPrefix ?
'-' + self.transformPrefix + '-' + self.TRANSFORM_PROP : self.TRANSFORM_PROP;
self.perspectiveProp = self.transformPrefix ?
self.transformPrefix + h.pascalCase(self.PERSPECTIVE_PROP) : self.PERSPECTIVE_PROP;
self.perspectiveOriginProp = self.transformPrefix ?
self.transformPrefix + h.pascalCase(self.PERSPECTIVE_ORIGIN_PROP) :
self.PERSPECTIVE_ORIGIN_PROP;
self.callActions('afterSetPrefixes', arguments);
}
});
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Has = function() {
this.transitions = false;
this.promises = false;
h.seal(this);
};
// Assign a singleton instance to `mixitup.features` and initialise:
mixitup.features = new mixitup.Features();
mixitup.features.init();
/**
* A group of properties defining the mixer's animation and effects settings.
*
* @constructor
* @memberof mixitup.Config
* @name animation
* @namespace
* @public
* @since 2.0.0
*/
mixitup.ConfigAnimation = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A boolean dictating whether or not animation should be enabled for the MixItUp instance.
* If `false`, all operations will occur instantly and syncronously, although callback
* functions and any returned promises will still be fulfilled.
*
* @example <caption>Example: Create a mixer with all animations disabled</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* enable: false
* }
* });
*
* @name enable
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.enable = true;
/**
* A string of one or more space-seperated properties to which transitions will be
* applied for all filtering animations.
*
* Properties can be listed any order or combination, although they will be applied in a specific
* predefined order to produce consistent results.
*
* To learn more about available effects, experiment with our <a href="https://www.kunkalabs.com/mixitup/">
* sandbox demo</a> and try out the "Export config" button in the Animation options drop down.
*
* @example <caption>Example: Apply "fade" and "translateZ" effects to all animations</caption>
* // As targets are filtered in and out, they will fade between
* // opacity 1 and 0 and transform between translateZ(-100px) and
* // translateZ(0).
*
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade translateZ(-100px)'
* }
* });
*
* @name effects
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default 'fade scale'
*/
this.effects = 'fade scale';
/**
* A string of one or more space-seperated effects to be applied only to filter-in
* animations, overriding `config.animation.effects` if set.
*
* @example <caption>Example: Apply downwards vertical translate to targets being filtered in</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* effectsIn: 'fade translateY(-100%)'
* }
* });
*
* @name effectsIn
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default ''
*/
this.effectsIn = '';
/**
* A string of one or more space-seperated effects to be applied only to filter-out
* animations, overriding `config.animation.effects` if set.
*
* @example <caption>Example: Apply upwards vertical translate to targets being filtered out</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* effectsOut: 'fade translateY(-100%)'
* }
* });
*
* @name effectsOut
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default ''
*/
this.effectsOut = '';
/**
* An integer dictating the duration of all MixItUp animations in milliseconds, not
* including any additional delay apllied via the `'stagger'` effect.
*
* @example <caption>Example: Apply an animation duration of 200ms to all mixitup animations</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* duration: 200
* }
* });
*
* @name duration
* @memberof mixitup.Config.animation
* @instance
* @type {number}
* @default 600
*/
this.duration = 600;
/**
* A valid CSS3 transition-timing function or shorthand. For a full list of accepted
* values, visit <a href="http://easings.net" target="_blank">easings.net</a>.
*
* @example <caption>Example 1: Apply "ease-in-out" easing to all animations</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* easing: 'ease-in-out'
* }
* });
*
* @example <caption>Example 2: Apply a custom "cubic-bezier" easing function to all animations</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)'
* }
* });
*
* @name easing
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default 'ease'
*/
this.easing = 'ease';
/**
* A boolean dictating whether or not to apply perspective to the MixItUp container
* during animations. By default, perspective is always applied and creates the
* illusion of three-dimensional space for effects such as `translateZ`, `rotateX`,
* and `rotateY`.
*
* You may wish to disable this and define your own perspective settings via CSS.
*
* @example <caption>Example: Prevent perspective from being applied to any 3D transforms</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* applyPerspective: false
* }
* });
*
* @name applyPerspective
* @memberof mixitup.Config.animation
* @instance
* @type {bolean}
* @default true
*/
this.applyPerspective = true;
/**
* The perspective distance value to be applied to the container during animations,
* affecting any 3D-transform-based effects.
*
* @example <caption>Example: Set a perspective distance of 2000px</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'rotateY(-25deg)',
* perspectiveDistance: '2000px'
* }
* });
*
* @name perspectiveDistance
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default '3000px'
*/
this.perspectiveDistance = '3000px';
/**
* The perspective-origin value to be applied to the container during animations,
* affecting any 3D-transform-based effects.
*
* @example <caption>Example: Set a perspective origin in the top-right of the container</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'transateZ(-200px)',
* perspectiveOrigin: '100% 0'
* }
* });
*
* @name perspectiveOrigin
* @memberof mixitup.Config.animation
* @instance
* @type {string}
* @default '50% 50%'
*/
this.perspectiveOrigin = '50% 50%';
/**
* A boolean dictating whether or not to enable the queuing of operations.
*
* If `true` (default), and a control is clicked or an API call is made while another
* operation is progress, the operation will go into the queue and will be automatically exectuted
* when the previous operaitons is finished.
*
* If `false`, any requested operations will be ignored, and the `onMixBusy` callback and `mixBusy`
* event will be fired. If `debug.showWarnings` is enabled, a console warning will also occur.
*
* @example <caption>Example: Disable queuing</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* queue: false
* }
* });
*
* @name queue
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.queue = true;
/**
* An integer dictacting the maximum number of operations allowed in the queue at
* any time, when queuing is enabled.
*
* @example <caption>Example: Allow a maximum of 5 operations in the queue at any time</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* queueLimit: 5
* }
* });
*
* @name queueLimit
* @memberof mixitup.Config.animation
* @instance
* @type {number}
* @default 3
*/
this.queueLimit = 3;
/**
* A boolean dictating whether or not to transition the height and width of the
* container as elements are filtered in and out. If disabled, the container height
* will change abruptly.
*
* It may be desirable to disable this on mobile devices as the CSS `height` and
* `width` properties do not receive GPU-acceleration and can therefore cause stuttering.
*
* @example <caption>Example 1: Disable the transitioning of the container height and/or width</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* animateResizeContainer: false
* }
* });
*
* @example <caption>Example 2: Disable the transitioning of the container height and/or width for mobile devices only</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* animateResizeContainer: myFeatureTests.isMobile ? false : true
* }
* });
*
* @name animateResizeContainer
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.animateResizeContainer = true;
/**
* A boolean dictating whether or not to transition the height and width of target
* elements as they change throughout the course of an animation.
*
* This is often a must for flex-box grid layouts where the size of target elements may change
* depending on final their position in relation to their siblings, or for `.changeLayout()`
* operations where the size of targets change between layouts.
*
* NB: This feature requires additional calculations and manipulation to non-hardware-accelerated
* properties which may adversely affect performance on slower devices, and is therefore
* disabled by default.
*
* @example <caption>Example: Enable the transitioning of target widths and heights</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* animateResizeTargets: true
* }
* });
*
* @name animateResizeTargets
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default false
*/
this.animateResizeTargets = false;
/**
* A custom function used to manipulate the order in which the stagger delay is
* incremented when using the stagger effect.
*
* When using the 'stagger' effect, the delay applied to each target element is incremented
* based on its index. You may create a custom function to manipulate the order in which the
* delay is incremented and create engaging non-linear stagger effects.
*
* The function receives the index of the target element as a parameter, and must
* return an integer which serves as the multiplier for the stagger delay.
*
* @example <caption>Example 1: Stagger target elements by column in a 3-column grid</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade stagger(100ms)',
* staggerSequence: function(i) {
* return i % 3;
* }
* }
* });
*
* @example <caption>Example 2: Using an algorithm to produce a more complex sequence</caption>
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade stagger(100ms)',
* staggerSequence: function(i) {
* return (2*i) - (5*((i/3) - ((1/3) * (i%3))));
* }
* }
* });
*
* @name staggerSequence
* @memberof mixitup.Config.animation
* @instance
* @type {function}
* @default null
*/
this.staggerSequence = null;
/**
* A boolean dictating whether or not to reverse the direction of `translate`
* and `rotate` transforms for elements being filtered out.
*
* It can be used to create carousel-like animations where elements enter and exit
* from opposite directions. If enabled, the effect `translateX(-100%)` for elements
* being filtered in would become `translateX(100%)` for targets being filtered out.
*
* This functionality can also be achieved by providing seperate effects
* strings for `config.animation.effectsIn` and `config.animation.effectsOut`.
*
* @example <caption>Example: Reverse the desired direction on any translate/rotate effect for targets being filtered out</caption>
* // Elements being filtered in will be translated from '100%' to '0' while
* // elements being filtered out will be translated from 0 to '-100%'
*
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade translateX(100%)',
* reverseOut: true,
* nudge: false // Disable nudging to create a carousel-like effect
* }
* });
*
* @name reverseOut
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default false
*/
this.reverseOut = false;
/**
* A boolean dictating whether or not to "nudge" the animation path of targets
* when they are being filtered in and out simulatenously.
*
* This has been the default behavior of MixItUp since version 1, but it
* may be desirable to disable this effect when filtering directly from
* one exclusive set of targets to a different exclusive set of targets,
* to create a carousel-like effect, or a generally more subtle animation.
*
* @example <caption>Example: Disable the "nudging" of targets being filtered in and out simulatenously</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* nudge: false
* }
* });
*
* @name nudge
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.nudge = true;
/**
* A boolean dictating whether or not to clamp the height of the container while MixItUp's
* geometry tests are carried out before an operation.
*
* To prevent scroll-bar flicker, clamping is turned on by default. But in the case where the
* height of the container might affect its vertical positioning in the viewport
* (e.g. a vertically-centered container), this should be turned off to ensure accurate
* test results and a smooth animation.
*
* @example <caption>Example: Disable container height-clamping</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* clampHeight: false
* }
* });
*
* @name clampHeight
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.clampHeight = true;
/**
* A boolean dictating whether or not to clamp the width of the container while MixItUp's
* geometry tests are carried out before an operation.
*
* To prevent scroll-bar flicker, clamping is turned on by default. But in the case where the
* width of the container might affect its horitzontal positioning in the viewport
* (e.g. a horizontall-centered container), this should be turned off to ensure accurate
* test results and a smooth animation.
*
* @example <caption>Example: Disable container width-clamping</caption>
*
* var mixer = mixitup(containerEl, {
* animation: {
* clampWidth: false
* }
* });
*
* @name clampWidth
* @memberof mixitup.Config.animation
* @instance
* @type {boolean}
* @default true
*/
this.clampWidth = true;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigAnimation);
mixitup.ConfigAnimation.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigAnimation.prototype.constructor = mixitup.ConfigAnimation;
/**
* A group of properties relating to the behavior of the Mixer.
*
* @constructor
* @memberof mixitup.Config
* @name behavior
* @namespace
* @public
* @since 3.1.12
*/
mixitup.ConfigBehavior = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A boolean dictating whether to allow "live" sorting of the mixer.
*
* Because of the expensive nature of sorting, MixItUp makes use of several
* internal optimizations to skip redundant sorting operations, such as when
* the newly requested sort command is the same as the active one. The caveat
* to this optimization is that "live" edits to the value of a target's sorting
* attribute will be ignored when requesting a re-sort by the same attribute.
*
* By setting to `behavior.liveSort` to `true`, the mixer will always re-sort
* regardless of whether or not the sorting attribute and order have changed.
*
* @example <caption>Example: Enabling `liveSort` to allow for re-sorting</caption>
*
* var mixer = mixitup(containerEl, {
* behavior: {
* liveSort: true
* },
* load: {
* sort: 'edited:desc'
* }
* });
*
* var target = containerEl.children[3];
*
* console.log(target.getAttribute('data-edited')); // '2015-04-24'
*
* target.setAttribute('data-edited', '2017-08-10'); // Update the target's edited date
*
* mixer.sort('edited:desc')
* .then(function(state) {
* // The target is now at the top of the list
*
* console.log(state.targets[0] === target); // true
* });
*
* @name liveSort
* @memberof mixitup.Config.behavior
* @instance
* @type {boolean}
* @default false
*/
this.liveSort = false;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigBehavior);
mixitup.ConfigBehavior.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigBehavior.prototype.constructor = mixitup.ConfigBehavior;
/**
* A group of optional callback functions to be invoked at various
* points within the lifecycle of a mixer operation.
*
* Each function is analogous to an event of the same name triggered from the
* container element, and is invoked immediately after it.
*
* All callback functions receive the current `state` object as their first
* argument, as well as other more specific arguments described below.
*
* @constructor
* @memberof mixitup.Config
* @name callbacks
* @namespace
* @public
* @since 2.0.0
*/
mixitup.ConfigCallbacks = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A callback function invoked immediately after any MixItUp operation is requested
* and before animations have begun.
*
* A second `futureState` argument is passed to the function which represents the final
* state of the mixer once the requested operation has completed.
*
* @example <caption>Example: Adding an `onMixStart` callback function</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixStart: function(state, futureState) {
* console.log('Starting operation...');
* }
* }
* });
*
* @name onMixStart
* @memberof mixitup.Config.callbacks
* @instance
* @type {function}
* @default null
*/
this.onMixStart = null;
/**
* A callback function invoked when a MixItUp operation is requested while another
* operation is in progress, and the animation queue is full, or queueing
* is disabled.
*
* @example <caption>Example: Adding an `onMixBusy` callback function</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixBusy: function(state) {
* console.log('Mixer busy');
* }
* }
* });
*
* @name onMixBusy
* @memberof mixitup.Config.callbacks
* @instance
* @type {function}
* @default null
*/
this.onMixBusy = null;
/**
* A callback function invoked after any MixItUp operation has completed, and the
* state has been updated.
*
* @example <caption>Example: Adding an `onMixEnd` callback function</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixEnd: function(state) {
* console.log('Operation complete');
* }
* }
* });
*
* @name onMixEnd
* @memberof mixitup.Config.callbacks
* @instance
* @type {function}
* @default null
*/
this.onMixEnd = null;
/**
* A callback function invoked whenever an operation "fails", i.e. no targets
* could be found matching the requested filter.
*
* @example <caption>Example: Adding an `onMixFail` callback function</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixFail: function(state) {
* console.log('No items could be found matching the requested filter');
* }
* }
* });
*
* @name onMixFail
* @memberof mixitup.Config.callbacks
* @instance
* @type {function}
* @default null
*/
this.onMixFail = null;
/**
* A callback function invoked whenever a MixItUp control is clicked, and before its
* respective operation is requested.
*
* The clicked element is assigned to the `this` keyword within the function. The original
* click event is passed to the function as the second argument, which can be useful if
* using `<a>` tags as controls where the default behavior needs to be prevented.
*
* Returning `false` from the callback will prevent the control click from triggering
* an operation.
*
* @example <caption>Example 1: Adding an `onMixClick` callback function</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixClick: function(state, originalEvent) {
* console.log('The control "' + this.innerText + '" was clicked');
* }
* }
* });
*
* @example <caption>Example 2: Using `onMixClick` to manipulate the original click event</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixClick: function(state, originalEvent) {
* // Prevent original click event from bubbling up:
* originalEvent.stopPropagation();
*
* // Prevent default behavior of clicked element:
* originalEvent.preventDefault();
* }
* }
* });
*
* @example <caption>Example 3: Using `onMixClick` to conditionally cancel operations</caption>
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixClick: function(state, originalEvent) {
* // Perform some conditional check:
*
* if (myApp.isLoading) {
* // By returning false, we can prevent the control click from triggering an operation.
*
* return false;
* }
* }
* }
* });
*
* @name onMixClick
* @memberof mixitup.Config.callbacks
* @instance
* @type {function}
* @default null
*/
this.onMixClick = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigCallbacks);
mixitup.ConfigCallbacks.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigCallbacks.prototype.constructor = mixitup.ConfigCallbacks;
/**
* A group of properties relating to clickable control elements.
*
* @constructor
* @memberof mixitup.Config
* @name controls
* @namespace
* @public
* @since 2.0.0
*/
mixitup.ConfigControls = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A boolean dictating whether or not controls should be enabled for the mixer instance.
*
* If `true` (default behavior), MixItUp will search the DOM for any clickable elements with
* `data-filter`, `data-sort` or `data-toggle` attributes, and bind them for click events.
*
* If `false`, no click handlers will be bound, and all functionality must therefore be performed
* via the mixer's API methods.
*
* If you do not intend to use the default controls, setting this property to `false` will
* marginally improve the startup time of your mixer instance, and will also prevent any other active
* mixer instances in the DOM which are bound to controls from controlling the instance.
*
* @example <caption>Example: Disabling controls</caption>
* var mixer = mixitup(containerEl, {
* controls: {
* enable: false
* }
* });
*
* // With the default controls disabled, we can only control
* // the mixer via its API methods, e.g.:
*
* mixer.filter('.cat-1');
*
* @name enable
* @memberof mixitup.Config.controls
* @instance
* @type {boolean}
* @default true
*/
this.enable = true;
/**
* A boolean dictating whether or not to use event delegation when binding click events
* to the default controls.
*
* If `false` (default behavior), each control button in the DOM will be found and
* individually bound when a mixer is instantiated, with their corresponding actions
* cached for performance.
*
* If `true`, a single click handler will be applied to the `window` (or container element - see
* `config.controls.scope`), and any click events triggered by elements with `data-filter`,
* `data-sort` or `data-toggle` attributes present will be handled as they propagate upwards.
*
* If you require a user interface where control buttons may be added, removed, or changed during the
* lifetime of a mixer, `controls.live` should be set to `true`. There is a marginal but unavoidable
* performance deficit when using live controls, as the value of each control button must be read
* from the DOM in real time once the click event has propagated.
*
* @example <caption>Example: Setting live controls</caption>
* var mixer = mixitup(containerEl, {
* controls: {
* live: true
* }
* });
*
* // Control buttons can now be added, remove and changed without breaking
* // the mixer's UI
*
* @name live
* @memberof mixitup.Config.controls
* @instance
* @type {boolean}
* @default true
*/
this.live = false;
/**
* A string dictating the "scope" to use when binding or querying the default controls. The available
* values are `'global'` or `'local'`.
*
* When set to `'global'` (default behavior), MixItUp will query the entire document for control buttons
* to bind, or delegate click events from (see `config.controls.live`).
*
* When set to `'local'`, MixItUp will only query (or bind click events to) its own container element.
* This may be desireable if you require multiple active mixer instances within the same document, with
* controls that would otherwise intefere with each other if scoped globally.
*
* Conversely, if you wish to control multiple instances with a single UI, you would create one
* set of controls and keep the controls scope of each mixer set to `global`.
*
* @example <caption>Example: Setting 'local' scoped controls</caption>
* var mixerOne = mixitup(containerOne, {
* controls: {
* scope: 'local'
* }
* });
*
* var mixerTwo = mixitup(containerTwo, {
* controls: {
* scope: 'local'
* }
* });
*
* // Both mixers can now exist within the same document with
* // isolated controls placed within their container elements.
*
* @name scope
* @memberof mixitup.Config.controls
* @instance
* @type {string}
* @default 'global'
*/
this.scope = 'global'; // enum: ['local' ,'global']
/**
* A string dictating the type of logic to apply when concatenating the filter selectors of
* active toggle buttons (i.e. any clickable element with a `data-toggle` attribute).
*
* If set to `'or'` (default behavior), selectors will be concatenated together as
* a comma-seperated list. For example:
*
* `'.cat-1, .cat-2'` (shows any elements matching `'.cat-1'` OR `'.cat-2'`)
*
* If set to `'and'`, selectors will be directly concatenated together. For example:
*
* `'.cat-1.cat-2'` (shows any elements which match both `'.cat-1'` AND `'.cat-2'`)
*
* @example <caption>Example: Setting "and" toggle logic</caption>
* var mixer = mixitup(containerEl, {
* controls: {
* toggleLogic: 'and'
* }
* });
*
* @name toggleLogic
* @memberof mixitup.Config.controls
* @instance
* @type {string}
* @default 'or'
*/
this.toggleLogic = 'or'; // enum: ['or', 'and']
/**
* A string dictating the filter behavior when all toggles are inactive.
*
* When set to `'all'` (default behavior), *all* targets will be shown by default
* when no toggles are active, or at the moment all active toggles are toggled off.
*
* When set to `'none'`, no targets will be shown by default when no toggles are
* active, or at the moment all active toggles are toggled off.
*
* @example <caption>Example 1: Setting the default toggle behavior to `'all'`</caption>
* var mixer = mixitup(containerEl, {
* controls: {
* toggleDefault: 'all'
* }
* });
*
* mixer.toggleOn('.cat-2')
* .then(function() {
* // Deactivate all active toggles
*
* return mixer.toggleOff('.cat-2')
* })
* .then(function(state) {
* console.log(state.activeFilter.selector); // 'all'
* console.log(state.totalShow); // 12
* });
*
* @example <caption>Example 2: Setting the default toggle behavior to `'none'`</caption>
* var mixer = mixitup(containerEl, {
* controls: {
* toggleDefault: 'none'
* }
* });
*
* mixer.toggleOn('.cat-2')
* .then(function() {
* // Deactivate all active toggles
*
* return mixer.toggleOff('.cat-2')
* })
* .then(function(state) {
* console.log(state.activeFilter.selector); // 'none'
* console.log(state.totalShow); // 0
* });
*
* @name toggleDefault
* @memberof mixitup.Config.controls
* @instance
* @type {string}
* @default 'all'
*/
this.toggleDefault = 'all'; // enum: ['all', 'none']
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigControls);
mixitup.ConfigControls.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigControls.prototype.constructor = mixitup.ConfigControls;
/**
* A group of properties defining the output and structure of class names programmatically
* added to controls and containers to reflect the state of the mixer.
*
* Most commonly, class names are added to controls by MixItUp to indicate that
* the control is active so that it can be styled accordingly - `'mixitup-control-active'` by default.
*
* Using a "BEM" like structure, each classname is broken into the three parts:
* a block namespace (`'mixitup'`), an element name (e.g. `'control'`), and an optional modifier
* name (e.g. `'active'`) reflecting the state of the element.
*
* By default, each part of the classname is concatenated together using single hyphens as
* delineators, but this can be easily customised to match the naming convention and style of
* your project.
*
* @constructor
* @memberof mixitup.Config
* @name classNames
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigClassNames = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* The "block" portion, or top-level namespace added to the start of any class names created by MixItUp.
*
* @example <caption>Example 1: changing the `config.classNames.block` value</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio'
* }
* });
*
* // Active control output: "portfolio-control-active"
*
* @example <caption>Example 2: Removing `config.classNames.block`</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: ''
* }
* });
*
* // Active control output: "control-active"
*
* @name block
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'mixitup'
*/
this.block = 'mixitup';
/**
* The "element" portion of the class name added to container.
*
* @name elementContainer
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'container'
*/
this.elementContainer = 'container';
/**
* The "element" portion of the class name added to filter controls.
*
* By default, all filter, sort, multimix and toggle controls take the same element value of `'control'`, but
* each type's element value can be individually overwritten to match the unique classNames of your controls as needed.
*
* @example <caption>Example 1: changing the `config.classNames.elementFilter` value</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* elementFilter: 'filter'
* }
* });
*
* // Active filter output: "mixitup-filter-active"
*
* @example <caption>Example 2: changing the `config.classNames.block` and `config.classNames.elementFilter` values</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio',
* elementFilter: 'filter'
* }
* });
*
* // Active filter output: "portfolio-filter-active"
*
* @name elementFilter
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'control'
*/
this.elementFilter = 'control';
/**
* The "element" portion of the class name added to sort controls.
*
* By default, all filter, sort, multimix and toggle controls take the same element value of `'control'`, but
* each type's element value can be individually overwritten to match the unique classNames of your controls as needed.
*
* @example <caption>Example 1: changing the `config.classNames.elementSort` value</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* elementSort: 'sort'
* }
* });
*
* // Active sort output: "mixitup-sort-active"
*
* @example <caption>Example 2: changing the `config.classNames.block` and `config.classNames.elementSort` values</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio',
* elementSort: 'sort'
* }
* });
*
* // Active sort output: "portfolio-sort-active"
*
* @name elementSort
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'control'
*/
this.elementSort = 'control';
/**
* The "element" portion of the class name added to multimix controls.
*
* By default, all filter, sort, multimix and toggle controls take the same element value of `'control'`, but
* each type's element value can be individually overwritten to match the unique classNames of your controls as needed.
*
* @example <caption>Example 1: changing the `config.classNames.elementMultimix` value</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* elementMultimix: 'multimix'
* }
* });
*
* // Active multimix output: "mixitup-multimix-active"
*
* @example <caption>Example 2: changing the `config.classNames.block` and `config.classNames.elementMultimix` values</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio',
* elementSort: 'multimix'
* }
* });
*
* // Active multimix output: "portfolio-multimix-active"
*
* @name elementMultimix
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'control'
*/
this.elementMultimix = 'control';
/**
* The "element" portion of the class name added to toggle controls.
*
* By default, all filter, sort, multimix and toggle controls take the same element value of `'control'`, but
* each type's element value can be individually overwritten to match the unique classNames of your controls as needed.
*
* @example <caption>Example 1: changing the `config.classNames.elementToggle` value</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* elementToggle: 'toggle'
* }
* });
*
* // Active toggle output: "mixitup-toggle-active"
*
* @example <caption>Example 2: changing the `config.classNames.block` and `config.classNames.elementToggle` values</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio',
* elementToggle: 'toggle'
* }
* });
*
* // Active toggle output: "portfolio-toggle-active"
*
* @name elementToggle
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'control'
*/
this.elementToggle = 'control';
/**
* The "modifier" portion of the class name added to active controls.
* @name modifierActive
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'active'
*/
this.modifierActive = 'active';
/**
* The "modifier" portion of the class name added to disabled controls.
*
* @name modifierDisabled
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'disabled'
*/
this.modifierDisabled = 'disabled';
/**
* The "modifier" portion of the class name added to the container when in a "failed" state.
*
* @name modifierFailed
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default 'failed'
*/
this.modifierFailed = 'failed';
/**
* The delineator used between the "block" and "element" portions of any class name added by MixItUp.
*
* If the block portion is ommited by setting it to an empty string, no delineator will be added.
*
* @example <caption>Example: changing the delineator to match BEM convention</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* delineatorElement: '__'
* }
* });
*
* // example active control output: "mixitup__control-active"
*
* @name delineatorElement
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default '-'
*/
this.delineatorElement = '-';
/**
* The delineator used between the "element" and "modifier" portions of any class name added by MixItUp.
*
* If the element portion is ommited by setting it to an empty string, no delineator will be added.
*
* @example <caption>Example: changing both delineators to match BEM convention</caption>
* var mixer = mixitup(containerEl, {
* classNames: {
* delineatorElement: '__'
* delineatorModifier: '--'
* }
* });
*
* // Active control output: "mixitup__control--active"
*
* @name delineatorModifier
* @memberof mixitup.Config.classNames
* @instance
* @type {string}
* @default '-'
*/
this.delineatorModifier = '-';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigClassNames);
mixitup.ConfigClassNames.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigClassNames.prototype.constructor = mixitup.ConfigClassNames;
/**
* A group of properties relating to MixItUp's dataset API.
*
* @constructor
* @memberof mixitup.Config
* @name data
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigData = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A string specifying the name of the key containing your data model's unique
* identifier (UID). To use the dataset API, a UID key must be specified and
* be present and unique on all objects in the dataset you provide to MixItUp.
*
* For example, if your dataset is made up of MongoDB documents, the UID
* key would be `'id'` or `'_id'`.
*
* @example <caption>Example: Setting the UID to `'id'`</caption>
* var mixer = mixitup(containerEl, {
* data: {
* uidKey: 'id'
* }
* });
*
* @name uidKey
* @memberof mixitup.Config.data
* @instance
* @type {string}
* @default ''
*/
this.uidKey = '';
/**
* A boolean dictating whether or not MixItUp should "dirty check" each object in
* your dataset for changes whenever `.dataset()` is called, and re-render any targets
* for which a change is found.
*
* Depending on the complexity of your data model, dirty checking can be expensive
* and is therefore disabled by default.
*
* NB: For changes to be detected, a new immutable instance of the edited model must be
* provided to mixitup, rather than manipulating properties on the existing instance.
* If your changes are a result of a DB write and read, you will most likely be calling
* `.dataset()` with a clean set of objects each time, so this will not be an issue.
*
* @example <caption>Example: Enabling dirty checking</caption>
*
* var myDataset = [
* {
* id: 0,
* title: "Blog Post Title 0"
* ...
* },
* {
* id: 1,
* title: "Blog Post Title 1"
* ...
* }
* ];
*
* // Instantiate a mixer with a pre-loaded dataset, and a target renderer
* // function defined
*
* var mixer = mixitup(containerEl, {
* data: {
* uidKey: 'id',
* dirtyCheck: true
* },
* load: {
* dataset: myDataset
* },
* render: {
* target: function() { ... }
* }
* });
*
* // For illustration, we will clone and edit the second object in the dataset.
* // NB: this would typically be done server-side in response to a DB update,
* and then re-queried via an API.
*
* myDataset[1] = Object.assign({}, myDataset[1]);
*
* myDataset[1].title = 'Blog Post Title 11';
*
* mixer.dataset(myDataset)
* .then(function() {
* // the target with ID "1", will be re-rendered reflecting its new title
* });
*
* @name dirtyCheck
* @memberof mixitup.Config.data
* @instance
* @type {boolean}
* @default false
*/
this.dirtyCheck = false;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigData);
mixitup.ConfigData.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigData.prototype.constructor = mixitup.ConfigData;
/**
* A group of properties allowing the toggling of various debug features.
*
* @constructor
* @memberof mixitup.Config
* @name debug
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigDebug = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A boolean dictating whether or not the mixer instance returned by the
* `mixitup()` factory function should expose private properties and methods.
*
* By default, mixer instances only expose their public API, but enabling
* debug mode will give you access to various mixer internals which may aid
* in debugging, or the authoring of extensions.
*
* @example <caption>Example: Enabling debug mode</caption>
*
* var mixer = mixitup(containerEl, {
* debug: {
* enable: true
* }
* });
*
* // Private properties and methods will now be visible on the mixer instance:
*
* console.log(mixer);
*
* @name enable
* @memberof mixitup.Config.debug
* @instance
* @type {boolean}
* @default false
*/
this.enable = false;
/**
* A boolean dictating whether or not warnings should be shown when various
* common gotchas occur.
*
* Warnings are intended to provide insights during development when something
* occurs that is not a fatal, but may indicate an issue with your integration,
* and are therefore turned on by default. However, you may wish to disable
* them in production.
*
* @example <caption>Example 1: Disabling warnings</caption>
*
* var mixer = mixitup(containerEl, {
* debug: {
* showWarnings: false
* }
* });
*
* @example <caption>Example 2: Disabling warnings based on environment</caption>
*
* var showWarnings = myAppConfig.environment === 'development' ? true : false;
*
* var mixer = mixitup(containerEl, {
* debug: {
* showWarnings: showWarnings
* }
* });
*
* @name showWarnings
* @memberof mixitup.Config.debug
* @instance
* @type {boolean}
* @default true
*/
this.showWarnings = true;
/**
* Used for server-side testing only.
*
* @private
* @name fauxAsync
* @memberof mixitup.Config.debug
* @instance
* @type {boolean}
* @default false
*/
this.fauxAsync = false;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigDebug);
mixitup.ConfigDebug.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigDebug.prototype.constructor = mixitup.ConfigDebug;
/**
* A group of properties relating to the layout of the container.
*
* @constructor
* @memberof mixitup.Config
* @name layout
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigLayout = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A boolean dictating whether or not mixitup should query all descendants
* of the container for targets, or only immediate children.
*
* By default, mixitup will query all descendants matching the
* `selectors.target` selector when indexing targets upon instantiation.
* This allows for targets to be nested inside a sub-container which is
* useful when ring-fencing targets from locally scoped controls in your
* markup (see `controls.scope`).
*
* However, if you are building a more complex UI requiring the nesting
* of mixers within mixers, you will most likely want to limit targets to
* immediate children of the container by setting this property to `false`.
*
* @example <caption>Example: Restricting targets to immediate children</caption>
*
* var mixer = mixitup(containerEl, {
* layout: {
* allowNestedTargets: false
* }
* });
*
* @name allowNestedTargets
* @memberof mixitup.Config.layout
* @instance
* @type {boolean}
* @default true
*/
this.allowNestedTargets = true;
/**
* A string specifying an optional class name to apply to the container when in
* its default state.
*
* By changing this class name or adding a class name to the container via the
* `.changeLayout()` API method, the CSS layout of the container can be changed,
* and MixItUp will attemp to gracefully animate the container and its targets
* between states.
*
* @example <caption>Example 1: Specifying a container class name</caption>
*
* var mixer = mixitup(containerEl, {
* layout: {
* containerClassName: 'grid'
* }
* });
*
* @example <caption>Example 2: Changing the default class name with `.changeLayout()`</caption>
*
* var mixer = mixitup(containerEl, {
* layout: {
* containerClassName: 'grid'
* }
* });
*
* mixer.changeLayout('list')
* .then(function(state) {
* console.log(state.activeContainerClass); // "list"
* });
*
* @name containerClassName
* @memberof mixitup.Config.layout
* @instance
* @type {string}
* @default ''
*/
this.containerClassName = '';
/**
* A reference to a non-target sibling element after which to insert targets
* when there are no targets in the container.
*
* @example <caption>Example: Setting a `siblingBefore` reference element</caption>
*
* var addButton = containerEl.querySelector('button');
*
* var mixer = mixitup(containerEl, {
* layout: {
* siblingBefore: addButton
* }
* });
*
* @name siblingBefore
* @memberof mixitup.Config.layout
* @instance
* @type {HTMLElement}
* @default null
*/
this.siblingBefore = null;
/**
* A reference to a non-target sibling element before which to insert targets
* when there are no targets in the container.
*
* @example <caption>Example: Setting an `siblingAfter` reference element</caption>
*
* var gap = containerEl.querySelector('.gap');
*
* var mixer = mixitup(containerEl, {
* layout: {
* siblingAfter: gap
* }
* });
*
* @name siblingAfter
* @memberof mixitup.Config.layout
* @instance
* @type {HTMLElement}
* @default null
*/
this.siblingAfter = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigLayout);
mixitup.ConfigLayout.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigLayout.prototype.constructor = mixitup.ConfigLayout;
/**
* A group of properties defining the initial state of the mixer on load (instantiation).
*
* @constructor
* @memberof mixitup.Config
* @name load
* @namespace
* @public
* @since 2.0.0
*/
mixitup.ConfigLoad = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A string defining any filtering to be statically applied to the mixer on load.
* As per the `.filter()` API, this can be any valid selector string, or the
* values `'all'` or `'none'`.
*
* @example <caption>Example 1: Defining an initial filter selector to be applied on load</caption>
*
* // The mixer will show only those targets matching '.category-a' on load.
*
* var mixer = mixitup(containerEl, {
* load: {
* filter: '.category-a'
* }
* });
*
* @example <caption>Example 2: Hiding all targets on load</caption>
*
* // The mixer will show hide all targets on load.
*
* var mixer = mixitup(containerEl, {
* load: {
* filter: 'none'
* }
* });
*
* @name filter
* @memberof mixitup.Config.load
* @instance
* @type {string}
* @default 'all'
*/
this.filter = 'all';
/**
* A string defining any sorting to be statically applied to the mixer on load.
* As per the `.sort()` API, this should be a valid "sort string" made up of
* an attribute to sort by (or `'default'`) followed by an optional sorting
* order, or the value `'random'`;
*
* @example <caption>Example: Defining sorting to be applied on load</caption>
*
* // The mixer will sort the container by the value of the `data-published-date`
* // attribute, in descending order.
*
* var mixer = mixitup(containerEl, {
* load: {
* sort: 'published-date:desc'
* }
* });
*
* @name sort
* @memberof mixitup.Config.load
* @instance
* @type {string}
* @default 'default:asc'
*/
this.sort = 'default:asc';
/**
* An array of objects representing the underlying data of any pre-rendered targets,
* when using the `.dataset()` API.
*
* NB: If targets are pre-rendered when the mixer is instantiated, this must be set.
*
* @example <caption>Example: Defining the initial underyling dataset</caption>
*
* var myDataset = [
* {
* id: 0,
* title: "Blog Post Title 0",
* ...
* },
* {
* id: 1,
* title: "Blog Post Title 1",
* ...
* }
* ];
*
* var mixer = mixitup(containerEl, {
* data: {
* uidKey: 'id'
* },
* load: {
* dataset: myDataset
* }
* });
*
* @name dataset
* @memberof mixitup.Config.load
* @instance
* @type {Array.<object>}
* @default null
*/
this.dataset = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigLoad);
mixitup.ConfigLoad.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigLoad.prototype.constructor = mixitup.ConfigLoad;
/**
* A group of properties defining the selectors used to query elements within a mixitup container.
*
* @constructor
* @memberof mixitup.Config
* @name selectors
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigSelectors = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A selector string used to query and index target elements within the container.
*
* By default, the class selector `'.mix'` is used, but this can be changed to an
* attribute or element selector to match the style of your project.
*
* @example <caption>Example 1: Changing the target selector</caption>
*
* var mixer = mixitup(containerEl, {
* selectors: {
* target: '.portfolio-item'
* }
* });
*
* @example <caption>Example 2: Using an attribute selector as a target selector</caption>
*
* // The mixer will search for any children with the attribute `data-ref="mix"`
*
* var mixer = mixitup(containerEl, {
* selectors: {
* target: '[data-ref="mix"]'
* }
* });
*
* @name target
* @memberof mixitup.Config.selectors
* @instance
* @type {string}
* @default '.mix'
*/
this.target = '.mix';
/**
* A optional selector string used to add further specificity to the querying of control elements,
* in addition to their mandatory data attribute (e.g. `data-filter`, `data-toggle`, `data-sort`).
*
* This can be used if other elements in your document must contain the above attributes
* (e.g. for use in third-party scripts), and would otherwise interfere with MixItUp. Adding
* an additional `control` selector of your choice allows MixItUp to restrict event handling
* to only those elements matching the defined selector.
*
* @name control
* @memberof mixitup.Config.selectors
* @instance
* @type {string}
* @default ''
*
* @example <caption>Example 1: Adding a `selectors.control` selector</caption>
*
* var mixer = mixitup(containerEl, {
* selectors: {
* control: '.mixitup-control'
* }
* });
*
* // Will not be handled:
* // <button data-filter=".category-a"></button>
*
* // Will be handled:
* // <button class="mixitup-control" data-filter=".category-a"></button>
*/
this.control = '';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigSelectors);
mixitup.ConfigSelectors.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigSelectors.prototype.constructor = mixitup.ConfigSelectors;
/**
* A group of optional render functions for creating and updating elements.
*
* All render functions receive a data object, and should return a valid HTML string.
*
* @constructor
* @memberof mixitup.Config
* @name render
* @namespace
* @public
* @since 3.0.0
*/
mixitup.ConfigRender = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A function returning an HTML string representing a target element, or a reference to a
* single DOM element.
*
* The function is invoked as part of the `.dataset()` API, whenever a new item is added
* to the dataset, or an item in the dataset changes (if `dataset.dirtyCheck` is enabled).
*
* The function receives the relevant dataset item as its first parameter.
*
* @example <caption>Example 1: Using string concatenation</caption>
*
* var mixer = mixitup(containerEl, {
* render: {
* target: function(item) {
* return (
* '&lt;div class="mix"&gt;' +
* '&lt;h2&gt;' + item.title + '&lt;/h2&gt;' +
* '&lt;/div&gt;'
* );
* }
* }
* });
*
* @example <caption>Example 2: Using an ES2015 template literal</caption>
*
* var mixer = mixitup(containerEl, {
* render: {
* target: function(item) {
* return (
* `&lt;div class="mix"&gt;
* &lt;h2&gt;${item.title}&lt;/h2&gt;
* &lt;/div&gt;`
* );
* }
* }
* });
*
* @example <caption>Example 3: Using a Handlebars template</caption>
*
* var targetTemplate = Handlebars.compile('&lt;div class="mix"&gt;&lt;h2&gt;{{title}}&lt;/h2&gt;&lt;/div&gt;');
*
* var mixer = mixitup(containerEl, {
* render: {
* target: targetTemplate
* }
* });
*
* @example <caption>Example 4: Returning a DOM element</caption>
*
* var mixer = mixitup(containerEl, {
* render: {
* target: function(item) {
* // Create a single element using your framework's built-in renderer
*
* var el = ...
*
* return el;
* }
* }
* });
*
* @name target
* @memberof mixitup.Config.render
* @instance
* @type {function}
* @default 'null'
*/
this.target = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigRender);
mixitup.ConfigRender.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigRender.prototype.constructor = mixitup.ConfigRender;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.ConfigTemplates = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ConfigTemplates);
mixitup.ConfigTemplates.prototype = Object.create(mixitup.Base.prototype);
mixitup.ConfigTemplates.prototype.constructor = mixitup.ConfigTemplates;
/**
* `mixitup.Config` is an interface used for customising the functionality of a
* mixer instance. It is organised into several semantically distinct sub-objects,
* each one pertaining to a particular aspect of MixItUp functionality.
*
* An object literal containing any or all of the available properies,
* known as the "configuration object", can be passed as the second parameter to
* the `mixitup` factory function when creating a mixer instance to customise its
* functionality as needed.
*
* If no configuration object is passed, the mixer instance will take on the default
* configuration values detailed below.
*
* @example <caption>Example 1: Creating and passing the configuration object</caption>
* // Create a configuration object with desired values
*
* var config = {
* animation: {
* enable: false
* },
* selectors: {
* target: '.item'
* }
* };
*
* // Pass the configuration object to the mixitup factory function
*
* var mixer = mixitup(containerEl, config);
*
* @example <caption>Example 2: Passing the configuration object inline</caption>
* // Typically, the configuration object is passed inline for brevity.
*
* var mixer = mixitup(containerEl, {
* controls: {
* live: true,
* toggleLogic: 'and'
* }
* });
*
*
* @constructor
* @memberof mixitup
* @namespace
* @public
* @since 2.0.0
*/
mixitup.Config = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.animation = new mixitup.ConfigAnimation();
this.behavior = new mixitup.ConfigBehavior();
this.callbacks = new mixitup.ConfigCallbacks();
this.controls = new mixitup.ConfigControls();
this.classNames = new mixitup.ConfigClassNames();
this.data = new mixitup.ConfigData();
this.debug = new mixitup.ConfigDebug();
this.layout = new mixitup.ConfigLayout();
this.load = new mixitup.ConfigLoad();
this.selectors = new mixitup.ConfigSelectors();
this.render = new mixitup.ConfigRender();
this.templates = new mixitup.ConfigTemplates();
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Config);
mixitup.Config.prototype = Object.create(mixitup.Base.prototype);
mixitup.Config.prototype.constructor = mixitup.Config;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.MixerDom = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.document = null;
this.body = null;
this.container = null;
this.parent = null;
this.targets = [];
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.MixerDom);
mixitup.MixerDom.prototype = Object.create(mixitup.Base.prototype);
mixitup.MixerDom.prototype.constructor = mixitup.MixerDom;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.UiClassNames = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.base = '';
this.active = '';
this.disabled = '';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.UiClassNames);
mixitup.UiClassNames.prototype = Object.create(mixitup.Base.prototype);
mixitup.UiClassNames.prototype.constructor = mixitup.UiClassNames;
/**
* An object into which all arbitrary arguments sent to '.dataset()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandDataset = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.dataset = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandDataset);
mixitup.CommandDataset.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandDataset.prototype.constructor = mixitup.CommandDataset;
/**
* An object into which all arbitrary arguments sent to '.multimix()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandMultimix = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.filter = null;
this.sort = null;
this.insert = null;
this.remove = null;
this.changeLayout = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandMultimix);
mixitup.CommandMultimix.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandMultimix.prototype.constructor = mixitup.CommandMultimix;
/**
* An object into which all arbitrary arguments sent to '.filter()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandFilter = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.selector = '';
this.collection = null;
this.action = 'show'; // enum: ['show', 'hide']
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandFilter);
mixitup.CommandFilter.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandFilter.prototype.constructor = mixitup.CommandFilter;
/**
* An object into which all arbitrary arguments sent to '.sort()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandSort = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.sortString = '';
this.attribute = '';
this.order = 'asc';
this.collection = null;
this.next = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandSort);
mixitup.CommandSort.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandSort.prototype.constructor = mixitup.CommandSort;
/**
* An object into which all arbitrary arguments sent to '.insert()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandInsert = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.index = 0;
this.collection = [];
this.position = 'before'; // enum: ['before', 'after']
this.sibling = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandInsert);
mixitup.CommandInsert.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandInsert.prototype.constructor = mixitup.CommandInsert;
/**
* An object into which all arbitrary arguments sent to '.remove()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandRemove = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.targets = [];
this.collection = [];
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandRemove);
mixitup.CommandRemove.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandRemove.prototype.constructor = mixitup.CommandRemove;
/**
* An object into which all arbitrary arguments sent to '.changeLayout()' are mapped.
*
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.CommandChangeLayout = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.containerClassName = '';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.CommandChangeLayout);
mixitup.CommandChangeLayout.prototype = Object.create(mixitup.Base.prototype);
mixitup.CommandChangeLayout.prototype.constructor = mixitup.CommandChangeLayout;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
* @param {string} type
* @param {string} selector
* @param {boolean} [live]
* @param {string} [parent]
* An optional string representing the name of the mixer.dom property containing a reference to a parent element.
*/
mixitup.ControlDefinition = function(type, selector, live, parent) {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.type = type;
this.selector = selector;
this.live = live || false;
this.parent = parent || '';
this.callActions('afterConstruct');
h.freeze(this);
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.ControlDefinition);
mixitup.ControlDefinition.prototype = Object.create(mixitup.Base.prototype);
mixitup.ControlDefinition.prototype.constructor = mixitup.ControlDefinition;
mixitup.controlDefinitions = [];
mixitup.controlDefinitions.push(new mixitup.ControlDefinition('multimix', '[data-filter][data-sort]'));
mixitup.controlDefinitions.push(new mixitup.ControlDefinition('filter', '[data-filter]'));
mixitup.controlDefinitions.push(new mixitup.ControlDefinition('sort', '[data-sort]'));
mixitup.controlDefinitions.push(new mixitup.ControlDefinition('toggle', '[data-toggle]'));
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Control = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.el = null;
this.selector = '';
this.bound = [];
this.pending = -1;
this.type = '';
this.status = 'inactive'; // enum: ['inactive', 'active', 'disabled', 'live']
this.filter = '';
this.sort = '';
this.canDisable = false;
this.handler = null;
this.classNames = new mixitup.UiClassNames();
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Control);
mixitup.Control.prototype = Object.create(mixitup.Base.prototype);
h.extend(mixitup.Control.prototype,
/** @lends mixitup.Control */
{
constructor: mixitup.Control,
/**
* @private
* @param {HTMLElement} el
* @param {string} type
* @param {string} selector
*/
init: function(el, type, selector) {
var self = this;
this.callActions('beforeInit', arguments);
self.el = el;
self.type = type;
self.selector = selector;
if (self.selector) {
self.status = 'live';
} else {
self.canDisable = typeof self.el.disable === 'boolean';
switch (self.type) {
case 'filter':
self.filter = self.el.getAttribute('data-filter');
break;
case 'toggle':
self.filter = self.el.getAttribute('data-toggle');
break;
case 'sort':
self.sort = self.el.getAttribute('data-sort');
break;
case 'multimix':
self.filter = self.el.getAttribute('data-filter');
self.sort = self.el.getAttribute('data-sort');
break;
}
}
self.bindClick();
mixitup.controls.push(self);
this.callActions('afterInit', arguments);
},
/**
* @private
* @param {mixitup.Mixer} mixer
* @return {boolean}
*/
isBound: function(mixer) {
var self = this,
isBound = false;
this.callActions('beforeIsBound', arguments);
isBound = self.bound.indexOf(mixer) > -1;
return self.callFilters('afterIsBound', isBound, arguments);
},
/**
* @private
* @param {mixitup.Mixer} mixer
* @return {void}
*/
addBinding: function(mixer) {
var self = this;
this.callActions('beforeAddBinding', arguments);
if (!self.isBound()) {
self.bound.push(mixer);
}
this.callActions('afterAddBinding', arguments);
},
/**
* @private
* @param {mixitup.Mixer} mixer
* @return {void}
*/
removeBinding: function(mixer) {
var self = this,
removeIndex = -1;
this.callActions('beforeRemoveBinding', arguments);
if ((removeIndex = self.bound.indexOf(mixer)) > -1) {
self.bound.splice(removeIndex, 1);
}
if (self.bound.length < 1) {
// No bindings exist, unbind event click handlers
self.unbindClick();
// Remove from `mixitup.controls` list
removeIndex = mixitup.controls.indexOf(self);
mixitup.controls.splice(removeIndex, 1);
if (self.status === 'active') {
self.renderStatus(self.el, 'inactive');
}
}
this.callActions('afterRemoveBinding', arguments);
},
/**
* @private
* @return {void}
*/
bindClick: function() {
var self = this;
this.callActions('beforeBindClick', arguments);
self.handler = function(e) {
self.handleClick(e);
};
h.on(self.el, 'click', self.handler);
this.callActions('afterBindClick', arguments);
},
/**
* @private
* @return {void}
*/
unbindClick: function() {
var self = this;
this.callActions('beforeUnbindClick', arguments);
h.off(self.el, 'click', self.handler);
self.handler = null;
this.callActions('afterUnbindClick', arguments);
},
/**
* @private
* @param {MouseEvent} e
* @return {void}
*/
handleClick: function(e) {
var self = this,
button = null,
mixer = null,
isActive = false,
returnValue = void(0),
command = {},
clone = null,
commands = [],
i = -1;
this.callActions('beforeHandleClick', arguments);
this.pending = 0;
mixer = self.bound[0];
if (!self.selector) {
button = self.el;
} else {
button = h.closestParent(e.target, mixer.config.selectors.control + self.selector, true, mixer.dom.document);
}
if (!button) {
self.callActions('afterHandleClick', arguments);
return;
}
switch (self.type) {
case 'filter':
command.filter = self.filter || button.getAttribute('data-filter');
break;
case 'sort':
command.sort = self.sort || button.getAttribute('data-sort');
break;
case 'multimix':
command.filter = self.filter || button.getAttribute('data-filter');
command.sort = self.sort || button.getAttribute('data-sort');
break;
case 'toggle':
command.filter = self.filter || button.getAttribute('data-toggle');
if (self.status === 'live') {
isActive = h.hasClass(button, self.classNames.active);
} else {
isActive = self.status === 'active';
}
break;
}
for (i = 0; i < self.bound.length; i++) {
// Create a clone of the command for each bound mixer instance
clone = new mixitup.CommandMultimix();
h.extend(clone, command);
commands.push(clone);
}
commands = self.callFilters('commandsHandleClick', commands, arguments);
self.pending = self.bound.length;
for (i = 0; mixer = self.bound[i]; i++) {
command = commands[i];
if (!command) {
// An extension may set a command null to indicate that the click should not be handled
continue;
}
if (!mixer.lastClicked) {
mixer.lastClicked = button;
}
mixitup.events.fire('mixClick', mixer.dom.container, {
state: mixer.state,
instance: mixer,
originalEvent: e,
control: mixer.lastClicked
}, mixer.dom.document);
if (typeof mixer.config.callbacks.onMixClick === 'function') {
returnValue = mixer.config.callbacks.onMixClick.call(mixer.lastClicked, mixer.state, e, mixer);
if (returnValue === false) {
// User has returned `false` from the callback, so do not handle click
continue;
}
}
if (self.type === 'toggle') {
isActive ? mixer.toggleOff(command.filter) : mixer.toggleOn(command.filter);
} else {
mixer.multimix(command);
}
}
this.callActions('afterHandleClick', arguments);
},
/**
* @param {object} command
* @param {Array<string>} toggleArray
* @return {void}
*/
update: function(command, toggleArray) {
var self = this,
actions = new mixitup.CommandMultimix();
self.callActions('beforeUpdate', arguments);
self.pending--;
self.pending = Math.max(0, self.pending);
if (self.pending > 0) return;
if (self.status === 'live') {
// Live control (status unknown)
self.updateLive(command, toggleArray);
} else {
// Static control
actions.sort = self.sort;
actions.filter = self.filter;
self.callFilters('actionsUpdate', actions, arguments);
self.parseStatusChange(self.el, command, actions, toggleArray);
}
self.callActions('afterUpdate', arguments);
},
/**
* @param {mixitup.CommandMultimix} command
* @param {Array<string>} toggleArray
* @return {void}
*/
updateLive: function(command, toggleArray) {
var self = this,
controlButtons = null,
actions = null,
button = null,
i = -1;
self.callActions('beforeUpdateLive', arguments);
if (!self.el) return;
controlButtons = self.el.querySelectorAll(self.selector);
for (i = 0; button = controlButtons[i]; i++) {
actions = new mixitup.CommandMultimix();
switch (self.type) {
case 'filter':
actions.filter = button.getAttribute('data-filter');
break;
case 'sort':
actions.sort = button.getAttribute('data-sort');
break;
case 'multimix':
actions.filter = button.getAttribute('data-filter');
actions.sort = button.getAttribute('data-sort');
break;
case 'toggle':
actions.filter = button.getAttribute('data-toggle');
break;
}
actions = self.callFilters('actionsUpdateLive', actions, arguments);
self.parseStatusChange(button, command, actions, toggleArray);
}
self.callActions('afterUpdateLive', arguments);
},
/**
* @param {HTMLElement} button
* @param {mixitup.CommandMultimix} command
* @param {mixitup.CommandMultimix} actions
* @param {Array<string>} toggleArray
* @return {void}
*/
parseStatusChange: function(button, command, actions, toggleArray) {
var self = this,
alias = '',
toggle = '',
i = -1;
self.callActions('beforeParseStatusChange', arguments);
switch (self.type) {
case 'filter':
if (command.filter === actions.filter) {
self.renderStatus(button, 'active');
} else {
self.renderStatus(button, 'inactive');
}
break;
case 'multimix':
if (command.sort === actions.sort && command.filter === actions.filter) {
self.renderStatus(button, 'active');
} else {
self.renderStatus(button, 'inactive');
}
break;
case 'sort':
if (command.sort.match(/:asc/g)) {
alias = command.sort.replace(/:asc/g, '');
}
if (command.sort === actions.sort || alias === actions.sort) {
self.renderStatus(button, 'active');
} else {
self.renderStatus(button, 'inactive');
}
break;
case 'toggle':
if (toggleArray.length < 1) self.renderStatus(button, 'inactive');
if (command.filter === actions.filter) {
self.renderStatus(button, 'active');
}
for (i = 0; i < toggleArray.length; i++) {
toggle = toggleArray[i];
if (toggle === actions.filter) {
// Button matches one active toggle
self.renderStatus(button, 'active');
break;
}
self.renderStatus(button, 'inactive');
}
break;
}
self.callActions('afterParseStatusChange', arguments);
},
/**
* @param {HTMLElement} button
* @param {string} status
* @return {void}
*/
renderStatus: function(button, status) {
var self = this;
self.callActions('beforeRenderStatus', arguments);
switch (status) {
case 'active':
h.addClass(button, self.classNames.active);
h.removeClass(button, self.classNames.disabled);
if (self.canDisable) self.el.disabled = false;
break;
case 'inactive':
h.removeClass(button, self.classNames.active);
h.removeClass(button, self.classNames.disabled);
if (self.canDisable) self.el.disabled = false;
break;
case 'disabled':
if (self.canDisable) self.el.disabled = true;
h.addClass(button, self.classNames.disabled);
h.removeClass(button, self.classNames.active);
break;
}
if (self.status !== 'live') {
// Update the control's status propery if not live
self.status = status;
}
self.callActions('afterRenderStatus', arguments);
}
});
mixitup.controls = [];
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.StyleData = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.x = 0;
this.y = 0;
this.top = 0;
this.right = 0;
this.bottom = 0;
this.left = 0;
this.width = 0;
this.height = 0;
this.marginRight = 0;
this.marginBottom = 0;
this.opacity = 0;
this.scale = new mixitup.TransformData();
this.translateX = new mixitup.TransformData();
this.translateY = new mixitup.TransformData();
this.translateZ = new mixitup.TransformData();
this.rotateX = new mixitup.TransformData();
this.rotateY = new mixitup.TransformData();
this.rotateZ = new mixitup.TransformData();
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.StyleData);
mixitup.StyleData.prototype = Object.create(mixitup.Base.prototype);
mixitup.StyleData.prototype.constructor = mixitup.StyleData;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.TransformData = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.value = 0;
this.unit = '';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.TransformData);
mixitup.TransformData.prototype = Object.create(mixitup.Base.prototype);
mixitup.TransformData.prototype.constructor = mixitup.TransformData;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.TransformDefaults = function() {
mixitup.StyleData.apply(this);
this.callActions('beforeConstruct');
this.scale.value = 0.01;
this.scale.unit = '';
this.translateX.value = 20;
this.translateX.unit = 'px';
this.translateY.value = 20;
this.translateY.unit = 'px';
this.translateZ.value = 20;
this.translateZ.unit = 'px';
this.rotateX.value = 90;
this.rotateX.unit = 'deg';
this.rotateY.value = 90;
this.rotateY.unit = 'deg';
this.rotateX.value = 90;
this.rotateX.unit = 'deg';
this.rotateZ.value = 180;
this.rotateZ.unit = 'deg';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.TransformDefaults);
mixitup.TransformDefaults.prototype = Object.create(mixitup.StyleData.prototype);
mixitup.TransformDefaults.prototype.constructor = mixitup.TransformDefaults;
/**
* @private
* @static
* @since 3.0.0
* @type {mixitup.TransformDefaults}
*/
mixitup.transformDefaults = new mixitup.TransformDefaults();
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.EventDetail = function() {
this.state = null;
this.futureState = null;
this.instance = null;
this.originalEvent = null;
};
/**
* The `mixitup.Events` class contains all custom events dispatched by MixItUp at various
* points within the lifecycle of a mixer operation.
*
* Each event is analogous to the callback function of the same name defined in
* the `callbacks` configuration object, and is triggered immediately before it.
*
* Events are always triggered from the container element on which MixItUp is instantiated
* upon.
*
* As with any event, registered event handlers receive the event object as a parameter
* which includes a `detail` property containting references to the current `state`,
* the `mixer` instance, and other event-specific properties described below.
*
* @constructor
* @namespace
* @memberof mixitup
* @public
* @since 3.0.0
*/
mixitup.Events = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* A custom event triggered immediately after any MixItUp operation is requested
* and before animations have begun.
*
* The `mixStart` event also exposes a `futureState` property via the
* `event.detail` object, which represents the final state of the mixer once
* the requested operation has completed.
*
* @name mixStart
* @memberof mixitup.Events
* @static
* @type {CustomEvent}
*/
this.mixStart = null;
/**
* A custom event triggered when a MixItUp operation is requested while another
* operation is in progress, and the animation queue is full, or queueing
* is disabled.
*
* @name mixBusy
* @memberof mixitup.Events
* @static
* @type {CustomEvent}
*/
this.mixBusy = null;
/**
* A custom event triggered after any MixItUp operation has completed, and the
* state has been updated.
*
* @name mixEnd
* @memberof mixitup.Events
* @static
* @type {CustomEvent}
*/
this.mixEnd = null;
/**
* A custom event triggered whenever a filter operation "fails", i.e. no targets
* could be found matching the requested filter.
*
* @name mixFail
* @memberof mixitup.Events
* @static
* @type {CustomEvent}
*/
this.mixFail = null;
/**
* A custom event triggered whenever a MixItUp control is clicked, and before its
* respective operation is requested.
*
* This event also exposes an `originalEvent` property via the `event.detail`
* object, which holds a reference to the original click event.
*
* @name mixClick
* @memberof mixitup.Events
* @static
* @type {CustomEvent}
*/
this.mixClick = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Events);
mixitup.Events.prototype = Object.create(mixitup.Base.prototype);
mixitup.Events.prototype.constructor = mixitup.Events;
/**
* @private
* @param {string} eventType
* @param {Element} el
* @param {object} detail
* @param {Document} [doc]
*/
mixitup.Events.prototype.fire = function(eventType, el, detail, doc) {
var self = this,
event = null,
eventDetail = new mixitup.EventDetail();
self.callActions('beforeFire', arguments);
if (typeof self[eventType] === 'undefined') {
throw new Error('Event type "' + eventType + '" not found.');
}
eventDetail.state = new mixitup.State();
h.extend(eventDetail.state, detail.state);
if (detail.futureState) {
eventDetail.futureState = new mixitup.State();
h.extend(eventDetail.futureState, detail.futureState);
}
eventDetail.instance = detail.instance;
if (detail.originalEvent) {
eventDetail.originalEvent = detail.originalEvent;
}
event = h.getCustomEvent(eventType, eventDetail, doc);
self.callFilters('eventFire', event, arguments);
el.dispatchEvent(event);
};
// Asign a singleton instance to `mixitup.events`:
mixitup.events = new mixitup.Events();
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.QueueItem = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.args = [];
this.instruction = null;
this.triggerElement = null;
this.deferred = null;
this.isToggling = false;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.QueueItem);
mixitup.QueueItem.prototype = Object.create(mixitup.Base.prototype);
mixitup.QueueItem.prototype.constructor = mixitup.QueueItem;
/**
* The `mixitup.Mixer` class is used to hold discreet, user-configured
* instances of MixItUp on a provided container element.
*
* Mixer instances are returned whenever the `mixitup()` factory function is called,
* which expose a range of methods enabling API-based filtering, sorting,
* insertion, removal and more.
*
* @constructor
* @namespace
* @memberof mixitup
* @public
* @since 3.0.0
*/
mixitup.Mixer = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.config = new mixitup.Config();
this.id = '';
this.isBusy = false;
this.isToggling = false;
this.incPadding = true;
this.controls = [];
this.targets = [];
this.origOrder = [];
this.cache = {};
this.toggleArray = [];
this.targetsMoved = 0;
this.targetsImmovable = 0;
this.targetsBound = 0;
this.targetsDone = 0;
this.staggerDuration = 0;
this.effectsIn = null;
this.effectsOut = null;
this.transformIn = [];
this.transformOut = [];
this.queue = [];
this.state = null;
this.lastOperation = null;
this.lastClicked = null;
this.userCallback = null;
this.userDeferred = null;
this.dom = new mixitup.MixerDom();
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Mixer);
mixitup.Mixer.prototype = Object.create(mixitup.Base.prototype);
h.extend(mixitup.Mixer.prototype,
/** @lends mixitup.Mixer */
{
constructor: mixitup.Mixer,
/**
* @private
* @instance
* @since 3.0.0
* @param {HTMLElement} container
* @param {HTMLElement} document
* @param {string} id
* @param {object} [config]
*/
attach: function(container, document, id, config) {
var self = this,
target = null,
i = -1;
self.callActions('beforeAttach', arguments);
self.id = id;
if (config) {
h.extend(self.config, config, true, true);
}
self.sanitizeConfig();
self.cacheDom(container, document);
if (self.config.layout.containerClassName) {
h.addClass(self.dom.container, self.config.layout.containerClassName);
}
if (!mixitup.features.has.transitions) {
self.config.animation.enable = false;
}
if (typeof window.console === 'undefined') {
self.config.debug.showWarnings = false;
}
if (self.config.data.uidKey) {
// If the dataset API is in use, force disable controls
self.config.controls.enable = false;
}
self.indexTargets();
self.state = self.getInitialState();
for (i = 0; target = self.lastOperation.toHide[i]; i++) {
target.hide();
}
if (self.config.controls.enable) {
self.initControls();
self.buildToggleArray(null, self.state);
self.updateControls({
filter: self.state.activeFilter,
sort: self.state.activeSort
});
}
self.parseEffects();
self.callActions('afterAttach', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
sanitizeConfig: function() {
var self = this;
self.callActions('beforeSanitizeConfig', arguments);
// Sanitize enum/string config options
self.config.controls.scope = self.config.controls.scope.toLowerCase().trim();
self.config.controls.toggleLogic = self.config.controls.toggleLogic.toLowerCase().trim();
self.config.controls.toggleDefault = self.config.controls.toggleDefault.toLowerCase().trim();
self.config.animation.effects = self.config.animation.effects.trim();
self.callActions('afterSanitizeConfig', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {mixitup.State}
*/
getInitialState: function() {
var self = this,
state = new mixitup.State(),
operation = new mixitup.Operation();
self.callActions('beforeGetInitialState', arguments);
// Map initial values into a mock state object in order to construct an operation
state.activeContainerClassName = self.config.layout.containerClassName;
if (self.config.load.dataset) {
// Dataset API
if (!self.config.data.uidKey || typeof self.config.data.uidKey !== 'string') {
throw new TypeError(mixitup.messages.errorConfigDataUidKeyNotSet());
}
operation.startDataset = operation.newDataset = state.activeDataset = self.config.load.dataset.slice();
operation.startContainerClassName = operation.newContainerClassName = state.activeContainerClassName;
operation.show = self.targets.slice();
state = self.callFilters('stateGetInitialState', state, arguments);
} else {
// DOM API
state.activeFilter = self.parseFilterArgs([self.config.load.filter]).command;
state.activeSort = self.parseSortArgs([self.config.load.sort]).command;
state.totalTargets = self.targets.length;
state = self.callFilters('stateGetInitialState', state, arguments);
if (
state.activeSort.collection || state.activeSort.attribute ||
state.activeSort.order === 'random' || state.activeSort.order === 'desc'
) {
// Sorting on load
operation.newSort = state.activeSort;
self.sortOperation(operation);
self.printSort(false, operation);
self.targets = operation.newOrder;
} else {
operation.startOrder = operation.newOrder = self.targets;
}
operation.startFilter = operation.newFilter = state.activeFilter;
operation.startSort = operation.newSort = state.activeSort;
operation.startContainerClassName = operation.newContainerClassName = state.activeContainerClassName;
if (operation.newFilter.selector === 'all') {
operation.newFilter.selector = self.config.selectors.target;
} else if (operation.newFilter.selector === 'none') {
operation.newFilter.selector = '';
}
}
operation = self.callFilters('operationGetInitialState', operation, [state]);
self.lastOperation = operation;
if (operation.newFilter) {
self.filterOperation(operation);
}
state = self.buildState(operation);
return state;
},
/**
* Caches references of DOM elements neccessary for the mixer's functionality.
*
* @private
* @instance
* @since 3.0.0
* @param {HTMLElement} el
* @param {HTMLHtmlElement} document
* @return {void}
*/
cacheDom: function(el, document) {
var self = this;
self.callActions('beforeCacheDom', arguments);
self.dom.document = document;
self.dom.body = self.dom.document.querySelector('body');
self.dom.container = el;
self.dom.parent = el;
self.callActions('afterCacheDom', arguments);
},
/**
* Indexes all child elements of the mixer matching the `selectors.target`
* selector, instantiating a mixitup.Target for each one.
*
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
indexTargets: function() {
var self = this,
target = null,
el = null,
dataset = null,
i = -1;
self.callActions('beforeIndexTargets', arguments);
self.dom.targets = self.config.layout.allowNestedTargets ?
self.dom.container.querySelectorAll(self.config.selectors.target) :
h.children(self.dom.container, self.config.selectors.target, self.dom.document);
self.dom.targets = h.arrayFromList(self.dom.targets);
self.targets = [];
if ((dataset = self.config.load.dataset) && dataset.length !== self.dom.targets.length) {
throw new Error(mixitup.messages.errorDatasetPrerenderedMismatch());
}
if (self.dom.targets.length) {
for (i = 0; el = self.dom.targets[i]; i++) {
target = new mixitup.Target();
target.init(el, self, dataset ? dataset[i] : void(0));
target.isInDom = true;
self.targets.push(target);
}
self.dom.parent = self.dom.targets[0].parentElement === self.dom.container ?
self.dom.container :
self.dom.targets[0].parentElement;
}
self.origOrder = self.targets;
self.callActions('afterIndexTargets', arguments);
},
initControls: function() {
var self = this,
definition = '',
controlElements = null,
el = null,
parent = null,
delagators = null,
control = null,
i = -1,
j = -1;
self.callActions('beforeInitControls', arguments);
switch (self.config.controls.scope) {
case 'local':
parent = self.dom.container;
break;
case 'global':
parent = self.dom.document;
break;
default:
throw new Error(mixitup.messages.errorConfigInvalidControlsScope());
}
for (i = 0; definition = mixitup.controlDefinitions[i]; i++) {
if (self.config.controls.live || definition.live) {
if (definition.parent) {
delagators = self.dom[definition.parent];
if (!delagators || delagators.length < 0) continue;
if (typeof delagators.length !== 'number') {
delagators = [delagators];
}
} else {
delagators = [parent];
}
for (j = 0; (el = delagators[j]); j++) {
control = self.getControl(el, definition.type, definition.selector);
self.controls.push(control);
}
} else {
controlElements = parent.querySelectorAll(self.config.selectors.control + definition.selector);
for (j = 0; (el = controlElements[j]); j++) {
control = self.getControl(el, definition.type, '');
if (!control) continue;
self.controls.push(control);
}
}
}
self.callActions('afterInitControls', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {HTMLElement} el
* @param {string} type
* @param {string} selector
* @return {mixitup.Control|null}
*/
getControl: function(el, type, selector) {
var self = this,
control = null,
i = -1;
self.callActions('beforeGetControl', arguments);
if (!selector) {
// Static controls only
for (i = 0; control = mixitup.controls[i]; i++) {
if (control.el === el && control.isBound(self)) {
// Control already bound to this mixer (as another type).
// NB: This prevents duplicate controls from being registered where a selector
// might collide, eg: "[data-filter]" and "[data-filter][data-sort]"
return self.callFilters('controlGetControl', null, arguments);
} else if (control.el === el && control.type === type && control.selector === selector) {
// Another mixer is already using this control, add this mixer as a binding
control.addBinding(self);
return self.callFilters('controlGetControl', control, arguments);
}
}
}
// Create new control
control = new mixitup.Control();
control.init(el, type, selector);
control.classNames.base = h.getClassname(self.config.classNames, type);
control.classNames.active = h.getClassname(self.config.classNames, type, self.config.classNames.modifierActive);
control.classNames.disabled = h.getClassname(self.config.classNames, type, self.config.classNames.modifierDisabled);
// Add a reference to this mixer as a binding
control.addBinding(self);
return self.callFilters('controlGetControl', control, arguments);
},
/**
* Creates a compound selector by joining the `toggleArray` value as per the
* defined toggle logic.
*
* @private
* @instance
* @since 3.0.0
* @return {string}
*/
getToggleSelector: function() {
var self = this,
delineator = self.config.controls.toggleLogic === 'or' ? ', ' : '',
toggleSelector = '';
self.callActions('beforeGetToggleSelector', arguments);
self.toggleArray = h.clean(self.toggleArray);
toggleSelector = self.toggleArray.join(delineator);
if (toggleSelector === '') {
toggleSelector = self.config.controls.toggleDefault;
}
return self.callFilters('selectorGetToggleSelector', toggleSelector, arguments);
},
/**
* Breaks compound selector strings in an array of discreet selectors,
* as per the active `controls.toggleLogic` configuration option. Accepts
* either a dynamic command object, or a state object.
*
* @private
* @instance
* @since 2.0.0
* @param {object} [command]
* @param {mixitup.State} [state]
* @return {void}
*/
buildToggleArray: function(command, state) {
var self = this,
activeFilterSelector = '';
self.callActions('beforeBuildToggleArray', arguments);
if (command && command.filter) {
activeFilterSelector = command.filter.selector.replace(/\s/g, '');
} else if (state) {
activeFilterSelector = state.activeFilter.selector.replace(/\s/g, '');
} else {
return;
}
if (activeFilterSelector === self.config.selectors.target || activeFilterSelector === 'all') {
activeFilterSelector = '';
}
if (self.config.controls.toggleLogic === 'or') {
self.toggleArray = activeFilterSelector.split(',');
} else {
self.toggleArray = self.splitCompoundSelector(activeFilterSelector);
}
self.toggleArray = h.clean(self.toggleArray);
self.callActions('afterBuildToggleArray', arguments);
},
/**
* Takes a compound selector (e.g. `.cat-1.cat-2`, `[data-cat="1"][data-cat="2"]`)
* and breaks into its individual selectors.
*
* @private
* @instance
* @since 3.0.0
* @param {string} compoundSelector
* @return {string[]}
*/
splitCompoundSelector: function(compoundSelector) {
// Break at a `.` or `[`, capturing the delineator
var partials = compoundSelector.split(/([\.\[])/g),
toggleArray = [],
selector = '',
i = -1;
if (partials[0] === '') {
partials.shift();
}
for (i = 0; i < partials.length; i++) {
if (i % 2 === 0) {
selector = '';
}
selector += partials[i];
if (i % 2 !== 0) {
toggleArray.push(selector);
}
}
return toggleArray;
},
/**
* Updates controls to their active/inactive state based on the command or
* current state of the mixer.
*
* @private
* @instance
* @since 2.0.0
* @param {object} command
* @return {void}
*/
updateControls: function(command) {
var self = this,
control = null,
output = new mixitup.CommandMultimix(),
i = -1;
self.callActions('beforeUpdateControls', arguments);
// Sanitise to defaults
if (command.filter) {
output.filter = command.filter.selector;
} else {
output.filter = self.state.activeFilter.selector;
}
if (command.sort) {
output.sort = self.buildSortString(command.sort);
} else {
output.sort = self.buildSortString(self.state.activeSort);
}
if (output.filter === self.config.selectors.target) {
output.filter = 'all';
}
if (output.filter === '') {
output.filter = 'none';
}
h.freeze(output);
for (i = 0; control = self.controls[i]; i++) {
control.update(output, self.toggleArray);
}
self.callActions('afterUpdateControls', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {mixitup.CommandSort} command
* @return {string}
*/
buildSortString: function(command) {
var self = this;
var output = '';
output += command.sortString;
if (command.next) {
output += ' ' + self.buildSortString(command.next);
}
return output;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {object} command
* @param {Operation} operation
* @return {Promise.<mixitup.State>}
*/
insertTargets: function(command, operation) {
var self = this,
nextSibling = null,
insertionIndex = -1,
frag = null,
target = null,
el = null,
i = -1;
self.callActions('beforeInsertTargets', arguments);
if (typeof command.index === 'undefined') command.index = 0;
nextSibling = self.getNextSibling(command.index, command.sibling, command.position);
frag = self.dom.document.createDocumentFragment();
if (nextSibling) {
insertionIndex = h.index(nextSibling, self.config.selectors.target);
} else {
insertionIndex = self.targets.length;
}
if (command.collection) {
for (i = 0; el = command.collection[i]; i++) {
if (self.dom.targets.indexOf(el) > -1) {
throw new Error(mixitup.messages.errorInsertPreexistingElement());
}
// Ensure elements are hidden when they are added to the DOM, so they can
// be animated in gracefully
el.style.display = 'none';
frag.appendChild(el);
frag.appendChild(self.dom.document.createTextNode(' '));
if (!h.isElement(el, self.dom.document) || !el.matches(self.config.selectors.target)) continue;
target = new mixitup.Target();
target.init(el, self);
target.isInDom = true;
self.targets.splice(insertionIndex, 0, target);
insertionIndex++;
}
self.dom.parent.insertBefore(frag, nextSibling);
}
// Since targets have been added, the original order must be updated
operation.startOrder = self.origOrder = self.targets;
self.callActions('afterInsertTargets', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Number} [index]
* @param {Element} [sibling]
* @param {string} [position]
* @return {Element}
*/
getNextSibling: function(index, sibling, position) {
var self = this,
element = null;
index = Math.max(index, 0);
if (sibling && position === 'before') {
// Explicit sibling
element = sibling;
} else if (sibling && position === 'after') {
// Explicit sibling
element = sibling.nextElementSibling || null;
} else if (self.targets.length > 0 && typeof index !== 'undefined') {
// Index and targets exist
element = (index < self.targets.length || !self.targets.length) ?
self.targets[index].dom.el :
self.targets[self.targets.length - 1].dom.el.nextElementSibling;
} else if (self.targets.length === 0 && self.dom.parent.children.length > 0) {
// No targets but other siblings
if (self.config.layout.siblingAfter) {
element = self.config.layout.siblingAfter;
} else if (self.config.layout.siblingBefore) {
element = self.config.layout.siblingBefore.nextElementSibling;
} else {
self.dom.parent.children[0];
}
} else {
element === null;
}
return self.callFilters('elementGetNextSibling', element, arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
filterOperation: function(operation) {
var self = this,
testResult = false,
index = -1,
action = '',
target = null,
i = -1;
self.callActions('beforeFilterOperation', arguments);
action = operation.newFilter.action;
for (i = 0; target = operation.newOrder[i]; i++) {
if (operation.newFilter.collection) {
// show via collection
testResult = operation.newFilter.collection.indexOf(target.dom.el) > -1;
} else {
// show via selector
if (operation.newFilter.selector === '') {
testResult = false;
} else {
testResult = target.dom.el.matches(operation.newFilter.selector);
}
}
self.evaluateHideShow(testResult, target, action, operation);
}
if (operation.toRemove.length) {
for (i = 0; target = operation.show[i]; i++) {
if (operation.toRemove.indexOf(target) > -1) {
// If any shown targets should be removed, move them into the toHide array
operation.show.splice(i, 1);
if ((index = operation.toShow.indexOf(target)) > -1) {
operation.toShow.splice(index, 1);
}
operation.toHide.push(target);
operation.hide.push(target);
i--;
}
}
}
operation.matching = operation.show.slice();
if (operation.show.length === 0 && operation.newFilter.selector !== '' && self.targets.length !== 0) {
operation.hasFailed = true;
}
self.callActions('afterFilterOperation', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {boolean} testResult
* @param {Element} target
* @param {string} action
* @param {Operation} operation
* @return {void}
*/
evaluateHideShow: function(testResult, target, action, operation) {
var self = this,
filteredTestResult = false,
args = Array.prototype.slice.call(arguments, 1);
filteredTestResult = self.callFilters('testResultEvaluateHideShow', testResult, args);
self.callActions('beforeEvaluateHideShow', arguments);
if (
filteredTestResult === true && action === 'show' ||
filteredTestResult === false && action === 'hide'
) {
operation.show.push(target);
!target.isShown && operation.toShow.push(target);
} else {
operation.hide.push(target);
target.isShown && operation.toHide.push(target);
}
self.callActions('afterEvaluateHideShow', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
sortOperation: function(operation) {
var self = this,
newOrder = [],
target = null,
el = null,
i = -1;
self.callActions('beforeSortOperation', arguments);
operation.startOrder = self.targets;
if (operation.newSort.collection) {
// Sort by collection
newOrder = [];
for (i = 0; (el = operation.newSort.collection[i]); i++) {
if (self.dom.targets.indexOf(el) < 0) {
throw new Error(mixitup.messages.errorSortNonExistentElement());
}
target = new mixitup.Target();
target.init(el, self);
target.isInDom = true;
newOrder.push(target);
}
operation.newOrder = newOrder;
} else if (operation.newSort.order === 'random') {
// Sort random
operation.newOrder = h.arrayShuffle(operation.startOrder);
} else if (operation.newSort.attribute === '') {
// Sort by default
operation.newOrder = self.origOrder.slice();
if (operation.newSort.order === 'desc') {
operation.newOrder.reverse();
}
} else {
// Sort by attribute
operation.newOrder = operation.startOrder.slice();
operation.newOrder.sort(function(a, b) {
return self.compare(a, b, operation.newSort);
});
}
if (h.isEqualArray(operation.newOrder, operation.startOrder)) {
operation.willSort = false;
}
self.callActions('afterSortOperation', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {mixitup.Target} a
* @param {mixitup.Target} b
* @param {mixitup.CommandSort} command
* @return {Number}
*/
compare: function(a, b, command) {
var self = this,
order = command.order,
attrA = self.getAttributeValue(a, command.attribute),
attrB = self.getAttributeValue(b, command.attribute);
if (isNaN(attrA * 1) || isNaN(attrB * 1)) {
attrA = attrA.toLowerCase();
attrB = attrB.toLowerCase();
} else {
attrA = attrA * 1;
attrB = attrB * 1;
}
if (attrA < attrB) {
return order === 'asc' ? -1 : 1;
}
if (attrA > attrB) {
return order === 'asc' ? 1 : -1;
}
if (attrA === attrB && command.next) {
return self.compare(a, b, command.next);
}
return 0;
},
/**
* Reads the values of any data attributes present the provided target element
* which match the current sort command.
*
* @private
* @instance
* @since 3.0.0
* @param {mixitup.Target} target
* @param {string} [attribute]
* @return {(String|Number)}
*/
getAttributeValue: function(target, attribute) {
var self = this,
value = '';
value = target.dom.el.getAttribute('data-' + attribute);
if (value === null) {
if (self.config.debug.showWarnings) {
// Encourage users to assign values to all targets to avoid erroneous sorting
// when types are mixed
console.warn(mixitup.messages.warningInconsistentSortingAttributes({
attribute: 'data-' + attribute
}));
}
}
// If an attribute is not present, return 0 as a safety value
return self.callFilters('valueGetAttributeValue', value || 0, arguments);
},
/**
* Inserts elements into the DOM in the appropriate
* order using a document fragment for minimal
* DOM thrashing
*
* @private
* @instance
* @since 2.0.0
* @param {boolean} isResetting
* @param {Operation} operation
* @return {void}
*/
printSort: function(isResetting, operation) {
var self = this,
startOrder = isResetting ? operation.newOrder : operation.startOrder,
newOrder = isResetting ? operation.startOrder : operation.newOrder,
nextSibling = startOrder.length ? startOrder[startOrder.length - 1].dom.el.nextElementSibling : null,
frag = window.document.createDocumentFragment(),
whitespace = null,
target = null,
el = null,
i = -1;
self.callActions('beforePrintSort', arguments);
// Empty the container
for (i = 0; target = startOrder[i]; i++) {
el = target.dom.el;
if (el.style.position === 'absolute') continue;
h.removeWhitespace(el.previousSibling);
el.parentElement.removeChild(el);
}
whitespace = nextSibling ? nextSibling.previousSibling : self.dom.parent.lastChild;
if (whitespace && whitespace.nodeName === '#text') {
h.removeWhitespace(whitespace);
}
for (i = 0; target = newOrder[i]; i++) {
// Add targets into a document fragment
el = target.dom.el;
if (h.isElement(frag.lastChild)) {
frag.appendChild(window.document.createTextNode(' '));
}
frag.appendChild(el);
}
// Insert the document fragment into the container
// before any other non-target elements
if (self.dom.parent.firstChild && self.dom.parent.firstChild !== nextSibling) {
frag.insertBefore(window.document.createTextNode(' '), frag.childNodes[0]);
}
if (nextSibling) {
frag.appendChild(window.document.createTextNode(' '));
self.dom.parent.insertBefore(frag, nextSibling);
} else {
self.dom.parent.appendChild(frag);
}
self.callActions('afterPrintSort', arguments);
},
/**
* Parses user-defined sort strings (i.e. `default:asc`) into sort commands objects.
*
* @private
* @instance
* @since 3.0.0
* @param {string} sortString
* @param {mixitup.CommandSort} command
* @return {mixitup.CommandSort}
*/
parseSortString: function(sortString, command) {
var self = this,
rules = sortString.split(' '),
current = command,
rule = [],
i = -1;
// command.sortString = sortString;
for (i = 0; i < rules.length; i++) {
rule = rules[i].split(':');
current.sortString = rules[i];
current.attribute = h.dashCase(rule[0]);
current.order = rule[1] || 'asc';
switch (current.attribute) {
case 'default':
// treat "default" as sorting by no attribute
current.attribute = '';
break;
case 'random':
// treat "random" as an order not an attribute
current.attribute = '';
current.order = 'random';
break;
}
if (!current.attribute || current.order === 'random') break;
if (i < rules.length - 1) {
// Embed reference to the next command
current.next = new mixitup.CommandSort();
h.freeze(current);
current = current.next;
}
}
return self.callFilters('commandsParseSort', command, arguments);
},
/**
* Parses all effects out of the user-defined `animation.effects` string into
* their respective properties and units.
*
* @private
* @instance
* @since 2.0.0
* @return {void}
*/
parseEffects: function() {
var self = this,
transformName = '',
effectsIn = self.config.animation.effectsIn || self.config.animation.effects,
effectsOut = self.config.animation.effectsOut || self.config.animation.effects;
self.callActions('beforeParseEffects', arguments);
self.effectsIn = new mixitup.StyleData();
self.effectsOut = new mixitup.StyleData();
self.transformIn = [];
self.transformOut = [];
self.effectsIn.opacity = self.effectsOut.opacity = 1;
self.parseEffect('fade', effectsIn, self.effectsIn, self.transformIn);
self.parseEffect('fade', effectsOut, self.effectsOut, self.transformOut, true);
for (transformName in mixitup.transformDefaults) {
if (!(mixitup.transformDefaults[transformName] instanceof mixitup.TransformData)) {
continue;
}
self.parseEffect(transformName, effectsIn, self.effectsIn, self.transformIn);
self.parseEffect(transformName, effectsOut, self.effectsOut, self.transformOut, true);
}
self.parseEffect('stagger', effectsIn, self.effectsIn, self.transformIn);
self.parseEffect('stagger', effectsOut, self.effectsOut, self.transformOut, true);
self.callActions('afterParseEffects', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {string} effectName
* @param {string} effectString
* @param {StyleData} effects
* @param {String[]} transform
* @param {boolean} [isOut]
*/
parseEffect: function(effectName, effectString, effects, transform, isOut) {
var self = this,
re = /\(([^)]+)\)/,
propIndex = -1,
str = '',
match = [],
val = '',
units = ['%', 'px', 'em', 'rem', 'vh', 'vw', 'deg'],
unit = '',
i = -1;
self.callActions('beforeParseEffect', arguments);
if (typeof effectString !== 'string') {
throw new TypeError(mixitup.messages.errorConfigInvalidAnimationEffects());
}
if (effectString.indexOf(effectName) < 0) {
// The effect is not present in the effects string
if (effectName === 'stagger') {
// Reset stagger to 0
self.staggerDuration = 0;
}
return;
}
// The effect is present
propIndex = effectString.indexOf(effectName + '(');
if (propIndex > -1) {
// The effect has a user defined value in parentheses
// Extract from the first parenthesis to the end of string
str = effectString.substring(propIndex);
// Match any number of characters between "(" and ")"
match = re.exec(str);
val = match[1];
}
switch (effectName) {
case 'fade':
effects.opacity = val ? parseFloat(val) : 0;
break;
case 'stagger':
self.staggerDuration = val ? parseFloat(val) : 100;
// TODO: Currently stagger must be applied globally, but
// if seperate values are specified for in/out, this should
// be respected
break;
default:
// All other effects are transforms following the same structure
if (isOut && self.config.animation.reverseOut && effectName !== 'scale') {
effects[effectName].value =
(val ? parseFloat(val) : mixitup.transformDefaults[effectName].value) * -1;
} else {
effects[effectName].value =
(val ? parseFloat(val) : mixitup.transformDefaults[effectName].value);
}
if (val) {
for (i = 0; unit = units[i]; i++) {
if (val.indexOf(unit) > -1) {
effects[effectName].unit = unit;
break;
}
}
} else {
effects[effectName].unit = mixitup.transformDefaults[effectName].unit;
}
transform.push(
effectName +
'(' +
effects[effectName].value +
effects[effectName].unit +
')'
);
}
self.callActions('afterParseEffect', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {State}
*/
buildState: function(operation) {
var self = this,
state = new mixitup.State(),
target = null,
i = -1;
self.callActions('beforeBuildState', arguments);
// Map target elements into state arrays.
// the real target objects should never be exposed
for (i = 0; target = self.targets[i]; i++) {
if (!operation.toRemove.length || operation.toRemove.indexOf(target) < 0) {
state.targets.push(target.dom.el);
}
}
for (i = 0; target = operation.matching[i]; i++) {
state.matching.push(target.dom.el);
}
for (i = 0; target = operation.show[i]; i++) {
state.show.push(target.dom.el);
}
for (i = 0; target = operation.hide[i]; i++) {
if (!operation.toRemove.length || operation.toRemove.indexOf(target) < 0) {
state.hide.push(target.dom.el);
}
}
state.id = self.id;
state.container = self.dom.container;
state.activeFilter = operation.newFilter;
state.activeSort = operation.newSort;
state.activeDataset = operation.newDataset;
state.activeContainerClassName = operation.newContainerClassName;
state.hasFailed = operation.hasFailed;
state.totalTargets = self.targets.length;
state.totalShow = operation.show.length;
state.totalHide = operation.hide.length;
state.totalMatching = operation.matching.length;
state.triggerElement = operation.triggerElement;
return self.callFilters('stateBuildState', state, arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {boolean} shouldAnimate
* @param {Operation} operation
* @return {void}
*/
goMix: function(shouldAnimate, operation) {
var self = this,
deferred = null;
self.callActions('beforeGoMix', arguments);
// If the animation duration is set to 0ms,
// or no effects specified,
// or the container is hidden
// then abort animation
if (
!self.config.animation.duration || !self.config.animation.effects || !h.isVisible(self.dom.container)
) {
shouldAnimate = false;
}
if (
!operation.toShow.length &&
!operation.toHide.length &&
!operation.willSort &&
!operation.willChangeLayout
) {
// If nothing to show or hide, and not sorting or
// changing layout
shouldAnimate = false;
}
if (
!operation.startState.show.length &&
!operation.show.length
) {
// If nothing currently shown, nothing to show
shouldAnimate = false;
}
mixitup.events.fire('mixStart', self.dom.container, {
state: operation.startState,
futureState: operation.newState,
instance: self
}, self.dom.document);
if (typeof self.config.callbacks.onMixStart === 'function') {
self.config.callbacks.onMixStart.call(
self.dom.container,
operation.startState,
operation.newState,
self
);
}
h.removeClass(self.dom.container, h.getClassname(self.config.classNames, 'container', self.config.classNames.modifierFailed));
if (!self.userDeferred) {
// Queue empty, no pending operations
deferred = self.userDeferred = h.defer(mixitup.libraries);
} else {
// Use existing deferred
deferred = self.userDeferred;
}
self.isBusy = true;
if (!shouldAnimate || !mixitup.features.has.transitions) {
// Abort
if (self.config.debug.fauxAsync) {
setTimeout(function() {
self.cleanUp(operation);
}, self.config.animation.duration);
} else {
self.cleanUp(operation);
}
return self.callFilters('promiseGoMix', deferred.promise, arguments);
}
// If we should animate and the platform supports transitions, go for it
if (window.pageYOffset !== operation.docState.scrollTop) {
window.scrollTo(operation.docState.scrollLeft, operation.docState.scrollTop);
}
if (self.config.animation.applyPerspective) {
self.dom.parent.style[mixitup.features.perspectiveProp] =
self.config.animation.perspectiveDistance;
self.dom.parent.style[mixitup.features.perspectiveOriginProp] =
self.config.animation.perspectiveOrigin;
}
if (
self.config.animation.animateResizeContainer &&
operation.startHeight !== operation.newHeight &&
operation.viewportDeltaY !== operation.startHeight - operation.newHeight
) {
self.dom.parent.style.height = operation.startHeight + 'px';
}
if (
self.config.animation.animateResizeContainer &&
operation.startWidth !== operation.newWidth &&
operation.viewportDeltaX !== operation.startWidth - operation.newWidth
) {
self.dom.parent.style.width = operation.startWidth + 'px';
}
if (operation.startHeight === operation.newHeight) {
self.dom.parent.style.height = operation.startHeight + 'px';
}
if (operation.startWidth === operation.newWidth) {
self.dom.parent.style.width = operation.startWidth + 'px';
}
if (operation.startHeight === operation.newHeight && operation.startWidth === operation.newWidth) {
self.dom.parent.style.overflow = 'hidden';
}
requestAnimationFrame(function() {
self.moveTargets(operation);
});
return self.callFilters('promiseGoMix', deferred.promise, arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
getStartMixData: function(operation) {
var self = this,
parentStyle = window.getComputedStyle(self.dom.parent),
parentRect = self.dom.parent.getBoundingClientRect(),
target = null,
data = {},
i = -1,
boxSizing = parentStyle[mixitup.features.boxSizingProp];
self.incPadding = (boxSizing === 'border-box');
self.callActions('beforeGetStartMixData', arguments);
for (i = 0; target = operation.show[i]; i++) {
data = target.getPosData();
operation.showPosData[i] = {
startPosData: data
};
}
for (i = 0; target = operation.toHide[i]; i++) {
data = target.getPosData();
operation.toHidePosData[i] = {
startPosData: data
};
}
operation.startX = parentRect.left;
operation.startY = parentRect.top;
operation.startHeight = self.incPadding ?
parentRect.height :
parentRect.height -
parseFloat(parentStyle.paddingTop) -
parseFloat(parentStyle.paddingBottom) -
parseFloat(parentStyle.borderTop) -
parseFloat(parentStyle.borderBottom);
operation.startWidth = self.incPadding ?
parentRect.width :
parentRect.width -
parseFloat(parentStyle.paddingLeft) -
parseFloat(parentStyle.paddingRight) -
parseFloat(parentStyle.borderLeft) -
parseFloat(parentStyle.borderRight);
self.callActions('afterGetStartMixData', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
setInter: function(operation) {
var self = this,
target = null,
i = -1;
self.callActions('beforeSetInter', arguments);
// Prevent scrollbar flicker on non-inertial scroll platforms by clamping height/width
if (self.config.animation.clampHeight) {
self.dom.parent.style.height = operation.startHeight + 'px';
self.dom.parent.style.overflow = 'hidden';
}
if (self.config.animation.clampWidth) {
self.dom.parent.style.width = operation.startWidth + 'px';
self.dom.parent.style.overflow = 'hidden';
}
for (i = 0; target = operation.toShow[i]; i++) {
target.show();
}
if (operation.willChangeLayout) {
h.removeClass(self.dom.container, operation.startContainerClassName);
h.addClass(self.dom.container, operation.newContainerClassName);
}
self.callActions('afterSetInter', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
getInterMixData: function(operation) {
var self = this,
target = null,
i = -1;
self.callActions('beforeGetInterMixData', arguments);
for (i = 0; target = operation.show[i]; i++) {
operation.showPosData[i].interPosData = target.getPosData();
}
for (i = 0; target = operation.toHide[i]; i++) {
operation.toHidePosData[i].interPosData = target.getPosData();
}
self.callActions('afterGetInterMixData', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
setFinal: function(operation) {
var self = this,
target = null,
i = -1;
self.callActions('beforeSetFinal', arguments);
operation.willSort && self.printSort(false, operation);
for (i = 0; target = operation.toHide[i]; i++) {
target.hide();
}
self.callActions('afterSetFinal', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
getFinalMixData: function(operation) {
var self = this,
parentStyle = null,
parentRect = null,
target = null,
i = -1;
self.callActions('beforeGetFinalMixData', arguments);
for (i = 0; target = operation.show[i]; i++) {
operation.showPosData[i].finalPosData = target.getPosData();
}
for (i = 0; target = operation.toHide[i]; i++) {
operation.toHidePosData[i].finalPosData = target.getPosData();
}
// Remove clamping
if (self.config.animation.clampHeight || self.config.animation.clampWidth) {
self.dom.parent.style.height =
self.dom.parent.style.width =
self.dom.parent.style.overflow = '';
}
if (!self.incPadding) {
parentStyle = window.getComputedStyle(self.dom.parent);
}
parentRect = self.dom.parent.getBoundingClientRect();
operation.newX = parentRect.left;
operation.newY = parentRect.top;
operation.newHeight = self.incPadding ?
parentRect.height :
parentRect.height -
parseFloat(parentStyle.paddingTop) -
parseFloat(parentStyle.paddingBottom) -
parseFloat(parentStyle.borderTop) -
parseFloat(parentStyle.borderBottom);
operation.newWidth = self.incPadding ?
parentRect.width :
parentRect.width -
parseFloat(parentStyle.paddingLeft) -
parseFloat(parentStyle.paddingRight) -
parseFloat(parentStyle.borderLeft) -
parseFloat(parentStyle.borderRight);
operation.viewportDeltaX = operation.docState.viewportWidth - this.dom.document.documentElement.clientWidth;
operation.viewportDeltaY = operation.docState.viewportHeight - this.dom.document.documentElement.clientHeight;
if (operation.willSort) {
self.printSort(true, operation);
}
for (i = 0; target = operation.toShow[i]; i++) {
target.hide();
}
for (i = 0; target = operation.toHide[i]; i++) {
target.show();
}
if (operation.willChangeLayout) {
h.removeClass(self.dom.container, operation.newContainerClassName);
h.addClass(self.dom.container, self.config.layout.containerClassName);
}
self.callActions('afterGetFinalMixData', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Operation} operation
*/
getTweenData: function(operation) {
var self = this,
target = null,
posData = null,
effectNames = Object.getOwnPropertyNames(self.effectsIn),
effectName = '',
effect = null,
widthChange = -1,
heightChange = -1,
i = -1,
j = -1;
self.callActions('beforeGetTweenData', arguments);
for (i = 0; target = operation.show[i]; i++) {
posData = operation.showPosData[i];
posData.posIn = new mixitup.StyleData();
posData.posOut = new mixitup.StyleData();
posData.tweenData = new mixitup.StyleData();
// Process x and y
if (target.isShown) {
posData.posIn.x = posData.startPosData.x - posData.interPosData.x;
posData.posIn.y = posData.startPosData.y - posData.interPosData.y;
} else {
posData.posIn.x = posData.posIn.y = 0;
}
posData.posOut.x = posData.finalPosData.x - posData.interPosData.x;
posData.posOut.y = posData.finalPosData.y - posData.interPosData.y;
// Process opacity
posData.posIn.opacity = target.isShown ? 1 : self.effectsIn.opacity;
posData.posOut.opacity = 1;
posData.tweenData.opacity = posData.posOut.opacity - posData.posIn.opacity;
// Adjust x and y if not nudging
if (!target.isShown && !self.config.animation.nudge) {
posData.posIn.x = posData.posOut.x;
posData.posIn.y = posData.posOut.y;
}
posData.tweenData.x = posData.posOut.x - posData.posIn.x;
posData.tweenData.y = posData.posOut.y - posData.posIn.y;
// Process width, height, and margins
if (self.config.animation.animateResizeTargets) {
posData.posIn.width = posData.startPosData.width;
posData.posIn.height = posData.startPosData.height;
// "||" Prevents width/height change from including 0 width/height if hiding or showing
widthChange = (posData.startPosData.width || posData.finalPosData.width) - posData.interPosData.width;
posData.posIn.marginRight = posData.startPosData.marginRight - widthChange;
heightChange = (posData.startPosData.height || posData.finalPosData.height) - posData.interPosData.height;
posData.posIn.marginBottom = posData.startPosData.marginBottom - heightChange;
posData.posOut.width = posData.finalPosData.width;
posData.posOut.height = posData.finalPosData.height;
widthChange = (posData.finalPosData.width || posData.startPosData.width) - posData.interPosData.width;
posData.posOut.marginRight = posData.finalPosData.marginRight - widthChange;
heightChange = (posData.finalPosData.height || posData.startPosData.height) - posData.interPosData.height;
posData.posOut.marginBottom = posData.finalPosData.marginBottom - heightChange;
posData.tweenData.width = posData.posOut.width - posData.posIn.width;
posData.tweenData.height = posData.posOut.height - posData.posIn.height;
posData.tweenData.marginRight = posData.posOut.marginRight - posData.posIn.marginRight;
posData.tweenData.marginBottom = posData.posOut.marginBottom - posData.posIn.marginBottom;
}
// Process transforms
for (j = 0; effectName = effectNames[j]; j++) {
effect = self.effectsIn[effectName];
if (!(effect instanceof mixitup.TransformData) || !effect.value) continue;
posData.posIn[effectName].value = effect.value;
posData.posOut[effectName].value = 0;
posData.tweenData[effectName].value =
posData.posOut[effectName].value - posData.posIn[effectName].value;
posData.posIn[effectName].unit =
posData.posOut[effectName].unit =
posData.tweenData[effectName].unit =
effect.unit;
}
}
for (i = 0; target = operation.toHide[i]; i++) {
posData = operation.toHidePosData[i];
posData.posIn = new mixitup.StyleData();
posData.posOut = new mixitup.StyleData();
posData.tweenData = new mixitup.StyleData();
// Process x and y
posData.posIn.x = target.isShown ? posData.startPosData.x - posData.interPosData.x : 0;
posData.posIn.y = target.isShown ? posData.startPosData.y - posData.interPosData.y : 0;
posData.posOut.x = self.config.animation.nudge ? 0 : posData.posIn.x;
posData.posOut.y = self.config.animation.nudge ? 0 : posData.posIn.y;
posData.tweenData.x = posData.posOut.x - posData.posIn.x;
posData.tweenData.y = posData.posOut.y - posData.posIn.y;
// Process width, height, and margins
if (self.config.animation.animateResizeTargets) {
posData.posIn.width = posData.startPosData.width;
posData.posIn.height = posData.startPosData.height;
widthChange = posData.startPosData.width - posData.interPosData.width;
posData.posIn.marginRight = posData.startPosData.marginRight - widthChange;
heightChange = posData.startPosData.height - posData.interPosData.height;
posData.posIn.marginBottom = posData.startPosData.marginBottom - heightChange;
}
// Process opacity
posData.posIn.opacity = 1;
posData.posOut.opacity = self.effectsOut.opacity;
posData.tweenData.opacity = posData.posOut.opacity - posData.posIn.opacity;
// Process transforms
for (j = 0; effectName = effectNames[j]; j++) {
effect = self.effectsOut[effectName];
if (!(effect instanceof mixitup.TransformData) || !effect.value) continue;
posData.posIn[effectName].value = 0;
posData.posOut[effectName].value = effect.value;
posData.tweenData[effectName].value =
posData.posOut[effectName].value - posData.posIn[effectName].value;
posData.posIn[effectName].unit =
posData.posOut[effectName].unit =
posData.tweenData[effectName].unit =
effect.unit;
}
}
self.callActions('afterGetTweenData', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Operation} operation
* @return {void}
*/
moveTargets: function(operation) {
var self = this,
target = null,
moveData = null,
posData = null,
statusChange = '',
willTransition = false,
staggerIndex = -1,
i = -1,
checkProgress = self.checkProgress.bind(self);
self.callActions('beforeMoveTargets', arguments);
// TODO: this is an extra loop in addition to the calcs
// done in getOperation, could some of this be done there?
for (i = 0; target = operation.show[i]; i++) {
moveData = new mixitup.IMoveData();
posData = operation.showPosData[i];
statusChange = target.isShown ? 'none' : 'show';
willTransition = self.willTransition(
statusChange,
operation.hasEffect,
posData.posIn,
posData.posOut
);
if (willTransition) {
// Prevent non-transitioning targets from incrementing the staggerIndex
staggerIndex++;
}
target.show();
moveData.posIn = posData.posIn;
moveData.posOut = posData.posOut;
moveData.statusChange = statusChange;
moveData.staggerIndex = staggerIndex;
moveData.operation = operation;
moveData.callback = willTransition ? checkProgress : null;
target.move(moveData);
}
for (i = 0; target = operation.toHide[i]; i++) {
posData = operation.toHidePosData[i];
moveData = new mixitup.IMoveData();
statusChange = 'hide';
willTransition = self.willTransition(statusChange, posData.posIn, posData.posOut);
moveData.posIn = posData.posIn;
moveData.posOut = posData.posOut;
moveData.statusChange = statusChange;
moveData.staggerIndex = i;
moveData.operation = operation;
moveData.callback = willTransition ? checkProgress : null;
target.move(moveData);
}
if (self.config.animation.animateResizeContainer) {
self.dom.parent.style[mixitup.features.transitionProp] =
'height ' + self.config.animation.duration + 'ms ease, ' +
'width ' + self.config.animation.duration + 'ms ease ';
requestAnimationFrame(function() {
if (
operation.startHeight !== operation.newHeight &&
operation.viewportDeltaY !== operation.startHeight - operation.newHeight
) {
self.dom.parent.style.height = operation.newHeight + 'px';
}
if (
operation.startWidth !== operation.newWidth &&
operation.viewportDeltaX !== operation.startWidth - operation.newWidth
) {
self.dom.parent.style.width = operation.newWidth + 'px';
}
});
}
if (operation.willChangeLayout) {
h.removeClass(self.dom.container, self.config.layout.ContainerClassName);
h.addClass(self.dom.container, operation.newContainerClassName);
}
self.callActions('afterMoveTargets', arguments);
},
/**
* @private
* @instance
* @return {boolean}
*/
hasEffect: function() {
var self = this,
EFFECTABLES = [
'scale',
'translateX', 'translateY', 'translateZ',
'rotateX', 'rotateY', 'rotateZ'
],
effectName = '',
effect = null,
result = false,
value = -1,
i = -1;
if (self.effectsIn.opacity !== 1) {
return self.callFilters('resultHasEffect', true, arguments);
}
for (i = 0; effectName = EFFECTABLES[i]; i++) {
effect = self.effectsIn[effectName];
value = (typeof effect && effect.value !== 'undefined') ?
effect.value : effect;
if (value !== 0) {
result = true;
break;
}
}
return self.callFilters('resultHasEffect', result, arguments);
},
/**
* Determines if a target element will transition in
* some fasion and therefore requires binding of
* transitionEnd
*
* @private
* @instance
* @since 3.0.0
* @param {string} statusChange
* @param {boolean} hasEffect
* @param {StyleData} posIn
* @param {StyleData} posOut
* @return {boolean}
*/
willTransition: function(statusChange, hasEffect, posIn, posOut) {
var self = this,
result = false;
if (!h.isVisible(self.dom.container)) {
// If the container is not visible, the transitionEnd
// event will not occur and MixItUp will hang
result = false;
} else if (
(statusChange !== 'none' && hasEffect) ||
posIn.x !== posOut.x ||
posIn.y !== posOut.y
) {
// If opacity and/or translate will change
result = true;
} else if (self.config.animation.animateResizeTargets) {
// Check if width, height or margins will change
result = (
posIn.width !== posOut.width ||
posIn.height !== posOut.height ||
posIn.marginRight !== posOut.marginRight ||
posIn.marginTop !== posOut.marginTop
);
} else {
result = false;
}
return self.callFilters('resultWillTransition', result, arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
checkProgress: function(operation) {
var self = this;
self.targetsDone++;
if (self.targetsBound === self.targetsDone) {
self.cleanUp(operation);
}
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Operation} operation
* @return {void}
*/
cleanUp: function(operation) {
var self = this,
target = null,
whitespaceBefore = null,
whitespaceAfter = null,
nextInQueue = null,
i = -1;
self.callActions('beforeCleanUp', arguments);
self.targetsMoved =
self.targetsImmovable =
self.targetsBound =
self.targetsDone = 0;
for (i = 0; target = operation.show[i]; i++) {
target.cleanUp();
target.show();
}
for (i = 0; target = operation.toHide[i]; i++) {
target.cleanUp();
target.hide();
}
if (operation.willSort) {
self.printSort(false, operation);
}
// Remove any styles applied to the parent container
self.dom.parent.style[mixitup.features.transitionProp] =
self.dom.parent.style.height =
self.dom.parent.style.width =
self.dom.parent.style.overflow =
self.dom.parent.style[mixitup.features.perspectiveProp] =
self.dom.parent.style[mixitup.features.perspectiveOriginProp] = '';
if (operation.willChangeLayout) {
h.removeClass(self.dom.container, operation.startContainerClassName);
h.addClass(self.dom.container, operation.newContainerClassName);
}
if (operation.toRemove.length) {
for (i = 0; target = self.targets[i]; i++) {
if (operation.toRemove.indexOf(target) > -1) {
if (
(whitespaceBefore = target.dom.el.previousSibling) && whitespaceBefore.nodeName === '#text' &&
(whitespaceAfter = target.dom.el.nextSibling) && whitespaceAfter.nodeName === '#text'
) {
h.removeWhitespace(whitespaceBefore);
}
if (!operation.willSort) {
// NB: Sorting will remove targets as a bi-product of `printSort()`
self.dom.parent.removeChild(target.dom.el);
}
self.targets.splice(i, 1);
target.isInDom = false;
i--;
}
}
// Since targets have been removed, the original order must be updated
self.origOrder = self.targets;
}
if (operation.willSort) {
self.targets = operation.newOrder;
}
self.state = operation.newState;
self.lastOperation = operation;
self.dom.targets = self.state.targets;
// mixEnd
mixitup.events.fire('mixEnd', self.dom.container, {
state: self.state,
instance: self
}, self.dom.document);
if (typeof self.config.callbacks.onMixEnd === 'function') {
self.config.callbacks.onMixEnd.call(self.dom.container, self.state, self);
}
if (operation.hasFailed) {
// mixFail
mixitup.events.fire('mixFail', self.dom.container, {
state: self.state,
instance: self
}, self.dom.document);
if (typeof self.config.callbacks.onMixFail === 'function') {
self.config.callbacks.onMixFail.call(self.dom.container, self.state, self);
}
h.addClass(self.dom.container, h.getClassname(self.config.classNames, 'container', self.config.classNames.modifierFailed));
}
// User-defined callback function
if (typeof self.userCallback === 'function') {
self.userCallback.call(self.dom.container, self.state, self);
}
if (typeof self.userDeferred.resolve === 'function') {
self.userDeferred.resolve(self.state);
}
self.userCallback = null;
self.userDeferred = null;
self.lastClicked = null;
self.isToggling = false;
self.isBusy = false;
if (self.queue.length) {
self.callActions('beforeReadQueueCleanUp', arguments);
nextInQueue = self.queue.shift();
// Update non-public API properties stored in queue
self.userDeferred = nextInQueue.deferred;
self.isToggling = nextInQueue.isToggling;
self.lastClicked = nextInQueue.triggerElement;
if (nextInQueue.instruction.command instanceof mixitup.CommandMultimix) {
self.multimix.apply(self, nextInQueue.args);
} else {
self.dataset.apply(self, nextInQueue.args);
}
}
self.callActions('afterCleanUp', arguments);
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseMultimixArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandMultimix();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
if (typeof arg === 'object') {
h.extend(instruction.command, arg);
} else if (typeof arg === 'boolean') {
instruction.animate = arg;
} else if (typeof arg === 'function') {
instruction.callback = arg;
}
}
// Coerce arbitrary command arguments into typed command objects
if (instruction.command.insert && !(instruction.command.insert instanceof mixitup.CommandInsert)) {
instruction.command.insert = self.parseInsertArgs([instruction.command.insert]).command;
}
if (instruction.command.remove && !(instruction.command.remove instanceof mixitup.CommandRemove)) {
instruction.command.remove = self.parseRemoveArgs([instruction.command.remove]).command;
}
if (instruction.command.filter && !(instruction.command.filter instanceof mixitup.CommandFilter)) {
instruction.command.filter = self.parseFilterArgs([instruction.command.filter]).command;
}
if (instruction.command.sort && !(instruction.command.sort instanceof mixitup.CommandSort)) {
instruction.command.sort = self.parseSortArgs([instruction.command.sort]).command;
}
if (instruction.command.changeLayout && !(instruction.command.changeLayout instanceof mixitup.CommandChangeLayout)) {
instruction.command.changeLayout = self.parseChangeLayoutArgs([instruction.command.changeLayout]).command;
}
instruction = self.callFilters('instructionParseMultimixArgs', instruction, arguments);
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseFilterArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandFilter();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (typeof arg === 'string') {
// Selector
instruction.command.selector = arg;
} else if (arg === null) {
instruction.command.collection = [];
} else if (typeof arg === 'object' && h.isElement(arg, self.dom.document)) {
// Single element
instruction.command.collection = [arg];
} else if (typeof arg === 'object' && typeof arg.length !== 'undefined') {
// Multiple elements in array, NodeList or jQuery collection
instruction.command.collection = h.arrayFromList(arg);
} else if (typeof arg === 'object') {
// Filter command
h.extend(instruction.command, arg);
} else if (typeof arg === 'boolean') {
instruction.animate = arg;
} else if (typeof arg === 'function') {
instruction.callback = arg;
}
}
if (instruction.command.selector && instruction.command.collection) {
throw new Error(mixitup.messages.errorFilterInvalidArguments());
}
instruction = self.callFilters('instructionParseFilterArgs', instruction, arguments);
h.freeze(instruction);
return instruction;
},
parseSortArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
sortString = '',
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandSort();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
switch (typeof arg) {
case 'string':
// Sort string
sortString = arg;
break;
case 'object':
// Array of element references
if (arg.length) {
instruction.command.collection = h.arrayFromList(arg);
}
break;
case 'boolean':
instruction.animate = arg;
break;
case 'function':
instruction.callback = arg;
break;
}
}
if (sortString) {
instruction.command = self.parseSortString(sortString, instruction.command);
}
instruction = self.callFilters('instructionParseSortArgs', instruction, arguments);
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 2.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseInsertArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandInsert();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
if (typeof arg === 'number') {
// Insert index
instruction.command.index = arg;
} else if (typeof arg === 'string' && ['before', 'after'].indexOf(arg) > -1) {
// 'before'/'after'
instruction.command.position = arg;
} else if (typeof arg === 'string') {
// Markup
instruction.command.collection =
h.arrayFromList(h.createElement(arg).childNodes);
} else if (typeof arg === 'object' && h.isElement(arg, self.dom.document)) {
// Single element
!instruction.command.collection.length ?
(instruction.command.collection = [arg]) :
(instruction.command.sibling = arg);
} else if (typeof arg === 'object' && arg.length) {
// Multiple elements in array or jQuery collection
!instruction.command.collection.length ?
(instruction.command.collection = arg) :
instruction.command.sibling = arg[0];
} else if (typeof arg === 'object' && arg.childNodes && arg.childNodes.length) {
// Document fragment
!instruction.command.collection.length ?
instruction.command.collection = h.arrayFromList(arg.childNodes) :
instruction.command.sibling = arg.childNodes[0];
} else if (typeof arg === 'object') {
// Insert command
h.extend(instruction.command, arg);
} else if (typeof arg === 'boolean') {
instruction.animate = arg;
} else if (typeof arg === 'function') {
instruction.callback = arg;
}
}
if (instruction.command.index && instruction.command.sibling) {
throw new Error(mixitup.messages.errorInsertInvalidArguments());
}
if (!instruction.command.collection.length && self.config.debug.showWarnings) {
console.warn(mixitup.messages.warningInsertNoElements());
}
instruction = self.callFilters('instructionParseInsertArgs', instruction, arguments);
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseRemoveArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
target = null,
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandRemove();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
switch (typeof arg) {
case 'number':
if (self.targets[arg]) {
instruction.command.targets[0] = self.targets[arg];
}
break;
case 'string':
instruction.command.collection = h.arrayFromList(self.dom.parent.querySelectorAll(arg));
break;
case 'object':
if (arg && arg.length) {
instruction.command.collection = arg;
} else if (h.isElement(arg, self.dom.document)) {
instruction.command.collection = [arg];
} else {
// Remove command
h.extend(instruction.command, arg);
}
break;
case 'boolean':
instruction.animate = arg;
break;
case 'function':
instruction.callback = arg;
break;
}
}
if (instruction.command.collection.length) {
for (i = 0; target = self.targets[i]; i++) {
if (instruction.command.collection.indexOf(target.dom.el) > -1) {
instruction.command.targets.push(target);
}
}
}
if (!instruction.command.targets.length && self.config.debug.showWarnings) {
console.warn(mixitup.messages.warningRemoveNoElements());
}
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseDatasetArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandDataset();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
switch (typeof arg) {
case 'object':
if (Array.isArray(arg) || typeof arg.length === 'number') {
instruction.command.dataset = arg;
} else {
// Change layout command
h.extend(instruction.command, arg);
}
break;
case 'boolean':
instruction.animate = arg;
break;
case 'function':
instruction.callback = arg;
break;
}
}
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Array<*>} args
* @return {mixitup.UserInstruction}
*/
parseChangeLayoutArgs: function(args) {
var self = this,
instruction = new mixitup.UserInstruction(),
arg = null,
i = -1;
instruction.animate = self.config.animation.enable;
instruction.command = new mixitup.CommandChangeLayout();
for (i = 0; i < args.length; i++) {
arg = args[i];
if (arg === null) continue;
switch (typeof arg) {
case 'string':
instruction.command.containerClassName = arg;
break;
case 'object':
// Change layout command
h.extend(instruction.command, arg);
break;
case 'boolean':
instruction.animate = arg;
break;
case 'function':
instruction.callback = arg;
break;
}
}
h.freeze(instruction);
return instruction;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {mixitup.QueueItem} queueItem
* @return {Promise.<mixitup.State>}
*/
queueMix: function(queueItem) {
var self = this,
deferred = null,
toggleSelector = '';
self.callActions('beforeQueueMix', arguments);
deferred = h.defer(mixitup.libraries);
if (self.config.animation.queue && self.queue.length < self.config.animation.queueLimit) {
queueItem.deferred = deferred;
self.queue.push(queueItem);
// Keep controls in sync with user interactions. Mixer will catch up as it drains the queue.
if (self.config.controls.enable) {
if (self.isToggling) {
self.buildToggleArray(queueItem.instruction.command);
toggleSelector = self.getToggleSelector();
self.updateControls({
filter: {
selector: toggleSelector
}
});
} else {
self.updateControls(queueItem.instruction.command);
}
}
} else {
if (self.config.debug.showWarnings) {
console.warn(mixitup.messages.warningMultimixInstanceQueueFull());
}
deferred.resolve(self.state);
mixitup.events.fire('mixBusy', self.dom.container, {
state: self.state,
instance: self
}, self.dom.document);
if (typeof self.config.callbacks.onMixBusy === 'function') {
self.config.callbacks.onMixBusy.call(self.dom.container, self.state, self);
}
}
return self.callFilters('promiseQueueMix', deferred.promise, arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Array.<object>} newDataset
* @return {Operation}
*/
getDataOperation: function(newDataset) {
var self = this,
operation = new mixitup.Operation(),
startDataset = [];
operation = self.callFilters('operationUnmappedGetDataOperation', operation, arguments);
if (self.dom.targets.length && !(startDataset = (self.state.activeDataset || [])).length) {
throw new Error(mixitup.messages.errorDatasetNotSet());
}
operation.id = h.randomHex();
operation.startState = self.state;
operation.startDataset = startDataset;
operation.newDataset = newDataset.slice();
self.diffDatasets(operation);
operation.startOrder = self.targets;
operation.newOrder = operation.show;
if (self.config.animation.enable) {
self.getStartMixData(operation);
self.setInter(operation);
operation.docState = h.getDocumentState(self.dom.document);
self.getInterMixData(operation);
self.setFinal(operation);
self.getFinalMixData(operation);
self.parseEffects();
operation.hasEffect = self.hasEffect();
self.getTweenData(operation);
}
self.targets = operation.show.slice();
operation.newState = self.buildState(operation);
// NB: Targets to be removed must be included in `self.targets` for removal during clean up,
// but are added after state is built so that state is accurate
Array.prototype.push.apply(self.targets, operation.toRemove);
operation = self.callFilters('operationMappedGetDataOperation', operation, arguments);
return operation;
},
/**
* @private
* @instance
* @since 3.0.0
* @param {mixitup.Operation} operation
* @return {void}
*/
diffDatasets: function(operation) {
var self = this,
persistantStartIds = [],
persistantNewIds = [],
insertedTargets = [],
data = null,
target = null,
el = null,
frag = null,
nextEl = null,
uids = {},
id = '',
i = -1;
self.callActions('beforeDiffDatasets', arguments);
for (i = 0; data = operation.newDataset[i]; i++) {
if (typeof (id = data[self.config.data.uidKey]) === 'undefined' || id.toString().length < 1) {
throw new TypeError(mixitup.messages.errorDatasetInvalidUidKey({
uidKey: self.config.data.uidKey
}));
}
if (!uids[id]) {
uids[id] = true;
} else {
throw new Error(mixitup.messages.errorDatasetDuplicateUid({
uid: id
}));
}
if ((target = self.cache[id]) instanceof mixitup.Target) {
// Already in cache
if (self.config.data.dirtyCheck && !h.deepEquals(data, target.data)) {
// change detected
el = target.render(data);
target.data = data;
if (el !== target.dom.el) {
// Update target element reference
if (target.isInDom) {
target.unbindEvents();
self.dom.parent.replaceChild(el, target.dom.el);
}
if (!target.isShown) {
el.style.display = 'none';
}
target.dom.el = el;
if (target.isInDom) {
target.bindEvents();
}
}
}
el = target.dom.el;
} else {
// New target
target = new mixitup.Target();
target.init(null, self, data);
target.hide();
}
if (!target.isInDom) {
// Adding to DOM
if (!frag) {
// Open frag
frag = self.dom.document.createDocumentFragment();
}
if (frag.lastElementChild) {
frag.appendChild(self.dom.document.createTextNode(' '));
}
frag.appendChild(target.dom.el);
target.isInDom = true;
target.unbindEvents();
target.bindEvents();
target.hide();
operation.toShow.push(target);
insertedTargets.push(target);
} else {
// Already in DOM
nextEl = target.dom.el.nextElementSibling;
persistantNewIds.push(id);
if (frag) {
// Close and insert previously opened frag
if (frag.lastElementChild) {
frag.appendChild(self.dom.document.createTextNode(' '));
}
self.insertDatasetFrag(frag, target.dom.el, insertedTargets);
frag = null;
}
}
operation.show.push(target);
}
if (frag) {
// Unclosed frag remaining
nextEl = nextEl || self.config.layout.siblingAfter;
if (nextEl) {
frag.appendChild(self.dom.document.createTextNode(' '));
}
self.insertDatasetFrag(frag, nextEl, insertedTargets);
}
for (i = 0; data = operation.startDataset[i]; i++) {
id = data[self.config.data.uidKey];
target = self.cache[id];
if (operation.show.indexOf(target) < 0) {
// Previously shown but now absent
operation.hide.push(target);
operation.toHide.push(target);
operation.toRemove.push(target);
} else {
persistantStartIds.push(id);
}
}
if (!h.isEqualArray(persistantStartIds, persistantNewIds)) {
operation.willSort = true;
}
self.callActions('afterDiffDatasets', arguments);
},
/**
* @private
* @instance
* @since 3.1.5
* @param {DocumentFragment} frag
* @param {(HTMLElement|null)} nextEl
* @param {Array.<mixitup.Target>} targets
* @return {void}
*/
insertDatasetFrag: function(frag, nextEl, targets) {
var self = this;
var insertAt = nextEl ? h.arrayFromList(self.dom.parent.children).indexOf(nextEl) : self.targets.length;
self.dom.parent.insertBefore(frag, nextEl);
while (targets.length) {
self.targets.splice(insertAt, 0, targets.shift());
insertAt++;
}
},
/**
* @private
* @instance
* @since 3.0.0
* @param {mixitup.CommandSort} sortCommandA
* @param {mixitup.CommandSort} sortCommandB
* @return {boolean}
*/
willSort: function(sortCommandA, sortCommandB) {
var self = this,
result = false;
if (
self.config.behavior.liveSort ||
sortCommandA.order === 'random' ||
sortCommandA.attribute !== sortCommandB.attribute ||
sortCommandA.order !== sortCommandB.order ||
sortCommandA.collection !== sortCommandB.collection ||
(sortCommandA.next === null && sortCommandB.next) ||
(sortCommandA.next && sortCommandB.next === null)
) {
result = true;
} else if (sortCommandA.next && sortCommandB.next) {
result = self.willSort(sortCommandA.next, sortCommandB.next);
} else {
result = false;
}
return self.callFilters('resultWillSort', result, arguments);
},
/**
* A shorthand method for `.filter('all')`. Shows all targets in the container.
*
* @example
*
* .show()
*
* @example <caption>Example: Showing all targets</caption>
*
* mixer.show()
* .then(function(state) {
* console.log(state.totalShow === state.totalTargets); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @return {Promise.<mixitup.State>}
*/
show: function() {
var self = this;
return self.filter('all');
},
/**
* A shorthand method for `.filter('none')`. Hides all targets in the container.
*
* @example
*
* .hide()
*
* @example <caption>Example: Hiding all targets</caption>
*
* mixer.hide()
* .then(function(state) {
* console.log(state.totalShow === 0); // true
* console.log(state.totalHide === state.totalTargets); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @return {Promise.<mixitup.State>}
*/
hide: function() {
var self = this;
return self.filter('none');
},
/**
* Returns a boolean indicating whether or not a MixItUp operation is
* currently in progress.
*
* @example
*
* .isMixing()
*
* @example <caption>Example: Checking the status of a mixer</caption>
*
* mixer.sort('random', function() {
* console.log(mixer.isMixing()) // false
* });
*
* console.log(mixer.isMixing()) // true
*
* @public
* @instance
* @since 2.0.0
* @return {boolean}
*/
isMixing: function() {
var self = this;
return self.isBusy;
},
/**
* Filters all targets in the container by a provided selector string, or the values `'all'`
* or `'none'`. Only targets matching the selector will be shown.
*
* @example
*
* .filter(selector [, animate] [, callback])
*
* @example <caption>Example 1: Filtering targets by a class selector</caption>
*
* mixer.filter('.category-a')
* .then(function(state) {
* console.log(state.totalShow === containerEl.querySelectorAll('.category-a').length); // true
* });
*
* @example <caption>Example 2: Filtering targets by an attribute selector</caption>
*
* mixer.filter('[data-category~="a"]')
* .then(function(state) {
* console.log(state.totalShow === containerEl.querySelectorAll('[data-category~="a"]').length); // true
* });
*
* @example <caption>Example 3: Filtering targets by a compound selector</caption>
*
* // Show only those targets with the classes 'category-a' AND 'category-b'
*
* mixer.filter('.category-a.category-c')
* .then(function(state) {
* console.log(state.totalShow === containerEl.querySelectorAll('.category-a.category-c').length); // true
* });
*
* @example <caption>Example 4: Filtering via an element collection</caption>
*
* var collection = Array.from(container.querySelectorAll('.mix'));
*
* console.log(collection.length); // 34
*
* // Filter the collection manually using Array.prototype.filter
*
* var filtered = collection.filter(function(target) {
* return parseInt(target.getAttribute('data-price')) > 10;
* });
*
* console.log(filtered.length); // 22
*
* // Pass the filtered collection to MixItUp
*
* mixer.filter(filtered)
* .then(function(state) {
* console.log(state.activeFilter.collection.length === 22); // true
* });
*
* @public
* @instance
* @since 2.0.0
* @param {(string|HTMLElement|Array.<HTMLElement>)} selector
* Any valid CSS selector (i.e. `'.category-a'`), or the values `'all'` or `'none'`. The filter method also accepts a reference to single target element or a collection of target elements to show.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
filter: function() {
var self = this,
instruction = self.parseFilterArgs(arguments);
return self.multimix({
filter: instruction.command
}, instruction.animate, instruction.callback);
},
/**
* Adds an additional selector to the currently active filter selector, concatenating
* as per the logic defined in `controls.toggleLogic`.
*
* @example
*
* .toggleOn(selector [, animate] [, callback])
*
* @example <caption>Example: Toggling on a filter selector</caption>
*
* console.log(mixer.getState().activeFilter.selector); // '.category-a'
*
* mixer.toggleOn('.category-b')
* .then(function(state) {
* console.log(state.activeFilter.selector); // '.category-a, .category-b'
* });
*
* @public
* @instance
* @since 3.0.0
* @param {string} selector
* Any valid CSS selector (i.e. `'.category-a'`)
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
toggleOn: function() {
var self = this,
instruction = self.parseFilterArgs(arguments),
selector = instruction.command.selector,
toggleSelector = '';
self.isToggling = true;
if (self.toggleArray.indexOf(selector) < 0) {
self.toggleArray.push(selector);
}
toggleSelector = self.getToggleSelector();
return self.multimix({
filter: toggleSelector
}, instruction.animate, instruction.callback);
},
/**
* Removes a selector from the active filter selector.
*
* @example
*
* .toggleOff(selector [, animate] [, callback])
*
* @example <caption>Example: Toggling off a filter selector</caption>
*
* console.log(mixer.getState().activeFilter.selector); // '.category-a, .category-b'
*
* mixer.toggleOff('.category-b')
* .then(function(state) {
* console.log(state.activeFilter.selector); // '.category-a'
* });
*
* @public
* @instance
* @since 3.0.0
* @param {string} selector
* Any valid CSS selector (i.e. `'.category-a'`)
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
toggleOff: function() {
var self = this,
instruction = self.parseFilterArgs(arguments),
selector = instruction.command.selector,
selectorIndex = self.toggleArray.indexOf(selector),
toggleSelector = '';
self.isToggling = true;
if (selectorIndex > -1) {
self.toggleArray.splice(selectorIndex, 1);
}
toggleSelector = self.getToggleSelector();
return self.multimix({
filter: toggleSelector
}, instruction.animate, instruction.callback);
},
/**
* Sorts all targets in the container according to a provided sort string.
*
* @example
*
* .sort(sortString [, animate] [, callback])
*
* @example <caption>Example 1: Sorting by the default DOM order</caption>
*
* // Reverse the default order of the targets
*
* mixer.sort('default:desc')
* .then(function(state) {
* console.log(state.activeSort.attribute === 'default'); // true
* console.log(state.activeSort.order === 'desc'); // true
* });
*
* @example <caption>Example 2: Sorting by a custom data-attribute</caption>
*
* // Sort the targets by the value of a `data-published-date` attribute
*
* mixer.sort('published-date:asc')
* .then(function(state) {
* console.log(state.activeSort.attribute === 'published-date'); // true
* console.log(state.activeSort.order === 'asc'); // true
* });
*
* @example <caption>Example 3: Sorting by multiple attributes</caption>
*
* // Sort the targets by the value of a `data-published-date` attribute, then by `data-title`
*
* mixer.sort('published-date:desc data-title:asc')
* .then(function(state) {
* console.log(state.activeSort.attribute === 'published-date'); // true
* console.log(state.activeSort.order === 'desc'); // true
*
* console.log(state.activeSort.next.attribute === 'title'); // true
* console.log(state.activeSort.next.order === 'asc'); // true
* });
*
* @example <caption>Example 4: Sorting by random</caption>
*
* mixer.sort('random')
* .then(function(state) {
* console.log(state.activeSort.order === 'random') // true
* });
*
* @example <caption>Example 5: Sorting via an element collection</caption>
*
* var collection = Array.from(container.querySelectorAll('.mix'));
*
* // Swap the position of two elements in the collection:
*
* var temp = collection[1];
*
* collection[1] = collection[0];
* collection[0] = temp;
*
* // Pass the sorted collection to MixItUp
*
* mixer.sort(collection)
* .then(function(state) {
* console.log(state.targets[0] === collection[0]); // true
* });
*
* @public
* @instance
* @since 2.0.0
* @param {(string|Array.<HTMLElement>)} sortString
* A valid sort string (e.g. `'default'`, `'published-date:asc'`, or `'random'`). The sort method also accepts an array of all target elements in a user-defined order.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
sort: function() {
var self = this,
instruction = self.parseSortArgs(arguments);
return self.multimix({
sort: instruction.command
}, instruction.animate, instruction.callback);
},
/**
* Changes the layout of the container by adding, removing or updating a
* layout-specific class name. If `animation.animateResizetargets` is
* enabled, MixItUp will attempt to gracefully animate the width, height,
* and position of targets between layout states.
*
* @example
*
* .changeLayout(containerClassName [, animate] [, callback])
*
* @example <caption>Example 1: Adding a new class name to the container</caption>
*
* mixer.changeLayout('container-list')
* .then(function(state) {
* console.log(state.activeContainerClass === 'container-list'); // true
* });
*
* @example <caption>Example 2: Removing a previously added class name from the container</caption>
*
* mixer.changeLayout('')
* .then(function(state) {
* console.log(state.activeContainerClass === ''); // true
* });
*
* @public
* @instance
* @since 2.0.0
* @param {string} containerClassName
* A layout-specific class name to add to the container.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
changeLayout: function() {
var self = this,
instruction = self.parseChangeLayoutArgs(arguments);
return self.multimix({
changeLayout: instruction.command
}, instruction.animate, instruction.callback);
},
/**
* Updates the contents and order of the container to reflect the provided dataset,
* if the dataset API is in use.
*
* The dataset API is designed for use in API-driven JavaScript applications, and
* can be used instead of DOM-based methods such as `.filter()`, `.sort()`,
* `.insert()`, etc. When used, insertion, removal, sorting and pagination can be
* achieved purely via changes to your data model, without the uglyness of having
* to interact with or query the DOM directly.
*
* @example
*
* .dataset(dataset [, animate] [, callback])
*
* @example <caption>Example 1: Rendering a dataset</caption>
*
* var myDataset = [
* {id: 1, ...},
* {id: 2, ...},
* {id: 3, ...}
* ];
*
* mixer.dataset(myDataset)
* .then(function(state) {
* console.log(state.totalShow === 3); // true
* });
*
* @example <caption>Example 2: Sorting a dataset</caption>
*
* // Create a new dataset in reverse order
*
* var newDataset = myDataset.slice().reverse();
*
* mixer.dataset(newDataset)
* .then(function(state) {
* console.log(state.activeDataset[0] === myDataset[2]); // true
* });
*
* @example <caption>Example 3: Removing an item from the dataset</caption>
*
* console.log(myDataset.length); // 3
*
* // Create a new dataset with the last item removed.
*
* var newDataset = myDataset.slice().pop();
*
* mixer.dataset(newDataset)
* .then(function(state) {
* console.log(state.totalShow === 2); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @param {Array.<object>} dataset
* An array of objects, each one representing the underlying data model of a target to be rendered.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
dataset: function() {
var self = this,
instruction = self.parseDatasetArgs(arguments),
operation = null,
queueItem = null,
animate = false;
self.callActions('beforeDataset', arguments);
if (!self.isBusy) {
if (instruction.callback) self.userCallback = instruction.callback;
animate = (instruction.animate ^ self.config.animation.enable) ? instruction.animate : self.config.animation.enable;
operation = self.getDataOperation(instruction.command.dataset);
return self.goMix(animate, operation);
} else {
queueItem = new mixitup.QueueItem();
queueItem.args = arguments;
queueItem.instruction = instruction;
return self.queueMix(queueItem);
}
},
/**
* Performs simultaneous `filter`, `sort`, `insert`, `remove` and `changeLayout`
* operations as requested.
*
* @example
*
* .multimix(multimixCommand [, animate] [, callback])
*
* @example <caption>Example 1: Performing simultaneous filtering and sorting</caption>
*
* mixer.multimix({
* filter: '.category-b',
* sort: 'published-date:desc'
* })
* .then(function(state) {
* console.log(state.activeFilter.selector === '.category-b'); // true
* console.log(state.activeSort.attribute === 'published-date'); // true
* });
*
* @example <caption>Example 2: Performing simultaneous sorting, insertion, and removal</caption>
*
* console.log(mixer.getState().totalShow); // 6
*
* // NB: When inserting via `multimix()`, an object should be provided as the value
* // for the `insert` portion of the command, allowing for a collection of elements
* // and an insertion index to be specified.
*
* mixer.multimix({
* sort: 'published-date:desc', // Sort the container, including any new elements
* insert: {
* collection: [newElementReferenceA, newElementReferenceB], // Add 2 new elements at index 5
* index: 5
* },
* remove: existingElementReference // Remove 1 existing element
* })
* .then(function(state) {
* console.log(state.activeSort.attribute === 'published-date'); // true
* console.log(state.totalShow === 7); // true
* });
*
* @public
* @instance
* @since 2.0.0
* @param {object} multimixCommand
* An object containing one or more things to do
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
multimix: function() {
var self = this,
operation = null,
animate = false,
queueItem = null,
instruction = self.parseMultimixArgs(arguments);
self.callActions('beforeMultimix', arguments);
if (!self.isBusy) {
operation = self.getOperation(instruction.command);
if (self.config.controls.enable) {
// Update controls for API calls
if (instruction.command.filter && !self.isToggling) {
// As we are not toggling, reset the toggle array
// so new filter overrides existing toggles
self.toggleArray.length = 0;
self.buildToggleArray(operation.command);
}
if (self.queue.length < 1) {
self.updateControls(operation.command);
}
}
if (instruction.callback) self.userCallback = instruction.callback;
// Always allow the instruction to override the instance setting
animate = (instruction.animate ^ self.config.animation.enable) ?
instruction.animate :
self.config.animation.enable;
self.callFilters('operationMultimix', operation, arguments);
return self.goMix(animate, operation);
} else {
queueItem = new mixitup.QueueItem();
queueItem.args = arguments;
queueItem.instruction = instruction;
queueItem.triggerElement = self.lastClicked;
queueItem.isToggling = self.isToggling;
return self.queueMix(queueItem);
}
},
/**
* @private
* @instance
* @since 3.0.0
* @param {object} multimixCommand
* @param {boolean} [isPreFetch]
* An optional boolean indicating that the operation is being pre-fetched for execution at a later time.
* @return {Operation|null}
*/
getOperation: function(multimixCommand) {
var self = this,
sortCommand = multimixCommand.sort,
filterCommand = multimixCommand.filter,
changeLayoutCommand = multimixCommand.changeLayout,
removeCommand = multimixCommand.remove,
insertCommand = multimixCommand.insert,
operation = new mixitup.Operation();
operation = self.callFilters('operationUnmappedGetOperation', operation, arguments);
operation.id = h.randomHex();
operation.command = multimixCommand;
operation.startState = self.state;
operation.triggerElement = self.lastClicked;
if (self.isBusy) {
if (self.config.debug.showWarnings) {
console.warn(mixitup.messages.warningGetOperationInstanceBusy());
}
return null;
}
if (insertCommand) {
self.insertTargets(insertCommand, operation);
}
if (removeCommand) {
operation.toRemove = removeCommand.targets;
}
operation.startSort = operation.newSort = operation.startState.activeSort;
operation.startOrder = operation.newOrder = self.targets;
if (sortCommand) {
operation.startSort = operation.startState.activeSort;
operation.newSort = sortCommand;
operation.willSort = self.willSort(sortCommand, operation.startState.activeSort);
if (operation.willSort) {
self.sortOperation(operation);
}
}
operation.startFilter = operation.startState.activeFilter;
if (filterCommand) {
operation.newFilter = filterCommand;
} else {
operation.newFilter = h.extend(new mixitup.CommandFilter(), operation.startFilter);
}
if (operation.newFilter.selector === 'all') {
operation.newFilter.selector = self.config.selectors.target;
} else if (operation.newFilter.selector === 'none') {
operation.newFilter.selector = '';
}
self.filterOperation(operation);
operation.startContainerClassName = operation.startState.activeContainerClassName;
if (changeLayoutCommand) {
operation.newContainerClassName = changeLayoutCommand.containerClassName;
if (operation.newContainerClassName !== operation.startContainerClassName) {
operation.willChangeLayout = true;
}
} else {
operation.newContainerClassName = operation.startContainerClassName;
}
if (self.config.animation.enable) {
// Populate the operation's position data
self.getStartMixData(operation);
self.setInter(operation);
operation.docState = h.getDocumentState(self.dom.document);
self.getInterMixData(operation);
self.setFinal(operation);
self.getFinalMixData(operation);
self.parseEffects();
operation.hasEffect = self.hasEffect();
self.getTweenData(operation);
}
if (operation.willSort) {
self.targets = operation.newOrder;
}
operation.newState = self.buildState(operation);
return self.callFilters('operationMappedGetOperation', operation, arguments);
},
/**
* Renders a previously created operation at a specific point in its path, as
* determined by a multiplier between 0 and 1.
*
* @example
* .tween(operation, multiplier)
*
* @private
* @instance
* @since 3.0.0
* @param {mixitup.Operation} operation
* An operation object created via the `getOperation` method
*
* @param {Float} multiplier
* Any number between 0 and 1 representing the percentage complete of the operation
* @return {void}
*/
tween: function(operation, multiplier) {
var target = null,
posData = null,
toHideIndex = -1,
i = -1;
multiplier = Math.min(multiplier, 1);
multiplier = Math.max(multiplier, 0);
for (i = 0; target = operation.show[i]; i++) {
posData = operation.showPosData[i];
target.applyTween(posData, multiplier);
}
for (i = 0; target = operation.hide[i]; i++) {
if (target.isShown) {
target.hide();
}
if ((toHideIndex = operation.toHide.indexOf(target)) > -1) {
posData = operation.toHidePosData[toHideIndex];
if (!target.isShown) {
target.show();
}
target.applyTween(posData, multiplier);
}
}
},
/**
* Inserts one or more new target elements into the container at a specified
* index.
*
* To be indexed as targets, new elements must match the `selectors.target`
* selector (`'.mix'` by default).
*
* @example
*
* .insert(newElements [, index] [, animate], [, callback])
*
* @example <caption>Example 1: Inserting a single element via reference</caption>
*
* console.log(mixer.getState().totalShow); // 0
*
* // Create a new element
*
* var newElement = document.createElement('div');
* newElement.classList.add('mix');
*
* mixer.insert(newElement)
* .then(function(state) {
* console.log(state.totalShow === 1); // true
* });
*
* @example <caption>Example 2: Inserting a single element via HTML string</caption>
*
* console.log(mixer.getState().totalShow); // 1
*
* // Create a new element via reference
*
* var newElementHtml = '&lt;div class="mix"&gt;&lt;/div&gt;';
*
* // Create and insert the new element at index 1
*
* mixer.insert(newElementHtml, 1)
* .then(function(state) {
* console.log(state.totalShow === 2); // true
* console.log(state.show[1].outerHTML === newElementHtml); // true
* });
*
* @example <caption>Example 3: Inserting multiple elements via reference</caption>
*
* console.log(mixer.getState().totalShow); // 2
*
* // Create an array of new elements to insert.
*
* var newElement1 = document.createElement('div');
* var newElement2 = document.createElement('div');
*
* newElement1.classList.add('mix');
* newElement2.classList.add('mix');
*
* var newElementsCollection = [newElement1, newElement2];
*
* // Insert the new elements starting at index 1
*
* mixer.insert(newElementsCollection, 1)
* .then(function(state) {
* console.log(state.totalShow === 4); // true
* console.log(state.show[1] === newElement1); // true
* console.log(state.show[2] === newElement2); // true
* });
*
* @example <caption>Example 4: Inserting a jQuery collection object containing one or more elements</caption>
*
* console.log(mixer.getState().totalShow); // 4
*
* var $newElement = $('&lt;div class="mix"&gt;&lt;/div&gt;');
*
* // Insert the new elements starting at index 3
*
* mixer.insert($newElement, 3)
* .then(function(state) {
* console.log(state.totalShow === 5); // true
* console.log(state.show[3] === $newElement[0]); // true
* });
*
* @public
* @instance
* @since 2.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string)} newElements
* A reference to a single element to insert, an array-like collection of elements, or an HTML string representing a single element.
* @param {number} index=0
* The index at which to insert the new element(s). `0` by default.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
insert: function() {
var self = this,
args = self.parseInsertArgs(arguments);
return self.multimix({
insert: args.command
}, args.animate, args.callback);
},
/**
* Inserts one or more new elements before a provided reference element.
*
* @example
*
* .insertBefore(newElements, referenceElement [, animate] [, callback])
*
* @example <caption>Example: Inserting a new element before a reference element</caption>
*
* // An existing reference element is chosen at index 2
*
* var referenceElement = mixer.getState().show[2];
*
* // Create a new element
*
* var newElement = document.createElement('div');
* newElement.classList.add('mix');
*
* mixer.insertBefore(newElement, referenceElement)
* .then(function(state) {
* // The new element is inserted into the container at index 2, before the reference element
*
* console.log(state.show[2] === newElement); // true
*
* // The reference element is now at index 3
*
* console.log(state.show[3] === referenceElement); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string)} newElements
* A reference to a single element to insert, an array-like collection of elements, or an HTML string representing a single element.
* @param {HTMLElement} referenceElement
* A reference to an existing element in the container to insert new elements before.
*@param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
insertBefore: function() {
var self = this,
args = self.parseInsertArgs(arguments);
return self.insert(args.command.collection, 'before', args.command.sibling, args.animate, args.callback);
},
/**
* Inserts one or more new elements after a provided reference element.
*
* @example
*
* .insertAfter(newElements, referenceElement [, animate] [, callback])
*
* @example <caption>Example: Inserting a new element after a reference element</caption>
*
* // An existing reference element is chosen at index 2
*
* var referenceElement = mixer.getState().show[2];
*
* // Create a new element
*
* var newElement = document.createElement('div');
* newElement.classList.add('mix');
*
* mixer.insertAfter(newElement, referenceElement)
* .then(function(state) {
* // The new element is inserted into the container at index 3, after the reference element
*
* console.log(state.show[3] === newElement); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string)} newElements
* A reference to a single element to insert, an array-like collection of elements, or an HTML string representing a single element.
* @param {HTMLElement} referenceElement
* A reference to an existing element in the container to insert new elements after.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
insertAfter: function() {
var self = this,
args = self.parseInsertArgs(arguments);
return self.insert(args.command.collection, 'after', args.command.sibling, args.animate, args.callback);
},
/**
* Inserts one or more new elements into the container before all existing targets.
*
* @example
*
* .prepend(newElements [,animate] [,callback])
*
* @example <caption>Example: Prepending a new element</caption>
*
* // Create a new element
*
* var newElement = document.createElement('div');
* newElement.classList.add('mix');
*
* // Insert the element into the container
*
* mixer.prepend(newElement)
* .then(function(state) {
* console.log(state.show[0] === newElement); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string)} newElements
* A reference to a single element to insert, an array-like collection of elements, or an HTML string representing a single element.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
prepend: function() {
var self = this,
args = self.parseInsertArgs(arguments);
return self.insert(0, args.command.collection, args.animate, args.callback);
},
/**
* Inserts one or more new elements into the container after all existing targets.
*
* @example
*
* .append(newElements [,animate] [,callback])
*
* @example <caption>Example: Appending a new element</caption>
*
* // Create a new element
*
* var newElement = document.createElement('div');
* newElement.classList.add('mix');
*
* // Insert the element into the container
*
* mixer.append(newElement)
* .then(function(state) {
* console.log(state.show[state.show.length - 1] === newElement); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string)} newElements
* A reference to a single element to insert, an array-like collection of elements, or an HTML string representing a single element.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
append: function() {
var self = this,
args = self.parseInsertArgs(arguments);
return self.insert(self.state.totalTargets, args.command.collection, args.animate, args.callback);
},
/**
* Removes one or more existing target elements from the container.
*
* @example
*
* .remove(elements [, animate] [, callback])
*
* @example <caption>Example 1: Removing an element by reference</caption>
*
* var elementToRemove = containerEl.firstElementChild;
*
* mixer.remove(elementToRemove)
* .then(function(state) {
* console.log(state.targets.indexOf(elementToRemove) === -1); // true
* });
*
* @example <caption>Example 2: Removing a collection of elements by reference</caption>
*
* var elementsToRemove = containerEl.querySelectorAll('.category-a');
*
* console.log(elementsToRemove.length) // 3
*
* mixer.remove(elementsToRemove)
* .then(function() {
* console.log(containerEl.querySelectorAll('.category-a').length); // 0
* });
*
* @example <caption>Example 3: Removing one or more elements by selector</caption>
*
* mixer.remove('.category-a')
* .then(function() {
* console.log(containerEl.querySelectorAll('.category-a').length); // 0
* });
*
* @example <caption>Example 4: Removing an element by index</caption>
*
* console.log(mixer.getState.totalShow); // 4
*
* // Remove the element at index 3
*
* mixer.remove(3)
* .then(function(state) {
* console.log(state.totalShow); // 3
* console.log(state.show[3]); // undefined
* });
*
*
* @public
* @instance
* @since 3.0.0
* @param {(HTMLElement|Array.<HTMLElement>|string|number)} elements
* A reference to a single element to remove, an array-like collection of elements, a selector string, or the index of an element to remove.
* @param {boolean} [animate=true]
* An optional boolean dictating whether the operation should animate, or occur syncronously with no animation. `true` by default.
* @param {function} [callback=null]
* An optional callback function to be invoked after the operation has completed.
* @return {Promise.<mixitup.State>}
* A promise resolving with the current state object.
*/
remove: function() {
var self = this,
args = self.parseRemoveArgs(arguments);
return self.multimix({
remove: args.command
}, args.animate, args.callback);
},
/**
* Retrieves the the value of any property or sub-object within the current
* mixitup configuration, or the whole configuration object.
*
* @example
*
* .getConfig([stringKey])
*
* @example <caption>Example 1: retrieve the entire configuration object</caption>
*
* var config = mixer.getConfig(); // Config { ... }
*
* @example <caption>Example 2: retrieve a named sub-object of configuration object</caption>
*
* var animation = mixer.getConfig('animation'); // ConfigAnimation { ... }
*
* @example <caption>Example 3: retrieve a value of configuration object via a dot-notation string key</caption>
*
* var effects = mixer.getConfig('animation.effects'); // 'fade scale'
*
* @public
* @instance
* @since 2.0.0
* @param {string} [stringKey] A "dot-notation" string key
* @return {*}
*/
getConfig: function(stringKey) {
var self = this,
value = null;
if (!stringKey) {
value = self.config;
} else {
value = h.getProperty(self.config, stringKey);
}
return self.callFilters('valueGetConfig', value, arguments);
},
/**
* Updates the configuration of the mixer, after it has been instantiated.
*
* See the Configuration Object documentation for a full list of avilable
* configuration options.
*
* @example
*
* .configure(config)
*
* @example <caption>Example 1: Updating animation options</caption>
*
* mixer.configure({
* animation: {
* effects: 'fade translateX(-100%)',
* duration: 300
* }
* });
*
* @example <caption>Example 2: Removing a callback after it has been set</caption>
*
* var mixer;
*
* function handleMixEndOnce() {
* // Do something ..
*
* // Then nullify the callback
*
* mixer.configure({
* callbacks: {
* onMixEnd: null
* }
* });
* };
*
* // Instantiate a mixer with a callback defined
*
* mixer = mixitup(containerEl, {
* callbacks: {
* onMixEnd: handleMixEndOnce
* }
* });
*
* @public
* @instance
* @since 3.0.0
* @param {object} config
* An object containing one of more configuration options.
* @return {void}
*/
configure: function(config) {
var self = this;
self.callActions('beforeConfigure', arguments);
h.extend(self.config, config, true, true);
self.callActions('afterConfigure', arguments);
},
/**
* Returns an object containing information about the current state of the
* mixer. See the State Object documentation for more information.
*
* NB: State objects are immutable and should therefore be regenerated
* after any operation.
*
* @example
*
* .getState();
*
* @example <caption>Example: Retrieving a state object</caption>
*
* var state = mixer.getState();
*
* console.log(state.totalShow + 'targets are currently shown');
*
* @public
* @instance
* @since 2.0.0
* @return {mixitup.State} An object reflecting the current state of the mixer.
*/
getState: function() {
var self = this,
state = null;
state = new mixitup.State();
h.extend(state, self.state);
h.freeze(state);
return self.callFilters('stateGetState', state, arguments);
},
/**
* Forces the re-indexing all targets within the container.
*
* This should only be used if some other piece of code in your application
* has manipulated the contents of your container, which should be avoided.
*
* If you need to add or remove target elements from the container, use
* the built-in `.insert()` or `.remove()` methods, and MixItUp will keep
* itself up to date.
*
* @example
*
* .forceRefresh()
*
* @example <caption>Example: Force refreshing the mixer after external DOM manipulation</caption>
*
* console.log(mixer.getState().totalShow); // 3
*
* // An element is removed from the container via some external DOM manipulation code:
*
* containerEl.removeChild(containerEl.firstElementChild);
*
* // The mixer does not know that the number of targets has changed:
*
* console.log(mixer.getState().totalShow); // 3
*
* mixer.forceRefresh();
*
* // After forceRefresh, the mixer is in sync again:
*
* console.log(mixer.getState().totalShow); // 2
*
* @public
* @instance
* @since 2.1.2
* @return {void}
*/
forceRefresh: function() {
var self = this;
self.indexTargets();
},
/**
* Forces the re-rendering of all targets when using the Dataset API.
*
* By default, targets are only re-rendered when `data.dirtyCheck` is
* enabled, and an item's data has changed when `dataset()` is called.
*
* The `forceRender()` method allows for the re-rendering of all targets
* in response to some arbitrary event, such as the changing of the target
* render function.
*
* Targets are rendered against their existing data.
*
* @example
*
* .forceRender()
*
* @example <caption>Example: Force render targets after changing the target render function</caption>
*
* console.log(container.innerHTML); // ... &lt;span class="mix"&gt;Foo&lt;/span&gt; ...
*
* mixer.configure({
* render: {
* target: (item) => `&lt;a href="/${item.slug}/" class="mix"&gt;${item.title}&lt;/a&gt;`
* }
* });
*
* mixer.forceRender();
*
* console.log(container.innerHTML); // ... &lt;a href="/foo/" class="mix"&gt;Foo&lt;/a&gt; ...
*
* @public
* @instance
* @since 3.2.1
* @return {void}
*/
forceRender: function() {
var self = this,
target = null,
el = null,
id = '';
for (id in self.cache) {
target = self.cache[id];
el = target.render(target.data);
if (el !== target.dom.el) {
// Update target element reference
if (target.isInDom) {
target.unbindEvents();
self.dom.parent.replaceChild(el, target.dom.el);
}
if (!target.isShown) {
el.style.display = 'none';
}
target.dom.el = el;
if (target.isInDom) {
target.bindEvents();
}
}
}
self.state = self.buildState(self.lastOperation);
},
/**
* Removes mixitup functionality from the container, unbinds all control
* event handlers, and deletes the mixer instance from MixItUp's internal
* cache.
*
* This should be performed whenever a mixer's container is removed from
* the DOM, such as during a page change in a single page application,
* or React's `componentWillUnmount()`.
*
* @example
*
* .destroy([cleanUp])
*
* @example <caption>Example: Destroying the mixer before removing its container element</caption>
*
* mixer.destroy();
*
* containerEl.parentElement.removeChild(containerEl);
*
* @public
* @instance
* @since 2.0.0
* @param {boolean} [cleanUp=false]
* An optional boolean dictating whether or not to clean up any inline `display: none;` styling applied to hidden targets.
* @return {void}
*/
destroy: function(cleanUp) {
var self = this,
control = null,
target = null,
i = 0;
self.callActions('beforeDestroy', arguments);
for (i = 0; control = self.controls[i]; i++) {
control.removeBinding(self);
}
for (i = 0; target = self.targets[i]; i++) {
if (cleanUp) {
target.show();
}
target.unbindEvents();
}
if (self.dom.container.id.match(/^MixItUp/)) {
self.dom.container.removeAttribute('id');
}
delete mixitup.instances[self.id];
self.callActions('afterDestroy', arguments);
}
});
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.IMoveData = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.posIn = null;
this.posOut = null;
this.operation = null;
this.callback = null;
this.statusChange = '';
this.duration = -1;
this.staggerIndex = -1;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.IMoveData);
mixitup.IMoveData.prototype = Object.create(mixitup.Base.prototype);
mixitup.IMoveData.prototype.constructor = mixitup.IMoveData;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.TargetDom = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.el = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.TargetDom);
mixitup.TargetDom.prototype = Object.create(mixitup.Base.prototype);
mixitup.TargetDom.prototype.constructor = mixitup.TargetDom;
/**
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Target = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.id = '';
this.sortString = '';
this.mixer = null;
this.callback = null;
this.isShown = false;
this.isBound = false;
this.isExcluded = false;
this.isInDom = false;
this.handler = null;
this.operation = null;
this.data = null;
this.dom = new mixitup.TargetDom();
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Target);
mixitup.Target.prototype = Object.create(mixitup.Base.prototype);
h.extend(mixitup.Target.prototype, {
constructor: mixitup.Target,
/**
* Initialises a newly instantiated Target.
*
* @private
* @instance
* @since 3.0.0
* @param {(Element|null)} el
* @param {object} mixer
* @param {object} [data]
* @return {void}
*/
init: function(el, mixer, data) {
var self = this,
id = '';
self.callActions('beforeInit', arguments);
self.mixer = mixer;
if (!el) {
// If no element is provided, render it
el = self.render(data);
}
self.cacheDom(el);
self.bindEvents();
if (self.dom.el.style.display !== 'none') {
self.isShown = true;
}
if (data && mixer.config.data.uidKey) {
if (typeof (id = data[mixer.config.data.uidKey]) === 'undefined' || id.toString().length < 1) {
throw new TypeError(mixitup.messages.errorDatasetInvalidUidKey({
uidKey: mixer.config.data.uidKey
}));
}
self.id = id;
self.data = data;
mixer.cache[id] = self;
}
self.callActions('afterInit', arguments);
},
/**
* Renders the target element using a user-defined renderer function.
*
* @private
* @instance
* @since 3.1.4
* @param {object} data
* @return {void}
*/
render: function(data) {
var self = this,
render = null,
el = null,
temp = null,
output = '';
self.callActions('beforeRender', arguments);
render = self.callFilters('renderRender', self.mixer.config.render.target, arguments);
if (typeof render !== 'function') {
throw new TypeError(mixitup.messages.errorDatasetRendererNotSet());
}
output = render(data);
if (output && typeof output === 'object' && h.isElement(output)) {
el = output;
} else if (typeof output === 'string') {
temp = document.createElement('div');
temp.innerHTML = output;
el = temp.firstElementChild;
}
return self.callFilters('elRender', el, arguments);
},
/**
* Caches references of DOM elements neccessary for the target's functionality.
*
* @private
* @instance
* @since 3.0.0
* @param {Element} el
* @return {void}
*/
cacheDom: function(el) {
var self = this;
self.callActions('beforeCacheDom', arguments);
self.dom.el = el;
self.callActions('afterCacheDom', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {string} attributeName
* @return {void}
*/
getSortString: function(attributeName) {
var self = this,
value = self.dom.el.getAttribute('data-' + attributeName) || '';
self.callActions('beforeGetSortString', arguments);
value = isNaN(value * 1) ?
value.toLowerCase() :
value * 1;
self.sortString = value;
self.callActions('afterGetSortString', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
show: function() {
var self = this;
self.callActions('beforeShow', arguments);
if (!self.isShown) {
self.dom.el.style.display = '';
self.isShown = true;
}
self.callActions('afterShow', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
hide: function() {
var self = this;
self.callActions('beforeHide', arguments);
if (self.isShown) {
self.dom.el.style.display = 'none';
self.isShown = false;
}
self.callActions('afterHide', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {mixitup.IMoveData} moveData
* @return {void}
*/
move: function(moveData) {
var self = this;
self.callActions('beforeMove', arguments);
if (!self.isExcluded) {
self.mixer.targetsMoved++;
}
self.applyStylesIn(moveData);
requestAnimationFrame(function() {
self.applyStylesOut(moveData);
});
self.callActions('afterMove', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {object} posData
* @param {number} multiplier
* @return {void}
*/
applyTween: function(posData, multiplier) {
var self = this,
propertyName = '',
tweenData = null,
posIn = posData.posIn,
currentTransformValues = [],
currentValues = new mixitup.StyleData(),
i = -1;
self.callActions('beforeApplyTween', arguments);
currentValues.x = posIn.x;
currentValues.y = posIn.y;
if (multiplier === 0) {
self.hide();
} else if (!self.isShown) {
self.show();
}
for (i = 0; propertyName = mixitup.features.TWEENABLE[i]; i++) {
tweenData = posData.tweenData[propertyName];
if (propertyName === 'x') {
if (!tweenData) continue;
currentValues.x = posIn.x + (tweenData * multiplier);
} else if (propertyName === 'y') {
if (!tweenData) continue;
currentValues.y = posIn.y + (tweenData * multiplier);
} else if (tweenData instanceof mixitup.TransformData) {
if (!tweenData.value) continue;
currentValues[propertyName].value =
posIn[propertyName].value + (tweenData.value * multiplier);
currentValues[propertyName].unit = tweenData.unit;
currentTransformValues.push(
propertyName + '(' + currentValues[propertyName].value + tweenData.unit + ')'
);
} else {
if (!tweenData) continue;
currentValues[propertyName] = posIn[propertyName] + (tweenData * multiplier);
self.dom.el.style[propertyName] = currentValues[propertyName];
}
}
if (currentValues.x || currentValues.y) {
currentTransformValues.unshift('translate(' + currentValues.x + 'px, ' + currentValues.y + 'px)');
}
if (currentTransformValues.length) {
self.dom.el.style[mixitup.features.transformProp] = currentTransformValues.join(' ');
}
self.callActions('afterApplyTween', arguments);
},
/**
* Applies the initial styling to a target element before any transition
* is applied.
*
* @private
* @instance
* @param {mixitup.IMoveData} moveData
* @return {void}
*/
applyStylesIn: function(moveData) {
var self = this,
posIn = moveData.posIn,
isFading = self.mixer.effectsIn.opacity !== 1,
transformValues = [];
self.callActions('beforeApplyStylesIn', arguments);
transformValues.push('translate(' + posIn.x + 'px, ' + posIn.y + 'px)');
if (self.mixer.config.animation.animateResizeTargets) {
if (moveData.statusChange !== 'show') {
// Don't apply posIn width or height or showing, as will be 0
self.dom.el.style.width = posIn.width + 'px';
self.dom.el.style.height = posIn.height + 'px';
}
self.dom.el.style.marginRight = posIn.marginRight + 'px';
self.dom.el.style.marginBottom = posIn.marginBottom + 'px';
}
isFading && (self.dom.el.style.opacity = posIn.opacity);
if (moveData.statusChange === 'show') {
transformValues = transformValues.concat(self.mixer.transformIn);
}
self.dom.el.style[mixitup.features.transformProp] = transformValues.join(' ');
self.callActions('afterApplyStylesIn', arguments);
},
/**
* Applies a transition followed by the final styles for the element to
* transition towards.
*
* @private
* @instance
* @param {mixitup.IMoveData} moveData
* @return {void}
*/
applyStylesOut: function(moveData) {
var self = this,
transitionRules = [],
transformValues = [],
isResizing = self.mixer.config.animation.animateResizeTargets,
isFading = typeof self.mixer.effectsIn.opacity !== 'undefined';
self.callActions('beforeApplyStylesOut', arguments);
// Build the transition rules
transitionRules.push(self.writeTransitionRule(
mixitup.features.transformRule,
moveData.staggerIndex
));
if (moveData.statusChange !== 'none') {
transitionRules.push(self.writeTransitionRule(
'opacity',
moveData.staggerIndex,
moveData.duration
));
}
if (isResizing) {
transitionRules.push(self.writeTransitionRule(
'width',
moveData.staggerIndex,
moveData.duration
));
transitionRules.push(self.writeTransitionRule(
'height',
moveData.staggerIndex,
moveData.duration
));
transitionRules.push(self.writeTransitionRule(
'margin',
moveData.staggerIndex,
moveData.duration
));
}
// If no callback was provided, the element will
// not transition in any way so tag it as "immovable"
if (!moveData.callback) {
self.mixer.targetsImmovable++;
if (self.mixer.targetsMoved === self.mixer.targetsImmovable) {
// If the total targets moved is equal to the
// number of immovable targets, the operation
// should be considered finished
self.mixer.cleanUp(moveData.operation);
}
return;
}
// If the target will transition in some fasion,
// assign a callback function
self.operation = moveData.operation;
self.callback = moveData.callback;
// As long as the target is not excluded, increment
// the total number of targets bound
!self.isExcluded && self.mixer.targetsBound++;
// Tag the target as bound to differentiate from transitionEnd
// events that may come from stylesheet driven effects
self.isBound = true;
// Apply the transition
self.applyTransition(transitionRules);
// Apply width, height and margin negation
if (isResizing && moveData.posOut.width > 0 && moveData.posOut.height > 0) {
self.dom.el.style.width = moveData.posOut.width + 'px';
self.dom.el.style.height = moveData.posOut.height + 'px';
self.dom.el.style.marginRight = moveData.posOut.marginRight + 'px';
self.dom.el.style.marginBottom = moveData.posOut.marginBottom + 'px';
}
if (!self.mixer.config.animation.nudge && moveData.statusChange === 'hide') {
// If we're not nudging, the translation should be
// applied before any other transforms to prevent
// lateral movement
transformValues.push('translate(' + moveData.posOut.x + 'px, ' + moveData.posOut.y + 'px)');
}
// Apply fade
switch (moveData.statusChange) {
case 'hide':
isFading && (self.dom.el.style.opacity = self.mixer.effectsOut.opacity);
transformValues = transformValues.concat(self.mixer.transformOut);
break;
case 'show':
isFading && (self.dom.el.style.opacity = 1);
}
if (
self.mixer.config.animation.nudge ||
(!self.mixer.config.animation.nudge && moveData.statusChange !== 'hide')
) {
// Opposite of above - apply translate after
// other transform
transformValues.push('translate(' + moveData.posOut.x + 'px, ' + moveData.posOut.y + 'px)');
}
// Apply transforms
self.dom.el.style[mixitup.features.transformProp] = transformValues.join(' ');
self.callActions('afterApplyStylesOut', arguments);
},
/**
* Combines the name of a CSS property with the appropriate duration and delay
* values to created a valid transition rule.
*
* @private
* @instance
* @since 3.0.0
* @param {string} property
* @param {number} staggerIndex
* @param {number} duration
* @return {string}
*/
writeTransitionRule: function(property, staggerIndex, duration) {
var self = this,
delay = self.getDelay(staggerIndex),
rule = '';
rule = property + ' ' +
(duration > 0 ? duration : self.mixer.config.animation.duration) + 'ms ' +
delay + 'ms ' +
(property === 'opacity' ? 'linear' : self.mixer.config.animation.easing);
return self.callFilters('ruleWriteTransitionRule', rule, arguments);
},
/**
* Calculates the transition delay for each target element based on its index, if
* staggering is applied. If defined, A custom `animation.staggerSeqeuence`
* function can be used to manipulate the order of indices to produce custom
* stagger effects (e.g. for use in a grid with irregular row lengths).
*
* @private
* @instance
* @since 2.0.0
* @param {number} index
* @return {number}
*/
getDelay: function(index) {
var self = this,
delay = -1;
if (typeof self.mixer.config.animation.staggerSequence === 'function') {
index = self.mixer.config.animation.staggerSequence.call(self, index, self.state);
}
delay = !!self.mixer.staggerDuration ? index * self.mixer.staggerDuration : 0;
return self.callFilters('delayGetDelay', delay, arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {string[]} rules
* @return {void}
*/
applyTransition: function(rules) {
var self = this,
transitionString = rules.join(', ');
self.callActions('beforeApplyTransition', arguments);
self.dom.el.style[mixitup.features.transitionProp] = transitionString;
self.callActions('afterApplyTransition', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Event} e
* @return {void}
*/
handleTransitionEnd: function(e) {
var self = this,
propName = e.propertyName,
canResize = self.mixer.config.animation.animateResizeTargets;
self.callActions('beforeHandleTransitionEnd', arguments);
if (
self.isBound &&
e.target.matches(self.mixer.config.selectors.target) &&
(
propName.indexOf('transform') > -1 ||
propName.indexOf('opacity') > -1 ||
canResize && propName.indexOf('height') > -1 ||
canResize && propName.indexOf('width') > -1 ||
canResize && propName.indexOf('margin') > -1
)
) {
self.callback.call(self, self.operation);
self.isBound = false;
self.callback = null;
self.operation = null;
}
self.callActions('afterHandleTransitionEnd', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {Event} e
* @return {void}
*/
eventBus: function(e) {
var self = this;
self.callActions('beforeEventBus', arguments);
switch (e.type) {
case 'webkitTransitionEnd':
case 'transitionend':
self.handleTransitionEnd(e);
}
self.callActions('afterEventBus', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
unbindEvents: function() {
var self = this;
self.callActions('beforeUnbindEvents', arguments);
h.off(self.dom.el, 'webkitTransitionEnd', self.handler);
h.off(self.dom.el, 'transitionend', self.handler);
self.callActions('afterUnbindEvents', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
bindEvents: function() {
var self = this,
transitionEndEvent = '';
self.callActions('beforeBindEvents', arguments);
transitionEndEvent = mixitup.features.transitionPrefix === 'webkit' ? 'webkitTransitionEnd' : 'transitionend';
self.handler = function(e) {
return self.eventBus(e);
};
h.on(self.dom.el, transitionEndEvent, self.handler);
self.callActions('afterBindEvents', arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @param {boolean} [getBox]
* @return {PosData}
*/
getPosData: function(getBox) {
var self = this,
styles = {},
rect = null,
posData = new mixitup.StyleData();
self.callActions('beforeGetPosData', arguments);
posData.x = self.dom.el.offsetLeft;
posData.y = self.dom.el.offsetTop;
if (self.mixer.config.animation.animateResizeTargets || getBox) {
rect = self.dom.el.getBoundingClientRect();
posData.top = rect.top;
posData.right = rect.right;
posData.bottom = rect.bottom;
posData.left = rect.left;
posData.width = rect.width;
posData.height = rect.height;
}
if (self.mixer.config.animation.animateResizeTargets) {
styles = window.getComputedStyle(self.dom.el);
posData.marginBottom = parseFloat(styles.marginBottom);
posData.marginRight = parseFloat(styles.marginRight);
}
return self.callFilters('posDataGetPosData', posData, arguments);
},
/**
* @private
* @instance
* @since 3.0.0
* @return {void}
*/
cleanUp: function() {
var self = this;
self.callActions('beforeCleanUp', arguments);
self.dom.el.style[mixitup.features.transformProp] = '';
self.dom.el.style[mixitup.features.transitionProp] = '';
self.dom.el.style.opacity = '';
if (self.mixer.config.animation.animateResizeTargets) {
self.dom.el.style.width = '';
self.dom.el.style.height = '';
self.dom.el.style.marginRight = '';
self.dom.el.style.marginBottom = '';
}
self.callActions('afterCleanUp', arguments);
}
});
/**
* A jQuery-collection-like wrapper around one or more `mixitup.Mixer` instances
* allowing simultaneous control of said instances similar to the MixItUp 2 API.
*
* @example
* new mixitup.Collection(instances)
*
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
* @param {mixitup.Mixer[]} instances
*/
mixitup.Collection = function(instances) {
var instance = null,
i = -1;
this.callActions('beforeConstruct');
for (i = 0; instance = instances[i]; i++) {
this[i] = instance;
}
this.length = instances.length;
this.callActions('afterConstruct');
h.freeze(this);
};
mixitup.BaseStatic.call(mixitup.Collection);
mixitup.Collection.prototype = Object.create(mixitup.Base.prototype);
h.extend(mixitup.Collection.prototype,
/** @lends mixitup.Collection */
{
constructor: mixitup.Collection,
/**
* Calls a method on all instances in the collection by passing the method
* name as a string followed by any applicable parameters to be curried into
* to the method.
*
* @example
* .mixitup(methodName[,arg1][,arg2..]);
*
* @example
* var collection = new Collection([mixer1, mixer2]);
*
* return collection.mixitup('filter', '.category-a')
* .then(function(states) {
* state.forEach(function(state) {
* console.log(state.activeFilter.selector); // .category-a
* });
* });
*
* @public
* @instance
* @since 3.0.0
* @param {string} methodName
* @return {Promise<Array<mixitup.State>>}
*/
mixitup: function(methodName) {
var self = this,
instance = null,
args = Array.prototype.slice.call(arguments),
tasks = [],
i = -1;
this.callActions('beforeMixitup');
args.shift();
for (i = 0; instance = self[i]; i++) {
tasks.push(instance[methodName].apply(instance, args));
}
return self.callFilters('promiseMixitup', h.all(tasks, mixitup.libraries), arguments);
}
});
/**
* `mixitup.Operation` objects contain all data neccessary to describe the full
* lifecycle of any MixItUp operation. They can be used to compute and store an
* operation for use at a later time (e.g. programmatic tweening).
*
* @constructor
* @namespace
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Operation = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.id = '';
this.args = [];
this.command = null;
this.showPosData = [];
this.toHidePosData = [];
this.startState = null;
this.newState = null;
this.docState = null;
this.willSort = false;
this.willChangeLayout = false;
this.hasEffect = false;
this.hasFailed = false;
this.triggerElement = null;
this.show = [];
this.hide = [];
this.matching = [];
this.toShow = [];
this.toHide = [];
this.toMove = [];
this.toRemove = [];
this.startOrder = [];
this.newOrder = [];
this.startSort = null;
this.newSort = null;
this.startFilter = null;
this.newFilter = null;
this.startDataset = null;
this.newDataset = null;
this.viewportDeltaX = 0;
this.viewportDeltaY = 0;
this.startX = 0;
this.startY = 0;
this.startHeight = 0;
this.startWidth = 0;
this.newX = 0;
this.newY = 0;
this.newHeight = 0;
this.newWidth = 0;
this.startContainerClassName = '';
this.startDisplay = '';
this.newContainerClassName = '';
this.newDisplay = '';
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Operation);
mixitup.Operation.prototype = Object.create(mixitup.Base.prototype);
mixitup.Operation.prototype.constructor = mixitup.Operation;
/**
* `mixitup.State` objects expose various pieces of data detailing the state of
* a MixItUp instance. They are provided at the start and end of any operation via
* callbacks and events, with the most recent state stored between operations
* for retrieval at any time via the API.
*
* @constructor
* @namespace
* @memberof mixitup
* @public
* @since 3.0.0
*/
mixitup.State = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/**
* The ID of the mixer instance.
*
* @name id
* @memberof mixitup.State
* @instance
* @type {string}
* @default ''
*/
this.id = '';
/**
* The currently active filter command as set by a control click or API call.
*
* @name activeFilter
* @memberof mixitup.State
* @instance
* @type {mixitup.CommandFilter}
* @default null
*/
this.activeFilter = null;
/**
* The currently active sort command as set by a control click or API call.
*
* @name activeSort
* @memberof mixitup.State
* @instance
* @type {mixitup.CommandSort}
* @default null
*/
this.activeSort = null;
/**
* The current layout-specific container class name, if applied.
*
* @name activeContainerClassName
* @memberof mixitup.State
* @instance
* @type {string}
* @default ''
*/
this.activeContainerClassName = '';
/**
* A reference to the container element that the mixer is instantiated on.
*
* @name container
* @memberof mixitup.State
* @instance
* @type {Element}
* @default null
*/
this.container = null;
/**
* An array of all target elements indexed by the mixer.
*
* @name targets
* @memberof mixitup.State
* @instance
* @type {Array.<Element>}
* @default []
*/
this.targets = [];
/**
* An array of all target elements not matching the current filter.
*
* @name hide
* @memberof mixitup.State
* @instance
* @type {Array.<Element>}
* @default []
*/
this.hide = [];
/**
* An array of all target elements matching the current filter and any additional
* limits applied such as pagination.
*
* @name show
* @memberof mixitup.State
* @instance
* @type {Array.<Element>}
* @default []
*/
this.show = [];
/**
* An array of all target elements matching the current filter irrespective of
* any additional limits applied such as pagination.
*
* @name matching
* @memberof mixitup.State
* @instance
* @type {Array.<Element>}
* @default []
*/
this.matching = [];
/**
* An integer representing the total number of target elements indexed by the
* mixer. Equivalent to `state.targets.length`.
*
* @name totalTargets
* @memberof mixitup.State
* @instance
* @type {number}
* @default -1
*/
this.totalTargets = -1;
/**
* An integer representing the total number of target elements matching the
* current filter and any additional limits applied such as pagination.
* Equivalent to `state.show.length`.
*
* @name totalShow
* @memberof mixitup.State
* @instance
* @type {number}
* @default -1
*/
this.totalShow = -1;
/**
* An integer representing the total number of target elements not matching
* the current filter. Equivalent to `state.hide.length`.
*
* @name totalHide
* @memberof mixitup.State
* @instance
* @type {number}
* @default -1
*/
this.totalHide = -1;
/**
* An integer representing the total number of target elements matching the
* current filter irrespective of any other limits applied such as pagination.
* Equivalent to `state.matching.length`.
*
* @name totalMatching
* @memberof mixitup.State
* @instance
* @type {number}
* @default -1
*/
this.totalMatching = -1;
/**
* A boolean indicating whether the last operation "failed", i.e. no targets
* could be found matching the filter.
*
* @name hasFailed
* @memberof mixitup.State
* @instance
* @type {boolean}
* @default false
*/
this.hasFailed = false;
/**
* The DOM element that was clicked if the last operation was triggered by the
* clicking of a control and not an API call.
*
* @name triggerElement
* @memberof mixitup.State
* @instance
* @type {Element|null}
* @default null
*/
this.triggerElement = null;
/**
* The currently active dataset underlying the rendered targets, if the
* dataset API is in use.
*
* @name activeDataset
* @memberof mixitup.State
* @instance
* @type {Array.<object>}
* @default null
*/
this.activeDataset = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.State);
mixitup.State.prototype = Object.create(mixitup.Base.prototype);
mixitup.State.prototype.constructor = mixitup.State;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.UserInstruction = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
this.command = {};
this.animate = false;
this.callback = null;
this.callActions('afterConstruct');
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.UserInstruction);
mixitup.UserInstruction.prototype = Object.create(mixitup.Base.prototype);
mixitup.UserInstruction.prototype.constructor = mixitup.UserInstruction;
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
*/
mixitup.Messages = function() {
mixitup.Base.call(this);
this.callActions('beforeConstruct');
/* Errors
----------------------------------------------------------------------------- */
this.ERROR_FACTORY_INVALID_CONTAINER =
'[MixItUp] An invalid selector or element reference was passed to the mixitup factory function';
this.ERROR_FACTORY_CONTAINER_NOT_FOUND =
'[MixItUp] The provided selector yielded no container element';
this.ERROR_CONFIG_INVALID_ANIMATION_EFFECTS =
'[MixItUp] Invalid value for `animation.effects`';
this.ERROR_CONFIG_INVALID_CONTROLS_SCOPE =
'[MixItUp] Invalid value for `controls.scope`';
this.ERROR_CONFIG_INVALID_PROPERTY =
'[MixitUp] Invalid configuration object property "${erroneous}"${suggestion}';
this.ERROR_CONFIG_INVALID_PROPERTY_SUGGESTION =
'. Did you mean "${probableMatch}"?';
this.ERROR_CONFIG_DATA_UID_KEY_NOT_SET =
'[MixItUp] To use the dataset API, a UID key must be specified using `data.uidKey`';
this.ERROR_DATASET_INVALID_UID_KEY =
'[MixItUp] The specified UID key "${uidKey}" is not present on one or more dataset items';
this.ERROR_DATASET_DUPLICATE_UID =
'[MixItUp] The UID "${uid}" was found on two or more dataset items. UIDs must be unique.';
this.ERROR_INSERT_INVALID_ARGUMENTS =
'[MixItUp] Please provider either an index or a sibling and position to insert, not both';
this.ERROR_INSERT_PREEXISTING_ELEMENT =
'[MixItUp] An element to be inserted already exists in the container';
this.ERROR_FILTER_INVALID_ARGUMENTS =
'[MixItUp] Please provide either a selector or collection `.filter()`, not both';
this.ERROR_DATASET_NOT_SET =
'[MixItUp] To use the dataset API with pre-rendered targets, a starting dataset must be set using `load.dataset`';
this.ERROR_DATASET_PRERENDERED_MISMATCH =
'[MixItUp] `load.dataset` does not match pre-rendered targets';
this.ERROR_DATASET_RENDERER_NOT_SET =
'[MixItUp] To insert an element via the dataset API, a target renderer function must be provided to `render.target`';
this.ERROR_SORT_NON_EXISTENT_ELEMENT =
'[MixItUp] An element to be sorted does not already exist in the container';
/* Warnings
----------------------------------------------------------------------------- */
this.WARNING_FACTORY_PREEXISTING_INSTANCE =
'[MixItUp] WARNING: This element already has an active MixItUp instance. The provided configuration object will be ignored.' +
' If you wish to perform additional methods on this instance, please create a reference.';
this.WARNING_INSERT_NO_ELEMENTS =
'[MixItUp] WARNING: No valid elements were passed to `.insert()`';
this.WARNING_REMOVE_NO_ELEMENTS =
'[MixItUp] WARNING: No valid elements were passed to `.remove()`';
this.WARNING_MULTIMIX_INSTANCE_QUEUE_FULL =
'[MixItUp] WARNING: An operation was requested but the MixItUp instance was busy. The operation was rejected because the ' +
'queue is full or queuing is disabled.';
this.WARNING_GET_OPERATION_INSTANCE_BUSY =
'[MixItUp] WARNING: Operations can be be created while the MixItUp instance is busy.';
this.WARNING_NO_PROMISE_IMPLEMENTATION =
'[MixItUp] WARNING: No Promise implementations could be found. If you wish to use promises with MixItUp please install' +
' an ES6 Promise polyfill.';
this.WARNING_INCONSISTENT_SORTING_ATTRIBUTES =
'[MixItUp] WARNING: The requested sorting data attribute "${attribute}" was not present on one or more target elements' +
' which may product unexpected sort output';
this.callActions('afterConstruct');
this.compileTemplates();
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Messages);
mixitup.Messages.prototype = Object.create(mixitup.Base.prototype);
mixitup.Messages.prototype.constructor = mixitup.Messages;
/**
* @return {void}
*/
mixitup.Messages.prototype.compileTemplates = function() {
var errorKey = '';
var errorMessage = '';
for (errorKey in this) {
if (typeof (errorMessage = this[errorKey]) !== 'string') continue;
this[h.camelCase(errorKey)] = h.template(errorMessage);
}
};
mixitup.messages = new mixitup.Messages();
/**
* @constructor
* @memberof mixitup
* @private
* @since 3.0.0
* @param {mixitup.Mixer} mixer
*/
mixitup.Facade = function Mixer(mixer) {
mixitup.Base.call(this);
this.callActions('beforeConstruct', arguments);
this.configure = mixer.configure.bind(mixer);
this.show = mixer.show.bind(mixer);
this.hide = mixer.hide.bind(mixer);
this.filter = mixer.filter.bind(mixer);
this.toggleOn = mixer.toggleOn.bind(mixer);
this.toggleOff = mixer.toggleOff.bind(mixer);
this.sort = mixer.sort.bind(mixer);
this.changeLayout = mixer.changeLayout.bind(mixer);
this.multimix = mixer.multimix.bind(mixer);
this.dataset = mixer.dataset.bind(mixer);
this.tween = mixer.tween.bind(mixer);
this.insert = mixer.insert.bind(mixer);
this.insertBefore = mixer.insertBefore.bind(mixer);
this.insertAfter = mixer.insertAfter.bind(mixer);
this.prepend = mixer.prepend.bind(mixer);
this.append = mixer.append.bind(mixer);
this.remove = mixer.remove.bind(mixer);
this.destroy = mixer.destroy.bind(mixer);
this.forceRefresh = mixer.forceRefresh.bind(mixer);
this.forceRender = mixer.forceRender.bind(mixer);
this.isMixing = mixer.isMixing.bind(mixer);
this.getOperation = mixer.getOperation.bind(mixer);
this.getConfig = mixer.getConfig.bind(mixer);
this.getState = mixer.getState.bind(mixer);
this.callActions('afterConstruct', arguments);
h.freeze(this);
h.seal(this);
};
mixitup.BaseStatic.call(mixitup.Facade);
mixitup.Facade.prototype = Object.create(mixitup.Base.prototype);
mixitup.Facade.prototype.constructor = mixitup.Facade;
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = mixitup;
} else if (typeof define === 'function' && define.amd) {
define(function() {
return mixitup;
});
} else if (typeof window.mixitup === 'undefined' || typeof window.mixitup !== 'function') {
window.mixitup = mixitup;
}
mixitup.BaseStatic.call(mixitup.constructor);
mixitup.NAME = 'mixitup';
mixitup.CORE_VERSION = '3.3.1';
})(window);