This repository has been archived on 2018-10-12. You can view files and clone it, but cannot push or open issues or pull requests.
node-task/public/js/ink.draggable.js

448 lines
20 KiB
JavaScript

/**
* Dragging elements around
* @module Ink.UI.Draggable_1
* @version 1
*/
Ink.createModule("Ink.UI.Draggable","1",["Ink.Dom.Element_1", "Ink.Dom.Event_1", "Ink.Dom.Css_1", "Ink.Dom.Browser_1", "Ink.Dom.Selector_1", "Ink.UI.Common_1"],function( InkElement, InkEvent, Css, Browser, Selector, Common) {
'use strict';
var x = 0,
y = 1; // For accessing coords in [x, y] arrays
// Get a value between two boundaries
function between (val, min, max) {
val = Math.min(val, max);
val = Math.max(val, min);
return val;
}
/**
* @class Ink.UI.Draggable
* @version 1
* @constructor
* @param {String|DOMElement} target Target element.
* @param {Object} [options] Optional object to configure the component.
* @param {String} [options.constraint] Movement constraint. None by default. Can be `vertical`, `horizontal`, or `both`.
* @param {String|DOMElement} [options.constraintElm] Constrain dragging to be within this element. None by default.
* @param {Number} [options.top] Limits to constrain draggable movement.
* @param {Number} [options.right] Limits to constrain draggable movement.
* @param {Number} [options.bottom] Limits to constrain draggable movement.
* @param {Number} [options.left] Limits to constrain draggable movement.
* @param {String|DOMElement} [options.handle] If specified, this element or CSS ID will be used as a handle for dragging.
* @param {Boolean} [options.revert] Flag to revert the draggable to the original position when dragging stops.
* @param {String} [options.cursor] Cursor type (CSS `cursor` value) used when the mouse is over the draggable object.
* @param {Number} [options.zIndex] Z-index applied to the draggable element while dragged.
* @param {Number} [options.fps] If set, throttles the drag effect to this number of frames per second.
* @param {DOMElement} [options.droppableProxy] If set, a shallow copy of this element will be moved around with transparent background.
* @param {String} [options.mouseAnchor] Anchor for the drag. Can be one of: 'left','center','right','top','center','bottom'.
* @param {String} [options.dragClass] Class to add when the draggable is being dragged. Defaults to drag.
* @param {Boolean} [options.skipChildren=true] Whether you have to drag the actual element, or dragging one of the children is okay too.
* @param {Function} [options.onStart] Callback called when dragging starts.
* @param {Function} [options.onEnd] Callback called when dragging stops.
* @param {Function} [options.onDrag] Callback called while dragging, prior to position updates.
* @param {Function} [options.onChange] Callback called while dragging, after position updates.
*
* @sample Ink_UI_Draggable_1.html
*/
function Draggable() {
Common.BaseUIComponent.apply(this, arguments);
}
Draggable._name = 'Draggable_1';
Draggable._optionDefinition = {
constraint: ['String', false],
constraintElm: ['Element', false],
top: ['Number', false],
right: ['Number', false],
bottom: ['Number', false],
left: ['Number', false],
handle: ['Element', false],
revert: ['Boolean', false],
cursor: ['String', 'move'],
zIndex: ['Number', 9999],
fps: ['Number', 0],
droppableProxy: ['Element', false],
mouseAnchor: ['String', undefined],
dragClass: ['String', 'drag'],
skipChildren: ['Boolean', true], // Magic/More Magic
onStart: ['Function', false],
onEnd: ['Function', false],
onDrag: ['Function', false],
onChange: ['Function', false]
};
Draggable.prototype = {
/**
* Init function called by the constructor
*
* @method _init
* @param {String|DOMElement} element Element ID of the element or DOM Element.
* @param {Object} [options] Options object for configuration of the module.
* @private
*/
_init: function() {
var o = this._options;
this.constraintElm = o.constraintElm && Common.elOrSelector(o.constraintElm);
this.handle = false;
this.elmStartPosition = false;
this.active = false;
this.dragged = false;
this.prevCoords = false;
this.placeholder = false;
this.position = false;
this.zindex = false;
this.firstDrag = true;
if (o.fps) {
this.deltaMs = 1000 / o.fps;
this.lastRunAt = 0;
}
this.handlers = {};
this.handlers.start = Ink.bindEvent(this._onStart,this);
this.handlers.dragFacade = Ink.bindEvent(this._onDragFacade,this);
this.handlers.drag = Ink.bindEvent(this._onDrag,this);
this.handlers.end = Ink.bindEvent(this._onEnd,this);
this.handlers.selectStart = function(event) { InkEvent.stop(event); return false; };
// set handle
this.handle = (this._options.handle) ?
Common.elOrSelector(this._options.handle) :
this._element;
this.handle.style.cursor = o.cursor;
InkEvent.observe(this.handle, 'touchstart', this.handlers.start);
InkEvent.observe(this.handle, 'mousedown', this.handlers.start);
if (Browser.IE) {
InkEvent.observe(this._element, 'selectstart', this.handlers.selectStart);
}
},
/**
* Removes the ability of the element of being dragged
*
* @method destroy
* @public
*/
destroy: function() {
InkEvent.stopObserving(this.handle, 'touchstart', this.handlers.start);
InkEvent.stopObserving(this.handle, 'mousedown', this.handlers.start);
if (Browser.IE) {
InkEvent.stopObserving(this._element, 'selectstart', this.handlers.selectStart);
}
},
/**
* Gets coordinates for a given event (with added page scroll)
*
* @method _getCoords
* @param {Object} e window.event object.
* @return {Array} Array where the first position is the x coordinate, the second is the y coordinate
* @private
*/
_getCoords: function(e) {
var ps = [InkElement.scrollWidth(), InkElement.scrollHeight()];
return {
x: (e.touches ? e.touches[0].clientX : e.clientX) + ps[x],
y: (e.touches ? e.touches[0].clientY : e.clientY) + ps[y]
};
},
/**
* Clones src element's relevant properties to dst
*
* @method _cloneStyle
* @param {DOMElement} src Element from where we're getting the styles
* @param {DOMElement} dst Element where we're placing the styles.
* @private
*/
_cloneStyle: function(src, dst) {
dst.className = src.className;
dst.style.borderWidth = '0';
dst.style.padding = '0';
dst.style.position = 'absolute';
dst.style.width = InkElement.elementWidth(src) + 'px';
dst.style.height = InkElement.elementHeight(src) + 'px';
dst.style.left = InkElement.elementLeft(src) + 'px';
dst.style.top = InkElement.elementTop(src) + 'px';
dst.style.cssFloat = Css.getStyle(src, 'float');
dst.style.display = Css.getStyle(src, 'display');
},
/**
* onStart event handler
*
* @method _onStart
* @param {Object} e window.event object
* @return {Boolean|void} In some cases return false. Otherwise is void
* @private
*/
_onStart: function(e) {
if (!this.active && InkEvent.isLeftClick(e) || typeof e.button === 'undefined') {
var tgtEl = InkEvent.element(e);
if (this._options.skipChildren && tgtEl !== this.handle) { return; }
InkEvent.stop(e);
Css.addClassName(this._element, this._options.dragClass);
this.elmStartPosition = [
InkElement.elementLeft(this._element),
InkElement.elementTop( this._element)
];
var pos = [
parseInt(Css.getStyle(this._element, 'left'), 10),
parseInt(Css.getStyle(this._element, 'top'), 10)
];
var dims = InkElement.elementDimensions(this._element);
this.originalPosition = [ pos[x] ? pos[x]: null, pos[y] ? pos[y] : null ];
this.delta = this._getCoords(e); // mouse coords at beginning of drag
this.active = true;
this.position = Css.getStyle(this._element, 'position');
this.zindex = Css.getStyle(this._element, 'zIndex');
var div = document.createElement('div');
div.style.position = this.position;
div.style.width = dims[x] + 'px';
div.style.height = dims[y] + 'px';
div.style.marginTop = Css.getStyle(this._element, 'margin-top');
div.style.marginBottom = Css.getStyle(this._element, 'margin-bottom');
div.style.marginLeft = Css.getStyle(this._element, 'margin-left');
div.style.marginRight = Css.getStyle(this._element, 'margin-right');
div.style.borderWidth = '0';
div.style.padding = '0';
div.style.cssFloat = Css.getStyle(this._element, 'float');
div.style.display = Css.getStyle(this._element, 'display');
div.style.visibility = 'hidden';
this.delta2 = [ this.delta.x - this.elmStartPosition[x], this.delta.y - this.elmStartPosition[y] ]; // diff between top-left corner of obj and mouse
if (this._options.mouseAnchor) {
var parts = this._options.mouseAnchor.split(' ');
var ad = [dims[x], dims[y]]; // starts with 'right bottom'
if (parts[0] === 'left') { ad[x] = 0; } else if(parts[0] === 'center') { ad[x] = parseInt(ad[x]/2, 10); }
if (parts[1] === 'top') { ad[y] = 0; } else if(parts[1] === 'center') { ad[y] = parseInt(ad[y]/2, 10); }
this.applyDelta = [this.delta2[x] - ad[x], this.delta2[y] - ad[y]];
}
var dragHandlerName = this._options.fps ? 'dragFacade' : 'drag';
this.placeholder = div;
if (this._options.onStart) { this._options.onStart(this._element, e); }
if (this._options.droppableProxy) { // create new transparent div to optimize DOM traversal during drag
this.proxy = document.createElement('div');
dims = [
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
];
var fs = this.proxy.style;
fs.width = dims[x] + 'px';
fs.height = dims[y] + 'px';
fs.position = 'fixed';
fs.left = '0';
fs.top = '0';
fs.zIndex = this._options.zindex + 1;
fs.backgroundColor = '#FF0000';
Css.setOpacity(this.proxy, 0);
var firstEl = document.body.firstChild;
while (firstEl && firstEl.nodeType !== 1) { firstEl = firstEl.nextSibling; }
document.body.insertBefore(this.proxy, firstEl);
InkEvent.observe(this.proxy, 'mousemove', this.handlers[dragHandlerName]);
InkEvent.observe(this.proxy, 'touchmove', this.handlers[dragHandlerName]);
}
else {
InkEvent.observe(document, 'mousemove', this.handlers[dragHandlerName]);
}
this._element.style.position = 'absolute';
this._element.style.zIndex = this._options.zindex;
this._element.parentNode.insertBefore(this.placeholder, this._element);
this._onDrag(e);
InkEvent.observe(document, 'mouseup', this.handlers.end);
InkEvent.observe(document, 'touchend', this.handlers.end);
return false;
}
},
/**
* Function that gets the timestamp of the current run from time to time. (FPS)
*
* @method _onDragFacade
* @param {Object} window.event object.
* @private
*/
_onDragFacade: function(e) {
var now = +new Date();
if (!this.lastRunAt || now > this.lastRunAt + this.deltaMs) {
this.lastRunAt = now;
this._onDrag(e);
}
},
/**
* Function that handles the dragging movement
*
* @method _onDrag
* @param {Object} window.event object.
* @private
*/
_onDrag: function(e) {
if (this.active) {
InkEvent.stop(e);
this.dragged = true;
var mouseCoords = this._getCoords(e),
mPosX = mouseCoords.x,
mPosY = mouseCoords.y,
o = this._options,
newX = false,
newY = false;
if (this.prevCoords && mPosX !== this.prevCoords.x || mPosY !== this.prevCoords.y) {
if (o.onDrag) { o.onDrag(this._element, e); }
this.prevCoords = mouseCoords;
newX = this.elmStartPosition[x] + mPosX - this.delta.x;
newY = this.elmStartPosition[y] + mPosY - this.delta.y;
var draggableSize = InkElement.elementDimensions(this._element);
if (this.constraintElm) {
var offset = InkElement.offset(this.constraintElm);
var size = InkElement.elementDimensions(this.constraintElm);
var constTop = offset[y] + (o.top || 0),
constBottom = offset[y] + size[y] - (o.bottom || 0),
constLeft = offset[x] + (o.left || 0),
constRight = offset[x] + size[x] - (o.right || 0);
newY = between(newY, constTop, constBottom - draggableSize[y]);
newX = between(newX, constLeft, constRight - draggableSize[x]);
} else if (o.constraint) {
var right = o.right === false ? InkElement.pageWidth() - draggableSize[x] : o.right,
left = o.left === false ? 0 : o.left,
top = o.top === false ? 0 : o.top,
bottom = o.bottom === false ? InkElement.pageHeight() - draggableSize[y] : o.bottom;
if (o.constraint === 'horizontal' || o.constraint === 'both') {
newX = between(newX, left, right);
}
if (o.constraint === 'vertical' || o.constraint === 'both') {
newY = between(newY, top, bottom);
}
}
var Droppable = Ink.getModule('Ink.UI.Droppable_1');
if (this.firstDrag) {
if (Droppable) { Droppable.updateAll(); }
/*this._element.style.position = 'absolute';
this._element.style.zIndex = this._options.zindex;
this._element.parentNode.insertBefore(this.placeholder, this._element);*/
this.firstDrag = false;
}
if (newX) { this._element.style.left = newX + 'px'; }
if (newY) { this._element.style.top = newY + 'px'; }
if (Droppable) {
// apply applyDelta defined on drag init
var mouseCoords2 = this._options.mouseAnchor ?
{x: mPosX - this.applyDelta[x], y: mPosY - this.applyDelta[y]} :
mouseCoords;
Droppable.action(mouseCoords2, 'drag', e, this._element);
}
if (o.onChange) { o.onChange(this); }
}
}
},
/**
* Function that handles the end of the dragging process
*
* @method _onEnd
* @param {Object} window.event object.
* @private
*/
_onEnd: function(e) {
InkEvent.stopObserving(document, 'mousemove', this.handlers.drag);
InkEvent.stopObserving(document, 'touchmove', this.handlers.drag);
if (this._options.fps) {
this._onDrag(e);
}
Css.removeClassName(this._element, this._options.dragClass);
if (this.active && this.dragged) {
if (this._options.droppableProxy) { // remove transparent div...
document.body.removeChild(this.proxy);
}
if (this.pt) { // remove debugging element...
InkElement.remove(this.pt);
this.pt = undefined;
}
/*if (this._options.revert) {
this.placeholder.parentNode.removeChild(this.placeholder);
}*/
if(this.placeholder) {
InkElement.remove(this.placeholder);
}
if (this._options.revert) {
this._element.style.position = this.position;
if (this.zindex !== null) {
this._element.style.zIndex = this.zindex;
}
else {
this._element.style.zIndex = 'auto';
} // restore default zindex of it had none
this._element.style.left = (this.originalPosition[x]) ? this.originalPosition[x] + 'px' : '';
this._element.style.top = (this.originalPosition[y]) ? this.originalPosition[y] + 'px' : '';
}
if (this._options.onEnd) {
this._options.onEnd(this._element, e);
}
var Droppable = Ink.getModule('Ink.UI.Droppable_1');
if (Droppable) {
Droppable.action(this._getCoords(e), 'drop', e, this._element);
}
this.position = false;
this.zindex = false;
this.firstDrag = true;
}
this.active = false;
this.dragged = false;
}
};
Common.createUIComponent(Draggable);
return Draggable;
});