/*! * * Selectable * Copyright (c) 2017 Karl Saunders (http://mobius.ovh) * Licensed under MIT (http://www.opensource.org/licenses/mit-license.php) * * Version: 0.17.8 * */ (function(root, factory) { var plugin = "Selectable"; if (typeof exports === "object") { module.exports = factory(plugin); } else if (typeof define === "function" && define.amd) { define([], factory); } else { root[plugin] = factory(plugin); } })( typeof global !== "undefined" ? global : this.window || this.global, function() { "use strict"; /** * Check for classList support * @type {Boolean} */ var _supports = "classList" in document.documentElement; /** * classList shim * @type {Object} */ var classList = { add: function(a, c) { _supports ? a.classList.add(c) : classList.contains(a, c) || (a.className = a.className.trim() + " " + c); }, remove: function(a, c) { _supports ? a.classList.remove(c) : classList.contains(a, c) && (a.className = a.className.replace( new RegExp("(^|\\s)" + c.split(" ").join("|") + "(\\s|$)", "gi"), " " )); }, contains: function(a, c) { if (a) return _supports ? a.classList.contains(c) : !!a.className && !!a.className.match(new RegExp("(\\s|^)" + c + "(\\s|$)")); } }; /** * Detect CTRL or META key press * @param {Object} e Event interface * @return {Boolean} */ var _isCmdKey = function(e) { return !!e.ctrlKey || !!e.metaKey; }; /** * Detect SHIFT key press * @param {Object} e Event interface * @return {Boolean} */ var _isShiftKey = function(e) { return !!e.shiftKey; }; var _axes = ["x", "y"]; var _axes1 = { x: "x1", y: "y1" }; var _axes2 = { x: "x2", y: "y2" }; /* SELECTABLE */ var Selectable = function(options) { this.version = "0.17.8"; this.v = this.version.split(".").map(function(s) { return parseInt(s, 10) }); this.touch = "ontouchstart" in window || (window.DocumentTouch && document instanceof DocumentTouch); this.init(options); }; Selectable.prototype = { /* ---------- PUBLIC METHODS ---------- */ /** * Init instance * @return {void} */ init: function(options) { var that = this; /** * Default configuration properties * @type {Object} */ var selectableConfig = { filter: ".ui-selectable", tolerance: "touch", appendTo: document.body, touch: true, toggleTouch: true, toggle: false, autoRefresh: true, throttle: 50, lassoSelect: "normal", autoScroll: { threshold: 0, increment: 10 }, saveState: false, ignore: false, maxSelectable: false, lasso: { border: "1px dotted #000", backgroundColor: "rgba(52, 152, 219, 0.2)" }, keys: ["shiftKey", "ctrlKey", "metaKey", ""], classes: { lasso: "ui-lasso", selected: "ui-selected", container: "ui-container", selecting: "ui-selecting", selectable: "ui-selectable", deselecting: "ui-deselecting" } }; this.config = _extend(selectableConfig, options); this.origin = { x: 0, y: 0 }; this.mouse = { x: 0, y: 0 }; var o = this.config; // Is auto-scroll enabled? this.autoscroll = isObject(o.autoScroll); this.lasso = false; if (o.lasso && isObject(o.lasso)) { this.lasso = document.createElement("div"); this.lasso.className = o.classes.lasso; _css( this.lasso, _extend({ position: "absolute", boxSizing: "border-box", opacity: 0 // border will show even at zero width / height }, o.lasso ) ); } if (this.touch) { o.toggle = o.toggleTouch; } if (!o.touch) { this.touch = false; } this.events = {}; var that = this; // bind events [ "_start", "_touchstart", "_drag", "_end", "_keyup", "_keydown", "_blur", "_focus" ].forEach(function(event) { that.events[event] = that[event].bind(that); }); this.events._refresh = _throttle(this.refresh, o.throttle, this); if (this.autoscroll) { this.events._scroll = this._onScroll.bind(this); } this.setContainer(); this.scroll = { x: this.bodyContainer ? window.pageXOffset : this.container.scrollLeft, y: this.bodyContainer ? window.pageYOffset : this.container.scrollTop }; if (isCollection(o.filter)) { this.nodes = [].slice.call(o.filter); } else if (typeof o.filter === "string") { this.nodes = [].slice.call(this.container.querySelectorAll(o.filter)); } // activate items this.nodes.forEach(function(node) { classList.add(node, o.classes.selectable); }); this.update(); this.enable(); setTimeout(function() { if (o.saveState) { that.state("save"); } that.emit(that.v[1] < 15 ? "selectable.init" : "init"); }, 10); }, /** * Update instance * @return {Void} */ update: function() { this._loadItems(); this.refresh(); this.emit(this.v[1] < 15 ? "selectable.update" : "update", this.items); }, /** * Update item coords * @return {Void} */ refresh: function() { var ww = window.innerWidth; var wh = window.innerHeight; var x = this.bodyContainer ? window.pageXOffset : this.container.scrollLeft; var y = this.bodyContainer ? window.pageYOffset : this.container.scrollTop; this.offsetWidth = this.container.offsetWidth; this.offsetHeight = this.container.offsetHeight; this.clientWidth = this.container.clientWidth; this.clientHeight = this.container.clientHeight; this.scrollWidth = this.container.scrollWidth; this.scrollHeight = this.container.scrollHeight; // get the parent container DOMRect this.boundingRect = _rect(this.container); if (this.bodyContainer) { this.boundingRect.x2 = ww; this.boundingRect.y2 = wh; } // get the parent container scroll dimensions this.scroll = { x: x, y: y, max: { x: this.scrollWidth - (this.bodyContainer ? ww : this.clientWidth), y: this.scrollHeight - (this.bodyContainer ? wh : this.clientHeight) }, size: { x: this.clientWidth, y: this.clientHeight }, scrollable: { x: this.scrollWidth > this.offsetWidth, y: this.scrollHeight > this.offsetHeight } }; for (var i = 0; i < this.nodes.length; i++) { this.items[i].rect = _rect(this.nodes[i]); } this.emit(this.v[1] < 15 ? "selectable.refresh" : "refresh"); }, /** * Add instance event listeners * @return {Void} */ bind: function() { var e = this.events; this.unbind(); if (this.touch) { this.on(this.container, "touchstart", e._touchstart); this.on(document, "touchend", e._end); this.on(document, "touchcancel", e._end); if (this.lasso !== false) { this.on(document, "touchmove", e._drag); } } else { this.on(this.container, "mousedown", e._start); this.on(document, "mouseup", e._end); this.on(document, "keydown", e._keydown); this.on(document, "keyup", e._keyup); this.on(this.container, "mouseenter", e._focus); this.on(this.container, "mouseover", e._focus); this.on(this.container, "mouseleave", e._blur); if (this.lasso !== false) { this.on(document, "mousemove", e._drag); } } if (this.autoscroll) { this.on(this.bodyContainer ? window : this.container, "scroll", e._scroll); } this.on(window, "resize", e._refresh); this.on(window, "scroll", e._refresh); }, /** * Remove instance event listeners * @return {Void} */ unbind: function() { var e = this.events; this.off(this.container, "mousedown", e._start); this.off(document, "mousemove", e._drag); this.off(document, "mouseup", e._end); this.off(document, "keydown", e._keydown); this.off(document, "keyup", e._keyup); this.off(this.container, "mouseenter", e._focus); this.off(this.container, "mouseleave", e._blur); if (this.autoscroll) { this.off( this.bodyContainer ? window : this.container, "scroll", e._scroll ); } // Mobile this.off(this.container, "touchstart", e._start); this.off(document, "touchend", e._end); this.off(document, "touchcancel", e._end); this.off(document, "touchmove", e._drag); this.off(window, "resize", e._refresh); this.off(window, "scroll", e._refresh); }, /** * Set the container * @param {String|Object} container CSS3 selector string or HTMLElement */ setContainer: function(container) { var o = this.config, old; if (this.container) { old = this.container; this.unbind(); } container = container || o.appendTo; if (typeof container === "string") { this.container = document.querySelector(container); } else if (container instanceof Element && container.nodeName) { this.container = container; } classList.add(this.container, o.classes.container); this.container._selectable = this; if (old) { classList.remove(old, o.classes.container); delete old._selectable } this.bodyContainer = this.container === document.body; this._loadItems(); if (this.autoscroll) { var style = _css(this.container); if (style.position === "static" && !this.bodyContainer) { this.container.style.position = "relative"; } } this.bind(); }, /** * Select an item * @param {Object} item * @return {Boolean} */ select: function(item, all, save) { var all = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var save = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; if (isCollection(item)) { var count = this.getSelectedItems().length; for (var i = 0; i < item.length; i++) { if (!!this.config.maxSelectable && count >= this.config.maxSelectable) { break; } this.select(item[i], false, false); count++; } if (save && this.config.saveState) { this.state("save"); } return this.getSelectedItems(); } item = this.get(item); if (item) { // toggle item if already selected if ( this.config.toggle && this.config.toggle === "drag" && !all && item.selected && !this.cmdDown ) { return this.deselect(item, false); } var el = item.node, o = this.config.classes; classList.remove(el, o.selecting); classList.add(el, o.selected); item.selecting = false; item.selected = true; item.startselected = true; if (save && this.config.saveState) { this.state("save"); } this.emit(this.v[1] < 15 ? "selectable.select" : "selecteditem", item); return item; } return false; }, /** * Unselect an item * @param {Object} item * @return {Boolean} */ deselect: function(item, save) { if (isCollection(item)) { for (var i = 0; i < item.length; i++) { this.deselect(item[i], false); } if (save && this.config.saveState) { this.state("save"); } return this.getSelectedItems(); } item = this.get(item); if (item) { var el = item.node, o = this.config.classes; item.selecting = false; item.selected = false; item.deselecting = false; item.startselected = false; classList.remove(el, o.deselecting); classList.remove(el, o.selecting); classList.remove(el, o.selected); if (save && this.config.saveState) { this.state("save"); } this.emit(this.v[1] < 15 ? "selectable.deselect" : "deselecteditem", item); return item; } return false; }, /** * Toggle an item * @param {Object} item * @return {Boolean} */ toggle: function(item) { var test = this.get(item); if (test) { if (!isCollection(test)) { test = [test]; } for (var i = 0; i < test.length; i++) { if (test[i].selected) { this.deselect(test[i], false); } else { this.select(test[i], false, false); } } if (this.config.saveState) { this.state("save"); } } }, /** * Add a node to the instance * @param {Object} node HTMLElement * * @return {Void} */ add: function(node) { var els = []; if (typeof node === "string") { node = [].slice.call(this.container.querySelectorAll(node)); } if (!isCollection(node)) { node = [node]; } for (var i = 0; i < node.length; i++) { if (this.nodes.indexOf(node[i]) < 0 && node[i] instanceof Element) { els.push(node[i]); classList.add(node[i], this.config.classes.selectable); } } this.nodes = this.nodes.concat(els); this.update(); // emit "addeditem" for each new item for (var i = 0; i < els.length; i++) { this.emit("addeditem", this.get(els[i])); } }, /** * Remove an item from the instance so it's deselectable * @param {Mixed} item index, node or object * @return {Boolean} */ remove: function(item, stop) { item = this.get(item); if (item) { if (isCollection(item)) { for (var i = item.length - 1; i >= 0; i--) { this.remove(item[i], i > 0); } } else { var el = item.node, o = this.config.classes, rm = classList.remove; rm(el, o.selectable); rm(el, o.deselecting); rm(el, o.selecting); rm(el, o.selected); this.nodes.splice(this.nodes.indexOf(item.node), 1); // emit "removeditem" this.emit("removeditem", item); } if (!stop) { this.update(); } return true; } return false; }, /** * Select all items * @return {Void} */ selectAll: function() { if ( !!this.config.maxSelectable && this.config.maxSelectable < this.items.length ) { return this._maxReached(); } for (var i = 0; i < this.items.length; i++) { this.select(this.items[i], true, false); } if (this.config.saveState) { this.state("save"); } }, /** * Select all items * @return {Void} */ invert: function() { var items = this.getItems(); if ( !!this.config.maxSelectable && this.config.maxSelectable < items.length ) { return this._maxReached(); } for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (item.selected) { this.deselect(item, false); } else { this.select(item, false, false); } } if (this.config.saveState) { this.state("save"); } }, /** * Unselect all items * @return {Void} */ clear: function(save) { var save = arguments.length > 0 && arguments[0] !== undefined ? false : true; for (var i = this.items.length - 1; i >= 0; i--) { this.deselect(this.items[i], false); } if (save && this.config.saveState) { this.state("save"); } }, /** * Get an item * @return {Object|Boolean} */ get: function(item) { var found = false; if (typeof item === "string") { item = [].slice.call(this.container.querySelectorAll(item)); } if (isCollection(item)) { found = []; for (var i = 0; i < item.length; i++) { var itm = this.get(item[i]); if (itm) { found.push(itm); } } } else { // item is an index if (!isNaN(item)) { if (this.items.indexOf(this.items[item]) >= 0) { found = this.items[item]; } } // item is a node else if (item instanceof Element) { found = this.items[this.nodes.indexOf(item)]; } // item is an item else if (isObject(item) && this.items.indexOf(item) >= 0) { found = item; } } return found; }, /** * Get all items * @return {Array} */ getItems: function() { return this.items; }, /** * Get all nodes * @return {Array} */ getNodes: function() { return this.nodes; }, /** * Get all selected items * @return {Array} */ getSelectedItems: function(invert) { return this.getItems().filter(function(item) { return invert ? !item.selected : item.selected; }); }, /** * Get all selected nodes * @return {Array} */ getSelectedNodes: function() { return this.getSelectedItems().map(function(item) { return item.node; }); }, /** * State method * @param {String} type * @return {Array} */ state: function(type) { var changed = false; var emit = false; switch (type) { case "save": this.states = this.states || []; this.states.push(this.getSelectedNodes()); // check we're at max saves limit if (isNumber(this.config.saveState)) { if (this.states.length > this.config.saveState) { this.states.shift(); } } // move the current state index to the last element this.currentState = this.states.length - 1; emit = true; break; case "undo": // decrement the current save state if (this.currentState > 0) { this.currentState--; changed = true; emit = true; } break; case "redo": // increment the current save state if (this.currentState < this.states.length - 1) { this.currentState++; changed = true; emit = true; } break; case "clear": this.states = []; this.currentState = false; break; } // check if the state changed if (changed) { // clear the current selection this.clear(false); // select the items in the saved state this.select(this.states[this.currentState], false, false); } // check if we need to emit the event if (emit) { this.emit( (this.v[1] < 15 ? "selectable.state." : "state.") + type, this.states[this.currentState], this.states ); } }, /** * Enable instance * @return {Boolean} */ enable: function() { if (!this.enabled) { var keys = this.config.keys; this.enabled = true; this.canShift = keys.indexOf("shiftKey") >= 0; this.canCtrl = keys.indexOf("ctrlKey") >= 0; this.canMeta = keys.indexOf("metaKey") >= 0; this.bind(); classList.add(this.container, this.config.classes.container); this.emit(this.v[1] < 15 ? "selectable.enable" : "enabled"); } return this.enabled; }, /** * Disable instance * @return {Boolean} */ disable: function() { if (this.enabled) { var keys = this.config.keys; this.enabled = false; this.unbind(); classList.remove(this.container, this.config.classes.container); this.emit(this.v[1] < 15 ? "selectable.disable" : "disabled"); } return this.enabled; }, /** * Destroy instance * @return {void} */ destroy: function() { this.disable(); this.listeners = false; this.clear(); this.state("clear"); this.remove(this.items); this.events = null; }, /** * Add custom event listener * @param {String} event * @param {Function} callback * @return {Void} */ on: function(listener, fn, capture) { if (typeof listener === "string") { this.listeners = this.listeners || {}; this.listeners[listener] = this.listeners[listener] || []; this.listeners[listener].push(fn); } else { arguments[0].addEventListener(arguments[1], arguments[2], false); } }, /** * Remove custom listener listener * @param {String} listener * @param {Function} callback * @return {Void} */ off: function(listener, fn) { if (typeof listener === "string") { this.listeners = this.listeners || {}; if (listener in this.listeners === false) return; this.listeners[listener].splice(this.listeners[listener].indexOf(fn), 1); } else { arguments[0].removeEventListener(arguments[1], arguments[2]); } }, /** * Fire custom listener * @param {String} listener * @return {Void} */ emit: function(listener) { this.listeners = this.listeners || {}; if (listener in this.listeners === false) return; for (var i = 0; i < this.listeners[listener].length; i++) { this.listeners[listener][i].apply( this, Array.prototype.slice.call(arguments, 1) ); } }, /* ---------- PRIVATE METHODS ---------- */ _maxReached: function() { return this.emit("maxitems"); }, /** * touchstart event listener * @param {Object} e Event interface * @return {Void} */ _touchstart: function(e) { this.off(this.container, "mousedown", this.events.start); this._start(e); }, /** * mousedown / touchstart event listener * @param {Object} e Event interface * @return {Void} */ _start: function(e) { var that = this, evt = this._getEvent(e), o = this.config, originalEl, cmd = _isCmdKey(e) && (this.canCtrl || this.canMeta), shift = this.canShift && _isShiftKey(e), count = this.getSelectedItems().length, max = o.maxSelectable; // max items reached if (!!max && count >= max && (cmd || shift)) { return this._maxReached(); } if ( !this.container.contains(e.target) || e.which === 3 || e.button > 0 || o.disabled ) return; // check if the parent container is scrollable and // prevent deselection when clicking on the scrollbars if ( (this.scroll.scrollable.y && evt.pageX > this.boundingRect.x1 + this.scroll.size.x) || (this.scroll.scrollable.x && evt.pageY > this.boundingRect.y1 + this.scroll.size.y) ) { return false; } // check for ignored descendants if (this.config.ignore) { var stop = false; var ignore = this.config.ignore; if (!Array.isArray(ignore)) { ignore = [ignore]; } for (var i = 0; i < ignore.length; i++) { var ancestor = e.target.closest(ignore[i]); if (ancestor) { stop = true; break; } } if (stop) { return false; } } // selectable nodes may have child elements // so let's get the closest selectable node var node = closest(e.target, function(el) { return ( el === that.container || classList.contains(el, o.classes.selectable) ); }); if (!node) return false; // allow form inputs to be receive focus if ( ["INPUT", "SELECT", "BUTTON", "TEXTAREA", "OPTION"].indexOf( e.target.tagName ) === -1 ) { e.preventDefault(); } this.dragging = true; this.origin = { x: evt.pageX + (this.bodyContainer ? 0 : this.scroll.x), y: evt.pageY + (this.bodyContainer ? 0 : this.scroll.y), scroll: { x: this.scroll.x, y: this.scroll.y } }; if (this.lasso) { this.container.appendChild(this.lasso); } if (node !== this.container) { var item = this.get(node); item.selecting = true; classList.add(node, o.classes.selecting); } else { // Fixes #32 if (o.lassoSelect == "sequential") { node = getClosestNodeToPointer(evt, this.items); } } if (o.autoRefresh) { this.refresh(); } if (shift && this.startEl && node !== this.container) { var items = this.items, currentIndex = this.getNodes().indexOf(node), lastIndex = this.getNodes().indexOf(this.startEl), step = currentIndex < lastIndex ? 1 : -1; while ((currentIndex += step) && currentIndex !== lastIndex) { items[currentIndex].selecting = true; } } for (var i = 0; i < this.items.length; i++) { var item = this.items[i], el = item.node, isCurrentNode = el === node; if (item.selected) { item.startselected = true; var deselect = o.toggle || cmd ? isCurrentNode : !isCurrentNode && !shift; if (deselect) { classList.remove(el, o.classes.selected); item.selected = false; classList.add(el, o.classes.deselecting); item.deselecting = true; } } if (isCurrentNode) { originalEl = item; } } this.startEl = node; this.emit(this.v[1] < 15 ? "selectable.start" : "start", e, originalEl); }, /** * mousmove / touchmove event listener * @param {Object} e Event interface * @return {Void} */ _drag: function(e) { var o = this.config; if (o.disabled || !this.dragging || (_isShiftKey(e) && this.canShift)) return; var tmp; var evt = this._getEvent(e); var cmd = _isCmdKey(e) && (this.canCtrl || this.canMeta); this.mouse = { x: evt.pageX, y: evt.pageY }; this.current = { x1: this.origin.x, y1: this.origin.y, x2: this.mouse.x + (this.bodyContainer ? 0 : this.scroll.x), y2: this.mouse.y + (this.bodyContainer ? 0 : this.scroll.y) }; // flip lasso for (var i = 0; i < _axes.length; i++) { var axis = _axes[i]; if (this.current[_axes1[axis]] > this.current[_axes2[axis]]) { tmp = this.current[_axes2[axis]]; this.current[_axes2[axis]] = this.current[_axes1[axis]]; this.current[_axes1[axis]] = tmp; } } // lasso coordinates this.coords = { x1: this.current.x1, x2: this.current.x2 - this.current.x1, y1: this.current.y1, y2: this.current.y2 - this.current.y1 }; if (o.lassoSelect === "normal") { /* highlight */ for (var i = 0; i < this.items.length; i++) { this._highlight( this.items[i], _isCmdKey(e) && (this.canCtrl || this.canMeta), evt ); } } else if (o.lassoSelect === "sequential") { this._sequentialSelect(evt); } // auto scroll if (this.autoscroll) { // subtract the parent container's position if (!this.bodyContainer) { this.coords.x1 -= this.boundingRect.x1; this.coords.y1 -= this.boundingRect.y1; } this._autoScroll(); } // lasso if (this.lasso) { // stop lasso causing overflow if ( !this.bodyContainer && this.autoscroll && !this.config.autoScroll.lassoOverflow ) { this._limitLasso(); } // style the lasso _css(this.lasso, { left: this.coords.x1, top: this.coords.y1, width: this.coords.x2, height: this.coords.y2, opacity: 1 }); } // emit the "drag" event this.emit(this.v[1] < 15 ? "selectable.drag" : "drag", e, this.coords); }, /** * mouseup / touchend event listener * @param {Object} e Event interface * @return {Void} */ _end: function(e) { if (!this.dragging) return; this.dragging = false; var that = this, o = that.config, node = e.target, evt = this._getEvent(e), endEl, selected = [], deselected = [], count = this.getSelectedItems().length, max = o.maxSelectable; // remove the lasso if (this.lasso && this.container.contains(this.lasso)) { this.container.removeChild(this.lasso); } if (this.lasso) { // Reset the lasso _css(this.lasso, { opacity: 0, left: 0, width: 0, top: 0, height: 0 }); // the lasso was the event.target so let's get the actual // node below the pointer node = document.elementFromPoint(evt.pageX, evt.pageY); if (!node) { node = this.container; } } // now let's get the closest valid selectable node endEl = closest(node, function(el) { return classList.contains(el, o.classes.selectable); }); var maxReached = false; // loop over items and check their state for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; // If we've mousedown'd and mouseup'd on the same selected item // toggling it's state to deselected won't work if we've dragged even // a small amount. This can happen if we're moving between items quickly // while the mouse button is down. We can fix that here. if (o.toggle && item.node === endEl && item.node === this.startEl) { if (item.selecting && item.startselected) { item.deselecting = true; item.selecting = false; } } // item was marked for deselect if (item.deselecting) { deselected.push(item); this.deselect(item, false); } // item was marked for select if (item.selecting) { // max items reached if (!!max && count + selected.length >= max) { item.selecting = false; classList.remove(item.node, o.classes.selecting); maxReached = true; } else { selected.push(item); this.select(item, false, false); } } } if (o.saveState) { this.state("save"); } this.emit( this.v[1] < 15 ? "selectable.end" : "end", e, selected, deselected ); if (maxReached) { return this._maxReached(); } }, /** * keydown event listener * @param {Object} e Event interface * @return {Void} */ _keydown: function(e) { this.cmdDown = _isCmdKey(e) && (this.canCtrl || this.canMeta); var code = false; if (e.key !== undefined) { code = e.key; } else if (e.keyCode !== undefined) { code = e.keyCode; } if (code) { if (this.cmdDown && this.focused) { switch (code) { case 65: case "a": case "A": e.preventDefault(); this.selectAll(); break; case 89: case "y": case "Y": this.state("redo"); break; case 90: case "z": case "Z": this.state("undo"); break; } } else { switch (code) { case 32: case " ": this.toggle(document.activeElement); break; } } } }, /** * keyup event listener * @param {Object} e Event interface * @return {Void} */ _keyup: function(e) { this.cmdDown = _isCmdKey(e) && (this.canCtrl || this.canMeta); }, /** * scroll event listener * @param {Object} e Event interface * @return {Void} */ _onScroll: function(e) { this.scroll.x = this.bodyContainer ? window.pageXOffset : this.container.scrollLeft; this.scroll.y = this.bodyContainer ? window.pageYOffset : this.container.scrollTop; for (var i = 0; i < this.items.length; i++) { this.items[i].rect = _rect(this.items[i].node); } }, /** * Load items from the given filter * @return {void} */ _loadItems: function() { var o = this.config; this.nodes = [].slice.call( this.container.querySelectorAll("." + o.classes.selectable) ); this.items = []; if (this.nodes.length) { for (var i = 0; i < this.nodes.length; i++) { var el = this.nodes[i]; classList.add(el, o.classes.selectable); var item = { node: el, rect: _rect(el), startselected: false, selected: classList.contains(el, o.classes.selected), selecting: classList.contains(el, o.classes.selecting), deselecting: classList.contains(el, o.classes.deselecting) }; var isTransformed = this._get2DTransformation(el); if (isTransformed) { var offset = _getOffset(el); var trans = isTransformed.translate, origin = isTransformed.origin, scale = isTransformed.scale, w = el.offsetWidth, h = el.offsetHeight, x = offset.left, y = offset.top; var orx = origin.x, ory = origin.y; var hx = w / 2, hy = h / 2; var cx = x + (hx - orx) * scale + orx, cy = y + (hy - ory) * scale + ory; var shx = hx * scale, shy = hy * scale; // rect coords var p = [{ x: cx - shx, y: cy - shy }, { x: cx + shx, y: cy - shy }, { x: cx + shx, y: cy + shy }, { x: cx - shx, y: cy + shy } ]; // rotate coords for (var n = 0; n <= 3; n++) { p[n] = _rotatePoint( p[n].x + trans.x, p[n].y + trans.y, x + orx + trans.x, y + ory + trans.y, isTransformed.angle ); } item.transform = { rect: p }; } this.items.push(item); } } }, /** * Get event * @return {Object} */ _getEvent: function(e) { if (this.touch) { if (e.type === "touchend") { return e.changedTouches[0]; } return e.touches[0]; } return e; }, /** * Scroll container * @return {Void} */ _autoScroll: function() { var as = this.config.autoScroll; var i = as.increment; var t = as.threshold; var inc = { x: 0, y: 0 }; if (this.bodyContainer) { this.mouse.x -= this.scroll.x; this.mouse.y -= this.scroll.y; } // check if we need to scroll for (var n = 0; n < _axes.length; n++) { var axis = _axes[n]; if ( this.mouse[axis] >= this.boundingRect[_axes2[axis]] - t && this.scroll[axis] < this.scroll.max[axis] ) { inc[axis] = i; } else if ( this.mouse[axis] <= this.boundingRect[_axes1[axis]] + t && this.scroll[axis] > 0 ) { inc[axis] = -i; } } // scroll the container if (this.bodyContainer) { window.scrollBy(inc.x, inc.y); } else { this.container.scrollTop += inc.y; this.container.scrollLeft += inc.x; } }, /** * Limit lasso to container boundaries * @return {Void} */ _limitLasso: function() { for (var i = 0; i < _axes.length; i++) { var axis = _axes[i]; var max = this.boundingRect[_axes1[axis]] + this.scroll.size[axis]; if ( this.mouse[axis] >= max && this.scroll[axis] >= this.scroll.max[axis] ) { var off = this.origin[axis] - this.boundingRect[_axes1[axis]] - this.scroll[axis]; this.coords[_axes1[axis]] = this.origin[axis] - this.boundingRect[_axes1[axis]]; this.coords[_axes2[axis]] = max - off - this.boundingRect[_axes1[axis]]; } if ( this.mouse[axis] <= this.boundingRect[_axes1[axis]] && this.scroll[axis] <= 0 ) { this.coords[_axes1[axis]] = 0; this.coords[_axes2[axis]] = this.origin[axis] - this.boundingRect[_axes1[axis]]; } } }, _sequentialSelect: function(e) { var c = this.config.classes, lastEl = document.elementFromPoint(e.pageX, e.pageY - window.pageYOffset), start, end, items; if (lastEl) { lastEl = lastEl.closest("." + c.selectable); if (lastEl) { if (this.mouse.y > this.origin.y) { start = this.nodes.indexOf(this.startEl); end = this.nodes.indexOf(lastEl); } else if (this.mouse.y < this.origin.y) { start = this.nodes.indexOf(lastEl); end = this.nodes.indexOf(this.startEl); } for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (i >= start && i <= end) { this._highlight(item, _isCmdKey(e) && (this.canCtrl || this.canMeta)); } else { item.selecting = false; item.node.classList.remove(c.selecting); } } } } }, /** * Highlight an item for selection based on lasso position * @param {Object} item * @return {Void} */ _highlight: function(item, cmd, evt) { var o = this.config, el = item.node, over = false; var x = this.bodyContainer ? 0 : this.scroll.x; var y = this.bodyContainer ? 0 : this.scroll.y; if (o.lassoSelect === "normal") { if (o.tolerance === "touch") { // element is 2d transformed so we need to do some more complex collision detection if (item.transform) { var a = [{ x: this.origin.x, y: this.origin.y }, { x: this.mouse.x + x, y: this.origin.y }, { x: this.mouse.x + x, y: this.mouse.y + y }, { x: this.origin.x, y: this.mouse.y + y } ]; over = _rectsIntersecting(a, item.transform.rect); } else { // element has no 2d transform applied so just detect collision with the bounding box over = !( item.rect.x1 + x > this.current.x2 || item.rect.x2 + x < this.current.x1 || item.rect.y1 + y > this.current.y2 || item.rect.y2 + y < this.current.y1 ); } } else if (o.tolerance === "fit") { // this relies on detecting the bounding box of the element so // both normal and 2d transformed elements will work over = item.rect.x1 + x > this.current.x1 && item.rect.x2 + x < this.current.x2 && item.rect.y1 + y > this.current.y1 && item.rect.y2 + y < this.current.y2; } } else { over = true; } if (over) { if (item.selected && !o.toggle) { classList.remove(el, o.classes.selected); item.selected = false; } if (item.deselecting && (!o.toggle || (o.toggle && o.toggle !== "drag"))) { classList.remove(el, o.classes.deselecting); item.deselecting = false; } if (!item.selecting) { classList.add(el, o.classes.selecting); item.selecting = true; } } else { if (item.selecting) { classList.remove(el, o.classes.selecting); item.selecting = false; if (cmd && item.startselected) { classList.add(el, o.classes.selected); item.selected = true; } else { if (item.startselected && !o.toggle) { classList.add(el, o.classes.deselecting); item.deselecting = true; } } } if (el.selected) { if (!cmd) { if (!item.startselected) { classList.remove(el, o.classes.selected); item.selected = false; classList.add(el, o.classes.deselecting); item.deselecting = true; } } } } }, /** * mouseenter event listener * @param {Object} e Event interface * @return {Void} */ _focus: function(e) { this.focused = true; classList.add(this.container, "ui-focused"); }, /** * mouseleave event listener * @param {Object} e Event interface * @return {Void} */ _blur: function(e) { this.focused = false; classList.remove(this.container, "ui-focused"); }, /** * Get an element's 2d transformation properties * @param {Object} el HTMLElement * @return {Bool|Object} */ _get2DTransformation: function(el) { var r = window.getComputedStyle(el, null), trans = r.getPropertyValue("-webkit-transform") || r.getPropertyValue("-moz-transform") || r.getPropertyValue("-ms-transform") || r.getPropertyValue("-o-transform") || r.getPropertyValue("transform") || !1; if (trans && "none" !== trans) { var e = trans.split("(")[1].split(")")[0].split(", "), a = parseFloat(e[0]), n = parseFloat(e[1]), l = Math.sqrt(a * a + n * n), o = r.transformOrigin.split(" ").map(function(o) { return parseFloat(o) }); return { angle: Math.round(Math.atan2(n, a) * (180 / Math.PI)), scale: l, origin: { x: parseFloat(o[0]), y: parseFloat(o[1]) }, translate: { x: parseFloat(e[4]), y: parseFloat(e[5]) } }; } return !1; } }; /* ---------- HELPER FUNCTIONS ---------- */ /** * Get node closest to mouse pointer / touch position * @param {Object} ev * @param {Array} items */ function getClosestNodeToPointer(ev, items) { var lens = []; items.forEach(function(item) { var len = Math.hypot( item.rect.x1 - parseInt(ev.clientX), item.rect.y1 - parseInt(ev.clientY) ); lens.push(parseInt(len)); }); var index = lens.indexOf(Math.min.apply(Math, lens)) return items[index].node; } /** * Find the closest matching ancestor to a node * @param {Object} el HTMLElement * @param {Function} fn Callback * @return {Object|Boolean} */ function closest(el, fn) { return ( el && el !== document.documentElement && (fn(el) ? el : closest(el.parentNode, fn)) ); } /** * Check is item is object * @return {Boolean} */ function isObject(val) { return Object.prototype.toString.call(val) === "[object Object]"; } /** * Check item is iterable * @param {Mixed} arr * @return {Boolean} */ function isCollection(arr) { return ( Array.isArray(arr) || arr instanceof HTMLCollection || arr instanceof NodeList ); } /** * Check var is a number * @param {Mixed} n * @return {Boolean} */ function isNumber(n) { if ("isInteger" in Number) { return Number.isInteger(n); } return !isNaN(n); } /** * Merge objects (reccursive) * @param {Object} r * @param {Object} t * @return {Object} */ function _extend(src, props) { for (var prop in props) { if (props.hasOwnProperty(prop)) { var val = props[prop]; if (val && isObject(val)) { src[prop] = src[prop] || {}; _extend(src[prop], val); } else { src[prop] = val; } } } return src; } /** * Mass assign style properties * @param {Object} i * @param {(String|Object)} t * @param {String|Object} */ function _css(i, t) { var e = i.style; if (i) { if (void 0 === t) return window.getComputedStyle(i); if (isObject(t)) for (var n in t) n in e || (n = "-webkit-" + n), (i.style[n] = t[n] + ("string" == typeof t[n] ? "" : "opacity" === n ? "" : "px")); } } /** * Get an element's DOMRect relative to the document instead of the viewport. * @param {Object} t HTMLElement * @param {Boolean} e Include margins * @return {Object} Formatted DOMRect copy */ function _rect(e) { var w = window, o = e.getBoundingClientRect(), b = document.documentElement || document.body.parentNode || document.body, d = void 0 !== w.pageXOffset ? w.pageXOffset : b.scrollLeft, n = void 0 !== w.pageYOffset ? w.pageYOffset : b.scrollTop; return { x1: o.left + d, x2: o.left + o.width + d, y1: o.top + n, y2: o.top + o.height + n, height: o.height, width: o.width }; } /** * Returns a function, that, as long as it continues to be invoked, will not be triggered. * @param {Function} fn * @param {Number} lim * @param {Boolean} now * @return {Function} */ function _throttle(fn, lim, context) { var wait; return function() { context = context || this; if (!wait) { fn.apply(context, arguments); wait = true; return setTimeout(function() { wait = false; }, lim); } }; } function _getOffset(el) { var top = 0, left = 0; do { top += el.offsetTop || 0; left += el.offsetLeft || 0; el = el.offsetParent; } while (el); return { top: top, left: left }; } function _rotatePoint(px, py, x, y, theta) { theta = (theta * Math.PI) / 180.0; return { x: Math.cos(theta) * (px - x) - Math.sin(theta) * (py - y) + x, y: Math.sin(theta) * (px - x) + Math.cos(theta) * (py - y) + y }; } /** * Determine whether there is an intersection between the two rects described * by the lists of vertices. Uses the Separating Axis Theorem. * * @param {Array} a Array of coords * @param {Array} b Array of coords * @return {Bool} */ function _rectsIntersecting(a, b) { var r, o, t, e, n, v, i, d, f = [a, b]; for (e = 0; e < f.length; e++) { var g = f[e]; for (n = 0; n < g.length; n++) { var h = (n + 1) % g.length, l = g[n], x = g[h], y = x.y - l.y, c = l.x - x.x; for (r = o = void 0, v = 0; v < a.length; v++) (t = y * a[v].x + c * a[v].y), (void 0 === r || t < r) && (r = t), (void 0 === o || o < t) && (o = t); for (i = d = void 0, v = 0; v < b.length; v++) (t = y * b[v].x + c * b[v].y), (void 0 === i || t < i) && (i = t), (void 0 === d || d < t) && (d = t); if (o < i || d < r) return !1; } } return !0; } return Selectable; } );