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