/**! * 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) { console.log(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);