/**!
* 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
Example 1: Creating a mixer instance with an element reference
* var containerEl = document.querySelector('.container');
*
* var mixer = mixitup(containerEl);
*
* @example Example 2: Creating a mixer instance with a selector string
* var mixer = mixitup('.container');
*
* @example Example 3: Passing a configuration object
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade scale(0.5)'
* }
* });
*
* @example Example 4: Passing an iframe reference
* 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 Example 1: Extending MixItUp with the Pagination Extension
*
* 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} tasks
* @param {object} libraries
* @return {Promise}
*/
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} 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 Example: Create a mixer with all animations disabled
* 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
* sandbox demo and try out the "Export config" button in the Animation options drop down.
*
* @example Example: Apply "fade" and "translateZ" effects to all animations
* // 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 Example: Apply downwards vertical translate to targets being filtered in
*
* 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 Example: Apply upwards vertical translate to targets being filtered out
*
* 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 Example: Apply an animation duration of 200ms to all mixitup animations
*
* 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 easings.net .
*
* @example Example 1: Apply "ease-in-out" easing to all animations
*
* var mixer = mixitup(containerEl, {
* animation: {
* easing: 'ease-in-out'
* }
* });
*
* @example Example 2: Apply a custom "cubic-bezier" easing function to all animations
* 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 Example: Prevent perspective from being applied to any 3D transforms
* 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 Example: Set a perspective distance of 2000px
* 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 Example: Set a perspective origin in the top-right of the container
* 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 Example: Disable queuing
* 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 Example: Allow a maximum of 5 operations in the queue at any time
* 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 Example 1: Disable the transitioning of the container height and/or width
* var mixer = mixitup(containerEl, {
* animation: {
* animateResizeContainer: false
* }
* });
*
* @example Example 2: Disable the transitioning of the container height and/or width for mobile devices only
* 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 Example: Enable the transitioning of target widths and heights
* 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 Example 1: Stagger target elements by column in a 3-column grid
* var mixer = mixitup(containerEl, {
* animation: {
* effects: 'fade stagger(100ms)',
* staggerSequence: function(i) {
* return i % 3;
* }
* }
* });
*
* @example Example 2: Using an algorithm to produce a more complex sequence
* 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 Example: Reverse the desired direction on any translate/rotate effect for targets being filtered out
* // 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 Example: Disable the "nudging" of targets being filtered in and out simulatenously
*
* 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 Example: Disable container height-clamping
*
* 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 Example: Disable container width-clamping
*
* 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 Example: Enabling `liveSort` to allow for re-sorting
*
* 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 Example: Adding an `onMixStart` callback function
* 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 Example: Adding an `onMixBusy` callback function
* 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 Example: Adding an `onMixEnd` callback function
* 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 Example: Adding an `onMixFail` callback function
* 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 `` 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 Example 1: Adding an `onMixClick` callback function
* var mixer = mixitup(containerEl, {
* callbacks: {
* onMixClick: function(state, originalEvent) {
* console.log('The control "' + this.innerText + '" was clicked');
* }
* }
* });
*
* @example Example 2: Using `onMixClick` to manipulate the original click event
* 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 Example 3: Using `onMixClick` to conditionally cancel operations
* 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 Example: Disabling controls
* 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 Example: Setting live controls
* 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 Example: Setting 'local' scoped controls
* 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 Example: Setting "and" toggle logic
* 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 Example 1: Setting the default toggle behavior to `'all'`
* 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 Example 2: Setting the default toggle behavior to `'none'`
* 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 Example 1: changing the `config.classNames.block` value
* var mixer = mixitup(containerEl, {
* classNames: {
* block: 'portfolio'
* }
* });
*
* // Active control output: "portfolio-control-active"
*
* @example Example 2: Removing `config.classNames.block`
* 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 Example 1: changing the `config.classNames.elementFilter` value
* var mixer = mixitup(containerEl, {
* classNames: {
* elementFilter: 'filter'
* }
* });
*
* // Active filter output: "mixitup-filter-active"
*
* @example Example 2: changing the `config.classNames.block` and `config.classNames.elementFilter` values
* 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 Example 1: changing the `config.classNames.elementSort` value
* var mixer = mixitup(containerEl, {
* classNames: {
* elementSort: 'sort'
* }
* });
*
* // Active sort output: "mixitup-sort-active"
*
* @example Example 2: changing the `config.classNames.block` and `config.classNames.elementSort` values
* 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 Example 1: changing the `config.classNames.elementMultimix` value
* var mixer = mixitup(containerEl, {
* classNames: {
* elementMultimix: 'multimix'
* }
* });
*
* // Active multimix output: "mixitup-multimix-active"
*
* @example Example 2: changing the `config.classNames.block` and `config.classNames.elementMultimix` values
* 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 Example 1: changing the `config.classNames.elementToggle` value
* var mixer = mixitup(containerEl, {
* classNames: {
* elementToggle: 'toggle'
* }
* });
*
* // Active toggle output: "mixitup-toggle-active"
*
* @example Example 2: changing the `config.classNames.block` and `config.classNames.elementToggle` values
* 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 Example: changing the delineator to match BEM convention
* 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 Example: changing both delineators to match BEM convention
* 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 Example: Setting the UID to `'id'`
* 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 Example: Enabling dirty checking
*
* 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 Example: Enabling debug mode
*
* 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 Example 1: Disabling warnings
*
* var mixer = mixitup(containerEl, {
* debug: {
* showWarnings: false
* }
* });
*
* @example Example 2: Disabling warnings based on environment
*
* 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 Example: Restricting targets to immediate children
*
* 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 Example 1: Specifying a container class name
*
* var mixer = mixitup(containerEl, {
* layout: {
* containerClassName: 'grid'
* }
* });
*
* @example Example 2: Changing the default class name with `.changeLayout()`
*
* 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 Example: Setting a `siblingBefore` reference element
*
* 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 Example: Setting an `siblingAfter` reference element
*
* 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 Example 1: Defining an initial filter selector to be applied on load
*
* // The mixer will show only those targets matching '.category-a' on load.
*
* var mixer = mixitup(containerEl, {
* load: {
* filter: '.category-a'
* }
* });
*
* @example Example 2: Hiding all targets on load
*
* // 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 Example: Defining sorting to be applied on load
*
* // 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 Example: Defining the initial underyling dataset
*
* 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.}
* @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 Example 1: Changing the target selector
*
* var mixer = mixitup(containerEl, {
* selectors: {
* target: '.portfolio-item'
* }
* });
*
* @example Example 2: Using an attribute selector as a target selector
*
* // 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 Example 1: Adding a `selectors.control` selector
*
* var mixer = mixitup(containerEl, {
* selectors: {
* control: '.mixitup-control'
* }
* });
*
* // Will not be handled:
* //
*
* // Will be handled:
* //
*/
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 Example 1: Using string concatenation
*
* var mixer = mixitup(containerEl, {
* render: {
* target: function(item) {
* return (
* '<div class="mix">' +
* '<h2>' + item.title + '</h2>' +
* '</div>'
* );
* }
* }
* });
*
* @example Example 2: Using an ES2015 template literal
*
* var mixer = mixitup(containerEl, {
* render: {
* target: function(item) {
* return (
* `<div class="mix">
* <h2>${item.title}</h2>
* </div>`
* );
* }
* }
* });
*
* @example Example 3: Using a Handlebars template
*
* var targetTemplate = Handlebars.compile('<div class="mix"><h2>{{title}}</h2></div>');
*
* var mixer = mixitup(containerEl, {
* render: {
* target: targetTemplate
* }
* });
*
* @example Example 4: Returning a DOM element
*
* 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 Example 1: Creating and passing the configuration object
* // 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 Example 2: Passing the configuration object inline
* // 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} 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} 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} 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.}
*/
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.}
*/
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.} 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.} 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 Example: Showing all targets
*
* mixer.show()
* .then(function(state) {
* console.log(state.totalShow === state.totalTargets); // true
* });
*
* @public
* @instance
* @since 3.0.0
* @return {Promise.}
*/
show: function() {
var self = this;
return self.filter('all');
},
/**
* A shorthand method for `.filter('none')`. Hides all targets in the container.
*
* @example
*
* .hide()
*
* @example Example: Hiding all targets
*
* 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.}
*/
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 Example: Checking the status of a mixer
*
* 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 Example 1: Filtering targets by a class selector
*
* mixer.filter('.category-a')
* .then(function(state) {
* console.log(state.totalShow === containerEl.querySelectorAll('.category-a').length); // true
* });
*
* @example Example 2: Filtering targets by an attribute selector
*
* mixer.filter('[data-category~="a"]')
* .then(function(state) {
* console.log(state.totalShow === containerEl.querySelectorAll('[data-category~="a"]').length); // true
* });
*
* @example Example 3: Filtering targets by a compound selector
*
* // 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 Example 4: Filtering via an element collection
*
* 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.)} 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.}
* 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 Example: Toggling on a filter selector
*
* 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.}
* 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 Example: Toggling off a filter selector
*
* 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.}
* 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 Example 1: Sorting by the default DOM order
*
* // 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 Example 2: Sorting by a custom data-attribute
*
* // 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 Example 3: Sorting by multiple attributes
*
* // 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 Example 4: Sorting by random
*
* mixer.sort('random')
* .then(function(state) {
* console.log(state.activeSort.order === 'random') // true
* });
*
* @example Example 5: Sorting via an element collection
*
* 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.)} 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.}
* 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 Example 1: Adding a new class name to the container
*
* mixer.changeLayout('container-list')
* .then(function(state) {
* console.log(state.activeContainerClass === 'container-list'); // true
* });
*
* @example Example 2: Removing a previously added class name from the container
*
* 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.}
* 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 Example 1: Rendering a dataset
*
* var myDataset = [
* {id: 1, ...},
* {id: 2, ...},
* {id: 3, ...}
* ];
*
* mixer.dataset(myDataset)
* .then(function(state) {
* console.log(state.totalShow === 3); // true
* });
*
* @example Example 2: Sorting a dataset
*
* // 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 Example 3: Removing an item from the dataset
*
* 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.} 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.}
* 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 Example 1: Performing simultaneous filtering and sorting
*
* 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 Example 2: Performing simultaneous sorting, insertion, and removal
*
* 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.}
* 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 Example 1: Inserting a single element via reference
*
* 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 Example 2: Inserting a single element via HTML string
*
* console.log(mixer.getState().totalShow); // 1
*
* // Create a new element via reference
*
* var newElementHtml = '<div class="mix"></div>';
*
* // 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 Example 3: Inserting multiple elements via reference
*
* 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 Example 4: Inserting a jQuery collection object containing one or more elements
*
* console.log(mixer.getState().totalShow); // 4
*
* var $newElement = $('<div class="mix"></div>');
*
* // 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.|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.}
* 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 Example: Inserting a new element before a reference element
*
* // 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.|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.}
* 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 Example: Inserting a new element after a reference element
*
* // 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.|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.}
* 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 Example: Prepending a new element
*
* // 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.|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.}
* 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 Example: Appending a new element
*
* // 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.|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.}
* 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 Example 1: Removing an element by reference
*
* var elementToRemove = containerEl.firstElementChild;
*
* mixer.remove(elementToRemove)
* .then(function(state) {
* console.log(state.targets.indexOf(elementToRemove) === -1); // true
* });
*
* @example Example 2: Removing a collection of elements by reference
*
* 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 Example 3: Removing one or more elements by selector
*
* mixer.remove('.category-a')
* .then(function() {
* console.log(containerEl.querySelectorAll('.category-a').length); // 0
* });
*
* @example Example 4: Removing an element by index
*
* 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.|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.}
* 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 Example 1: retrieve the entire configuration object
*
* var config = mixer.getConfig(); // Config { ... }
*
* @example Example 2: retrieve a named sub-object of configuration object
*
* var animation = mixer.getConfig('animation'); // ConfigAnimation { ... }
*
* @example Example 3: retrieve a value of configuration object via a dot-notation string key
*
* 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 Example 1: Updating animation options
*
* mixer.configure({
* animation: {
* effects: 'fade translateX(-100%)',
* duration: 300
* }
* });
*
* @example Example 2: Removing a callback after it has been set
*
* 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 Example: Retrieving a state object
*
* 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 Example: Force refreshing the mixer after external DOM manipulation
*
* 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 Example: Force render targets after changing the target render function
*
* console.log(container.innerHTML); // ... <span class="mix">Foo</span> ...
*
* mixer.configure({
* render: {
* target: (item) => `<a href="/${item.slug}/" class="mix">${item.title}</a>`
* }
* });
*
* mixer.forceRender();
*
* console.log(container.innerHTML); // ... <a href="/foo/" class="mix">Foo</a> ...
*
* @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 Example: Destroying the mixer before removing its container element
*
* 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>}
*/
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.}
* @default []
*/
this.targets = [];
/**
* An array of all target elements not matching the current filter.
*
* @name hide
* @memberof mixitup.State
* @instance
* @type {Array.}
* @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.}
* @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.}
* @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.}
* @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);