WebEditor/third_party/js/selectable.js
Matthew Welch 80847801f4 Changed to TypeScript
Moved third party files to separate folder
2021-01-23 16:01:19 -08:00

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;
}
);