1965 lines
66 KiB
JavaScript
1965 lines
66 KiB
JavaScript
/*!
|
|
*
|
|
* 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;
|
|
}
|
|
); |