summaryrefslogtreecommitdiff
path: root/static/js/drag-drop.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/js/drag-drop.js')
-rw-r--r--static/js/drag-drop.js572
1 files changed, 572 insertions, 0 deletions
diff --git a/static/js/drag-drop.js b/static/js/drag-drop.js
new file mode 100644
index 0000000..db720ec
--- /dev/null
+++ b/static/js/drag-drop.js
@@ -0,0 +1,572 @@
+/**
+ * DragDrop.js
+ *
+ * A JavaScript micro-framework for adding drag-and-drop functionality
+ * to elements for advanced UI development.
+ *
+ * @author James Brumond
+ * @version 0.3.0
+ * @copyright Copyright 2011 James Brumond
+ * @license Dual licensed under MIT and GPL
+ */
+
+/*jshint browser: true, bitwise: false, camelcase: false, eqnull: true, latedef: false,
+ plusplus: false, jquery: true, shadow: true, smarttabs: true, loopfunc: true */
+
+(function() {
+
+ var
+
+ // Is this a touch device?
+ touchEvents = ('ontouchstart' in window),
+
+ // A class to add when an element is being dragged
+ dragClass = 'drag',
+
+ /**
+ * The DragDrop namespace
+ *
+ * Example:
+ *
+ * DragDrop.bind ( element[, options ]);
+ * DragDrop.unbind ( reference );
+ *
+ * @access public
+ */
+ DragDrop = (function() {
+ var self = { },
+
+ // Determine the events to bind to
+ events = (touchEvents ?
+ {
+ start: 'touchstart',
+ move: 'touchmove',
+ end: 'touchend'
+ } : {
+ start: 'mousedown',
+ move: 'mousemove',
+ end: 'mouseup'
+ }
+ ),
+
+ // Elements already bound
+ bindings = [ ],
+
+ // Check if a given binding (element/anchor pair) already exists
+ bindingExists = function(element, anchor) {
+ for (var i = 0, c = bindings.length; i < c; i++) {
+ if (bindings[i] && bindings[i].element === element && bindings[i].anchor === anchor) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ // Do something with a given binding's given event stack
+ withBindingEvent = function(reference, event, func) {
+ if (bindings[reference._id] && bindings[reference._id].events[event]) {
+ func(bindings[reference._id].events[event]);
+ }
+ },
+
+ // Parse the arguments of DragDrop.bind
+ parseOptions = function(element, options) {
+ options = options || { };
+ options.element = element;
+ options.anchor = options.anchor || element;
+ options.boundingBox = options.boundingBox || null;
+ options.releaseAnchors = options.releaseAnchors || [ ];
+ options.releaseAnchors.unshift(document);
+ return options;
+ },
+
+ // The next binding ID to use
+ nextBinding = 1,
+
+ // ------------------------------------------------------------------
+ // A constructor for a resource type used in referencing bindings
+
+ BindingReference = function() {
+ this._id = nextBinding++;
+ };
+
+ BindingReference.prototype.unbind = function() {
+ return DragDrop.unbind(this);
+ };
+
+ BindingReference.prototype.bindEvent = function(event, func) {
+ return DragDrop.bindEvent(this, event, func);
+ };
+
+ BindingReference.prototype.unbindEvent = function(event, func) {
+ return DragDrop.unbindEvent(this, event, func);
+ };
+
+ BindingReference.prototype.invokeEvent = function(event, source) {
+ return DragDrop.invokeEvent(this, event, source);
+ };
+
+ BindingReference.prototype.setBoundingBox = function(box) {
+ bindings[this._id].boundingBox = box;
+ };
+
+ // ----------------------------------------------------------------------------
+ // Public Functions
+
+ // Make an element draggable
+ self.bind = function(element, options) {
+ options = parseOptions(element, options);
+ if (! isObject(options.element)) {
+ throw new Error('Must give an element to drag');
+ }
+ if (getStyle(options.element, 'position') === 'static') {
+ throw new Error('Cannot drag-drop an element with position:static');
+ }
+ // Check to make sure the elements aren't already bound
+ if (! bindingExists(options.element, options.anchor)) {
+ // Initialize the binding object
+ var reference = new BindingReference();
+ var binding = {
+ element: options.element,
+ anchor: options.anchor,
+ releaseAnchors: options.releaseAnchors,
+ dragging: false,
+ event: null,
+ shouldUnbind: false,
+ boundingBox: options.boundingBox,
+ events: {
+ beforedrag: Callstack(options.beforedrag),
+ dragstart: Callstack(options.dragstart),
+ dragend: Callstack(options.dragend),
+ drag: Callstack(options.drag),
+ unbind: Callstack(options.unbind)
+ }
+ };
+ // Bind the first event
+ binding.event = Events.bind(binding.anchor, events.start, function(e) {
+ // Make sure it's a left click or touch event
+ if ((window.event && e.button === 1) || e.button === 0 || touchEvents) {
+ stopEvent(e);
+ // Call any "beforedrag" events before calculations begin
+ binding.events.beforedrag.call(
+ binding.element, new DragEvent('beforedrag', e, binding)
+ );
+ // Make sure everyone knows the element is being dragged
+ binding.dragging = true;
+ addClass(binding.element, dragClass);
+ // Start calculating movement
+ var startX = getPos(binding.element, 'left');
+ var startY = getPos(binding.element, 'top');
+ // These are used in some bounding box calculations
+ var startOffsetLeft = binding.element.offsetLeft;
+ var startOffsetTop = binding.element.offsetTop;
+ var startTotalOffset = getOffset(binding.element);
+ // A place to hold on to event functions we are going to unbind later
+ var tempEvents = [ ];
+ // The target for the move and end events is dependent on the input type
+ var target = (touchEvents ? binding.anchor : document);
+ // Bind the movement event
+ tempEvents.push(Events.bind(target, events.move, function(e2) {
+ // Find all needed offsets
+ var offsetX = e2.clientX - e.clientX;
+ var offsetY = e2.clientY - e.clientY;
+ var offsetWidth = binding.element.offsetWidth;
+ var offsetHeight = binding.element.offsetHeight;
+ // Find the new positions
+ var posX = startX + offsetX;
+ var posY = startY + offsetY;
+ // Enforce any bounding box
+ if (binding.boundingBox) {
+ var box = binding.boundingBox;
+ var minX, maxX, minY, maxY;
+ // Bound inside offset parent
+ if (box === 'offsetParent') {
+ var parent = binding.element.offsetParent;
+ if (getStyle(binding.element, 'position') === 'relative') {
+ minX = -startOffsetLeft;
+ minY = -startOffsetTop;
+ } else {
+ minX = minY = 0;
+ }
+ maxX = parent.clientWidth + minX;
+ maxY = parent.clientHeight + minY;
+ }
+ // Bound to the dimensions of the window
+ else if (box === 'windowSize') {
+ var dimensions = getWindowSize();
+ if (getStyle(binding.element, 'position') === 'relative') {
+ minX = -startTotalOffset.x;
+ minY = -startTotalOffset.y;
+ } else {
+ minX = minY = 0;
+ }
+ maxX = dimensions.x + minX;
+ maxY = dimensions.y + minY;
+ }
+ // Manual bounding box
+ else {
+ minX = box.x.min;
+ maxX = box.x.max;
+ minY = box.y.min;
+ maxY = box.y.max;
+ }
+ posX = Math.max(minX, Math.min(maxX - offsetWidth, posX));
+ posY = Math.max(minY, Math.min(maxY - offsetHeight, posY));
+ }
+ // Move the element
+ binding.element.style.left = posX + 'px';
+ binding.element.style.top = posY + 'px';
+ // Call any "drag" events
+ binding.events.drag.call(
+ binding.element, new DragEvent('drag', e2, binding)
+ );
+ return stopEvent(e2);
+ }));
+ // Bind the release events
+ for (var i = 0, c = binding.releaseAnchors.length; i < c; i++) {
+ var elem = binding.releaseAnchors[i];
+ tempEvents.push(
+ Events.bind(elem, events.end, onRelease(elem))
+ );
+ }
+ // Avoid text selection problems
+ document.body.focus();
+ tempEvents.push(Events.bind(document, 'selectstart', false));
+ tempEvents.push(Events.bind(binding.anchor, 'dragstart', false));
+ // Call any "dragstart" events
+ binding.events.dragstart.call(
+ binding.element, new DragEvent('dragstart', e, binding)
+ );
+ return false;
+ }
+ function onRelease(elem) {
+ return function(e2) {
+ // Unbind move and end events
+ for (var i = 0, c = tempEvents.length; i < c; i++) {
+ Events.unbind(tempEvents[i]);
+ }
+ // Clean up...
+ binding.dragging = false;
+ removeClass(binding.element, dragClass);
+ if (binding.shouldUnbind) {
+ DragDrop.unbind(binding.element, binding.anchor);
+ }
+ // Call any "dragend" events
+ binding.events.dragend.call(
+ binding.element, new DragEvent('dragend', e2, binding, {
+ releaseAnchor: elem
+ })
+ );
+ return stopEvent(e2);
+ };
+ }
+ });
+ // Add the binding to the list
+ bindings[reference._id] = binding;
+ return reference;
+ }
+ };
+
+ // Remove an element's draggableness
+ self.unbind = function(reference) {
+ if (reference instanceof BindingReference) {
+ var id = reference._id;
+ if (bindings[id]) {
+ var binding = bindings[id];
+
+ if (binding.dragging) {
+ binding.shouldUnbind = true;
+ } else {
+ Events.unbind(binding.event);
+ bindings[id] = null;
+ }
+ // Call any "unbind" events
+ binding.events.unbind.call(
+ binding.element, new DragEvent('unbind', { target: true }, binding)
+ );
+ }
+ }
+ };
+
+ // Bind a drag event
+ self.bindEvent = function(reference, event, func) {
+ withBindingEvent(reference, event, function(stack) {
+ stack.push(func);
+ });
+ };
+
+ // Unbind a drag event
+ self.unbindEvent = function(reference, event, func) {
+ withBindingEvent(reference, event, function(stack) {
+ stack.remove(func);
+ });
+ };
+
+ // Manually invoke a drag event
+ self.invokeEvent = function(reference, event, source) {
+ withBindingEvent(reference, event, function(stack) {
+ stack.call(
+ bindings[reference._id].element,
+ new DragEvent(event, source, reference)
+ );
+ });
+ };
+
+ return self;
+ }()),
+
+// ----------------------------------------------------------------------------
+// Helper Functions
+
+ // Array Remove - By John Resig (MIT Licensed)
+ arrayRemove = function(array, from, to) {
+ var rest = array.slice((to || from) + 1 || array.length);
+ array.length = from < 0 ? array.length + from : from;
+ return array.push.apply(array, rest);
+ },
+
+ // Get the position of an element
+ getPos = function(elem, from) {
+ var pos = parseFloat(getStyle(elem, from));
+ return (isNaN(pos) ? 0 : pos);
+ },
+
+ // Get a style property from an element
+ getStyle = function(elem, prop) {
+ if (elem.currentStyle) {
+ return elem.currentStyle[prop];
+ } else if (window.getComputedStyle) {
+ return document.defaultView.getComputedStyle(elem, null).getPropertyValue(prop);
+ } else if (elem.style) {
+ return elem.style[prop];
+ }
+ },
+
+ // Get the dimensions of the window
+ getWindowSize = function() {
+ return {
+ x: window.innerWidth || document.documentElement.clientWidth || body().clientWidth,
+ y: window.innerHeight || document.documentElement.clientHeight || body().clientHeight
+ };
+ },
+
+ // Get the total offset position of an element in the document
+ getOffset = function(elem) {
+ var x = 0;
+ var y = 0;
+ if (elem.offsetParent) {
+ do {
+ x += elem.offsetLeft;
+ y += elem.offsetTop;
+ } while (elem = elem.offsetParent);
+ }
+ return {x: x, y: y};
+ },
+
+ // Stop an event
+ stopEvent = function(evt) {
+ if (evt.preventDefault) {
+ evt.preventDefault();
+ }
+ if (evt.stopPropagation) {
+ evt.stopPropagation();
+ }
+ evt.returnValue = false;
+ return false;
+ },
+
+ // Regular expressions for matching classnames
+ cnRegexes = { },
+
+ // Remove a class from an element
+ removeClass = function(elem, cn) {
+ if (! cnRegexes[cn]) {
+ cnRegexes[cn] = new RegExp('(^|\\s)+' + cn + '(\\s|$)+');
+ }
+ elem.className = elem.className.replace(cnRegexes[cn], ' ');
+ },
+
+ // Add a class to an element
+ addClass = function(elem, cn) {
+ removeClass(elem, cn);
+ elem.className += ' ' + cn;
+ },
+
+ // Check for a non-null object
+ isObject = function(value) {
+ return !! (value && typeof value === 'object');
+ },
+
+ // Gets the target property of an event
+ getEventTarget = function(evt) {
+ var target;
+ if (evt.target) {
+ target = evt.target;
+ } else if (evt.srcElement) {
+ target = evt.srcElement;
+ }
+ if (target.nodeType === 3) {
+ target = target.parentNode;
+ }
+ return target;
+ },
+
+ /**
+ * A stackable function
+ *
+ * @access private
+ * @param function an initial function
+ * @return function
+ */
+ Callstack = function(func) {
+ var stack = [ ];
+ var result = function() {
+ var ret;
+ for (var i = 0, c = stack.length; i < c; i++) {
+ ret = stack[i].apply(this, arguments);
+ }
+ return ret;
+ };
+ result.push = function() {
+ stack.push.apply(stack, arguments);
+ };
+ result.remove = function() {
+ var args = Array.prototype.slice.call(arguments);
+ var result = [ ];
+ OUTER: for (var i = 0, c1 = stack.length; i < c1; i++) {
+ for (var j = 0, c2 = args.length; j < c2; j++) {
+ if (stack[i] === args[j]) {
+ continue OUTER;
+ }
+ }
+ result.push(stack[i]);
+ }
+ stack = result;
+ };
+ if (typeof func === 'function') {
+ stack.push(func);
+ }
+ return result;
+ },
+
+ /**
+ * Custom event constructor
+ *
+ * @access private
+ * @param string type
+ * @param object original event object
+ */
+ DragEvent = function DragEvent(type, original, binding, extras) {
+ this.type = type;
+ this.originalEvent = original;
+ this.altKey = original.altKey || false;
+ this.ctrlKey = original.ctrlKey || false;
+ this.shiftKey = original.shiftKey || false;
+ this.timestamp = original.timestamp || (+new Date());
+ this.pos = getPosition(original);
+ this.binding = binding;
+ this.target = getEventTarget(original);
+
+ if (extras) {
+ for (var i in extras) {
+ if (extras.hasOwnProperty(i)) {
+ this[i] = extras[i];
+ }
+ }
+ }
+ },
+
+ /**
+ * A namespace with functions for event binding
+ *
+ * Example:
+ *
+ * Bind
+ * var evt = Events.bind(obj, 'event', function() { ... });
+ *
+ * Unbind
+ * Events.unbind(evt);
+ * -OR-
+ * evt.unbind();
+ *
+ * @access private
+ */
+ Events = (function() {
+
+ var
+
+ // Bind an event
+ bindEvent = (function() {
+ if (document.addEventListener) {
+ return function(obj, event, func) {
+ obj.addEventListener(event, func, false);
+ };
+ } else if (document.attachEvent) {
+ return function(obj, event, func) {
+ obj.attachEvent('on' + event, func);
+ };
+ } else {
+ return function() { };
+ }
+ }()),
+
+ // Unbind an event
+ unbindEvent = (function() {
+ if (document.removeEventListener) {
+ return function(obj, event, func) {
+ obj.removeEventListener(event, func, false);
+ };
+ } else if (document.detachEvent) {
+ return function(obj, event, func) {
+ obj.detachEvent('on' + event, func);
+ };
+ } else {
+ return function() { };
+ }
+ }());
+
+ // Build the return value
+ return {
+ bind: function(obj, event, func) {
+ var oldFunc = (func === false) ? function(e) {
+ return stopEvent(e);
+ } : func;
+ func = function(e) {
+ return oldFunc.call(obj, e || window.event);
+ };
+ bindEvent(obj, event, func);
+ var ret = function() {
+ unbindEvent(obj, event, func);
+ };
+ ret.unbind = function() {ret();};
+ return ret;
+ },
+ unbind: function(unbinder) {
+ unbinder();
+ }
+ };
+
+ }());
+
+ function getPosition(evt) {
+ var posX = 0;
+ var posY = 0;
+ if (evt.targetTouches) {
+ posX = evt.targetTouches[0].pageX;
+ posY = evt.targetTouches[0].pageY;
+ } else if (evt.pageX || evt.pageY) {
+ posX = evt.pageX;
+ posY = evt.pageY;
+ } else if (evt.clientX || evt.clientY) {
+ posX = evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
+ posY = evt.clientY + document.body.scrollTop + document.documentElement.scrollTop;
+ }
+ return {x: posX, y: posY};
+ }
+
+// ----------------------------------------------------------------------------
+// Expose
+
+ window.DragDrop = DragDrop;
+
+}()); \ No newline at end of file