1 /** * @author Garrett Smith, � 2008 * @version 1 * @fileoverview: * contains: APE.drag.Draggable, DragHandlers, APE.drag.DropTarget * * @requires APE.EventPublisher, APE.dom * * classNames: * <ul> * <li>activeDragClassName</li> * <li>focusClassName</li> * <li>selectedClassName</li> * <li>dragOverClassName (for dropTarget)</li> * <li>focusClassName (focused drag object)</li> * </ul> * * APE.drag.Draggable Features: * <ul> * <li>dragCopy</li> * <li>dragMultiple</li> * <li>setHandle(handle, useTree)</li> * </ul> * * @example Create a Draggable: *<pre> * var Draggable = APE.drag.Draggable, * el = document.getElementById(<var>"box"</var>), * box = Draggable.getByNode( el ); * box.keepInContainer = true; * box.activeDragClassName = "boxDragging"; * box.focusClassName = "boxFocused"; * * var bigBx = box.addDropTarget( document.getElementById("biggerBox") ); * bigBx.dragOverClassName = "boxDragOver"; * </pre> */ /** @name APE.drag.Draggable * @namespace */ APE.namespace("APE.drag"); /** * @private * @description Do not call the constructor directly. * @param {HTMLElement} el the element to drag. * @param {uint} [constraint] (0 | 1 | 2) default is 0. * @see APE.drag.Draggable.constraints */ APE.drag.Draggable = function(el, constraint) { this.id = el.id; this.el = this.origEl = el; this.style = el.style; this.isRel = APE.dom.getStyle(el, "position").toLowerCase() == "relative"; // default 'container' is the containing block. this.container = (this.isRel ? el.parentNode : APE.dom.getContainingBlock(el)); this.dropTargets = []; this.handle = el; this.constraint = constraint||0; this.init(); }; /** * @memberOf APE.drag.Draggable * @function * @return {APE.drag.Draggable} * @param {HTMLElement} el * @param {uint} [constraint] (0 | 1 | 2) default is 0. * @static * @description Use Draggable.getByNode, <em>not</em> new. */ APE.drag.Draggable.getByNode = APE.getByNode; /** @name APE.drag.instanceDestructor * @private * @param {HTMLElement} el the element to drag * @param {uint} constraint * @see APE.drag.Draggable.constraints */ APE.drag.Draggable.instanceDestructor = function() { var x, p, dO, DragHandlers = APE.drag.DragHandlers; for(x in this.instances) { dO = this.instances[x]; for(p in dO) if(dO.hasOwnProperty(p)) delete dO[p]; delete this.instances[x]; } if(dO) { dO.constructor.draggableList = {}; DragHandlers.focusedDO = DragHandlers.dO = null; } }; /** * @private * Callback handler for a draggable's intrinsic focus event. */ APE.drag.Draggable.focused = function(e) { // TODO: separate concern. // IE will fire focus events when, in retireClone, when this.el = this.origEl. // This can be demonstrated by holding the mouse down on a dragObj for 2 sec, then releasing the mouse. if(timeStamp - arguments.callee.timeStamp < 5) return; // recurrant. arguments.callee.timeStamp = timeStamp; // record. e = e||event; var timeStamp = new Date-0; if(typeof e.stopPropagation == "function") e.stopPropagation(); else e.cancelBubble = true; this.setFocus(true, e); }; /** * @private * Callback handler for a draggable's intrinsic focus event. */ APE.drag.Draggable.blurred = function(e) { this.setFocus(false, e); }; /** * @memberOf APE.drag.Draggable */ APE.drag.Draggable.constraints = { NONE : 0, HORZ : 1, VERT : 2 }; APE.drag.Draggable.prototype = { /** @type {boolean} * @private * @description internal flag */ hasFocus : false, /** @type {boolean} * @description set to true to make a temporary "ghost" copy dragged. */ dragCopy : false, /** @type {boolean} * @description set to true to allow this to be dragged as a group. */ dragMultiple : false, isSelected : false, /** * A subset of dropTargets that have ondragover or ondragout. * created onmousedown, to help boost performance by reducing count for ondragover * **/ _dragOverTargets : false, /** @event * @description Has been grabbed. */ onfocus : undefined, /** @event * @description Has been blurred. */ onblur : undefined, /** @event * @description Is about to move. */ onbeforedrag : undefined, /** @event * @description Has been grabbed. */ onbeforedragstart : undefined, /** @event * @param {Event} e dom event. * Mouse has moved. */ ondragstart : undefined, /** @event * @param {Event} e dom event. * @description Being dragged */ ondrag : undefined, /** @event * @description Dragging stopped before it escaped its container. */ ondragstop : undefined, /** @event * @description Dragging completed (as a result of mouseup). */ ondragend : undefined, /**@type {Number} * @description current x position*/ x : 0, /**@type {Number} * @description current y position*/ y : 0, /**@type {Number} * @description where drag started from */ origX : 0, /**@type {Number} * @description where drag started from */ origY : 0, /**@type {Number} * @description where draggable was grabbed from */ grabX : 0, /**@type {Number} * @description where draggable was grabbed from */ grabY : 0, /** @type {Number} * @description Where it will move to next. onbeforedrag */ newX : 0, /** @type {Number} * @description Where it will move to next. onbeforedrag */ newY : 0, /** @type {Number} * @description default: APE.drag.Draggable.constraints.NONE */ constraint : APE.drag.Draggable.constraints.NONE, /** @type {boolean} * @description drag object can be dragged outside of its container */ keepInContainer : false, /** @type {boolean} * @description drag object can be disabled by setting to this to false */ isDragEnabled : true, /** @type {String} * @description className to add when selected. */ selectedClassName : "", /** @type {String} * @description className to add before being dragged. */ activeDragClassName : "", /** @type {String} * @description className to add when focused. */ focusClassName : "", init : function(){ var EventPublisher = APE.EventPublisher, drag = APE.drag, Draggable = drag.Draggable, el = this.el; el.style.zIndex = APE.dom.getStyle(el, "zIndex") || Draggable.highestZIndex++; this._setIeTopLeft(); EventPublisher.add(el, "onfocus", Draggable.focused, this); EventPublisher.add(el, "onblur", Draggable.blurred, this); // For IE, if the attribute is not present, 0 will be returned. // For Moz, Webkit, Op, if attribute is not present, null will be returned, // but the default value for the DOM property will be -1 (truthy), so use getAttribute. if(!el.getAttribute('tabIndex')) el.tabIndex = 0; // Allow default kbd navigation. /** Will be dragged */ this.onbeforeexitcontainer = function() { return !this.keepInContainer; }; drag.DragHandlers.init(); }, useHandleTree : true, hasHandleSet : false, /** Sets a handle on a draggable * @param {HTMLElement} el the element to use as a handle. * By default, the handle is the draggable. * @param {boolean} [setHandleTree] if true, the draggable can use anything in the * handle's subtree for dragging. */ setHandle : function(el, setHandleTree){ this.handle = el; this.hasHandleSet = true; // Make sure user didn't forget the secondParam and expect true. this.useHandleTree = setHandleTree != false; }, /** @param {HTMLElement} target Element that is checked. * @private */ isInHandle : function(target) { return target == this.handle || (this.useHandleTree && APE.dom.contains( this.handle, target )); }, /** * Adds a drop target. * @param {HTMLElement|APE.drag.DropTarget} dropTarget either an element or a DropTarget. * @return {DropTarget} The drop target that was added. */ addDropTarget : function(dropTarget) { var DropTarget = APE.drag.DropTarget, el = DropTarget.getByNode(dropTarget).el, dropTargets = this.dropTargets; if(this.el === el) return; return dropTargets[dropTargets.length] = DropTarget.getByNode(el); }, /** * Grabs the draggable, centering it under the cursor. * @param {Event} e the event to grab the element from. * @param {int} [xOffset] amount of horizontal adjustment to apply. * @param {int} [xOffset] amount of vertical adjustment to apply. */ grab : function(e, xOffset, yOffset) { if(!e) e = event; var dom = APE.dom, Event = dom.Event, target = Event.getTarget(e), drag = APE.drag, DragHandlers = drag.DragHandlers; if(e.preventDefault) e.preventDefault(); e.returnValue = false; if(dom.contains(this.el, target)) return; this._fixFocus(e); var grabCoords = dom.getPixelCoords(this.el); this.grabX = grabCoords.x; this.grabY = grabCoords.y; // Get the container's offset. var eventCoords = Event.getCoords(e), offsetCoords = dom.getOffsetCoords(dom.getContainingBlock(this.el)), offsetY = eventCoords.y - offsetCoords.y, newY = Math.floor(offsetY - (this.handle.offsetHeight/2)), handleOffsetCoords = dom.getOffsetCoords(this.handle, this.el), constraints = drag.Draggable.constraints; if(this.constraint != constraints.VERT) { // Center the dragObject around the coords, but keep it inside. var offsetX = eventCoords.x - offsetCoords.x, newX = offsetX - Math.floor((this.handle.offsetWidth/2)); if(this.keepInContainer) { newX = Math.max(newX, 0); newX = Math.min(newX, this.container.clientWidth - this.el.offsetWidth); } this.moveToX(newX- handleOffsetCoords.x + (xOffset||0)); } if(this.constraint != constraints.HORZ) { if(this.keepInContainer) { newY = Math.max(newY, 0); newY = Math.min(newY, this.container.clientHeight - this.el.offsetHeight); } this.moveToY(newY - handleOffsetCoords.y + (yOffset||0)); } DragHandlers.dragObjGrabbed(e, this); DragHandlers.dO = this; }, /** * Selects the draggable, adding selectedClassName * @param {boolean} isSelect if false, deselects. */ select : function(isSelect) { var APE = window.APE, Draggable = APE.drag.Draggable; if(isSelect) { if(this.selectedClassName) APE.dom.addClass(this.el, this.selectedClassName); // onselect handler would go here, if/when needed. return false to prevent. if(this.dragMultiple && ! (this.id in Draggable.draggableList)) Draggable.draggableList[this.id] = this; } else { if(this.selectedClassName) APE.dom.removeClass(this.el, this.selectedClassName); // ondeselect handler would go here, if/when needed. delete Draggable.draggableList[this.id]; } this.isSelected = Boolean(isSelect); } /** * @param {boolean} isFocus if false, blurs. * @param {Event} e the DOM event. */ ,setFocus : function(isFocus, e) { if(isFocus == this.hasFocus) return; // nothing to do. if(!this.isDragEnabled) return false; //console.log('set ' + this + ' focus = ' + isFocus); var ret = true, DragHandlers = APE.drag.DragHandlers, dom = APE.dom; if(isFocus) { if(this.focusClassName) dom.addClass(this.el, this.focusClassName); if(typeof this.onfocus == "function") { ret = this.onfocus(e); } if(ret != false) { if(DragHandlers.focusedDO) { DragHandlers.focusedDO.setFocus(false, e); } DragHandlers.focusedDO = this; // Should be afterAdvice. } } else { if(this.focusClassName) dom.removeClass(this.el, this.focusClassName); if(typeof this.onblur == "function") ret = this.onblur(e); if(ret != false) DragHandlers.focusedDO = null; // Should be afterAdvice. } this.hasFocus = isFocus; return ret; }, /** @private * @type {APE.drag.DropTarget[]|boolean} * @description An array of DropTarget that has one of: * an ondragover or ondragout handler * a hoverClassName */ _dragOverTargets : false, /** @private */ _fixFocus : function(e) { // Mozilla will not give focus to/remove focus from an element when mouseDown returns false; // However, it is necessary to return false onmousedown to prevent selecting // an img dragObj and having the browser "grab" it. // // IE won't set focus unless the element has tabIndex. // Conditionally force focus when the element was clicked, regardless of IE/Moz anomalies. // Mozilla still won't get the focus event. var metaKey = e.metaKey || (/Win/.test(navigator.platform) && e.ctrlKey); if(!this.dragMultiple && APE.drag.DragHandlers.focusedDO && metaKey) return false; if(!this.hasFocus) { this.setFocus(!this.hasFocus, e); } }, /** @private * grab will create a clone here, and will be released to here. */ dragStart : function(e) { if(this.isBeingDragged) return; if(this.dragCopy) { this.assignClone(e); // this.el assigned to copyEl, this.origEl stays put. } if(typeof this.ondragstart == "function") this.ondragstart(e); if(this.activeDragClassName) APE.dom.addClass(this.el, this.activeDragClassName); // Check the coords after making the copyEl here. APE.drag.DragHandlers.setUpCoords(e, this); this.isBeingDragged = true; }, /** * releases the draggable, as if the mouse had been released. * @param {Event} [e] the event that triggered release */ release : function(e) { APE.drag.DragHandlers.dragObjReleased(e, this); if(typeof this.onrelease == "function") this.onrelease(e); }, /**@private * creates a copyEl for dragCopy */ assignClone : function(e) { var dom = APE.dom, addClass = "addClass", copyEl, el = this.el, origEl = el, copyElStyle; if(!this.copyEl) { this.origEl = el; this.copyEl = el.cloneNode(true); } copyEl = this.copyEl; copyElStyle = copyEl.style; if(this.focusClassName) dom[this.hasFocus ? addClass : "removeClass"](copyEl, this.focusClassName); copyElStyle.display = ""; if(copyEl.parentNode != el.parentNode) // In case the element was appened elsewhere, by external script el.parentNode.insertBefore(copyEl, el); // 100 draggable items appear above. copyElStyle.zIndex = parseInt(origEl.style.zIndex) + 100; if(this.origClassName) dom[addClass](el, this.origClassName); this.el = copyEl; this.style = copyElStyle; // This helps prevent copyEl from displacing other elements. if(this.isRel) { copyElStyle.marginBottom = -origEl.offsetHeight + -(parseInt(dom.getStyle(origEl, "marginBottom"))||0) + "px"; copyElStyle.marginright = -origEl.offsetWidth + -(parseInt(dom.getStyle(origEl, "marginRight"))||0) + "px"; } }, /** @private */ retireClone : function() { // this causes IE to lose focus, then fire focus for another element (IE decides which one). this.constructor.focused.timeStamp = new Date; if(this.copyEl.style.display == "none") return; this.el = this.origEl; this.style = this.origEl.style; // Update position of origEl, which was left behind. this.moveToX(this.x); this.moveToY(this.y); this.copyEl.style.display = "none"; if(this.origClassName) APE.dom.removeClass(this.el, this.origClassName); }, moveToX : ('pixelLeft'in document.documentElement.style ? function(x) { this.style.pixelLeft = this.x = x; } : function(x) { this.style.left = (this.x = x) + "px"; }), moveToY : ('pixelTop'in document.documentElement.style ? function(y) { this.style.pixelTop = this.y = y;} : function(y) { this.style.top = (this.y = y) + "px";}), moveToXY : ('pixelTop'in document.documentElement.style ? function(x,y) { var s = this.style; s.pixelLeft = this.x = x; s.pixelTop = this.y = y; } : function(x,y) { var s = this.style; s.left = (this.x = x) + "px"; s.top = (this.y = y) + "px"; }), /** * @private */ glideStart : function(x, y) { // Would be cleaner to separate this concern; APE.drag.Draggable should not have to concern itself // for animation and notifying subscribers of onglide, et c. if(this.animTimer) return; this.startX = x; this.startY = y; var dx = this.startX - this.grabX, dy = this.startY - this.grabY; // Calculate Hypoteneuse. this.GlideDist = Math.ceil(Math.sqrt((dx * dx) + (dy * dy))); if(this.GlideDist === 0) return; this.rx = Math.abs(dx)/this.GlideDist; this.ry = Math.abs(dy)/this.GlideDist; if(this.x > this.grabX) this.rx = -this.rx; if(this.y > this.grabY) this.ry = -this.ry; this.startTime = new Date().getTime(); this.animTimer = window.setInterval("APE.drag.Draggable.instances['"+this.id+"'].glide()", 10); }, /** * @private */ glide : function() { var t = new Date - this.startTime, // 2px per 10ms slight acceleration 10px/s d = Math.ceil(2 * t + .5 * .01 * t*t); if(d >= this.GlideDist) { this.animTimer = clearInterval(this.animTimer); if(this.constraint != 2) this.moveToX( this.grabX ); if(this.constraint != 1) this.moveToY( this.grabY ); if(this.copyEl) { this.el = this.origEl; this.style = this.origEl.style; this.copyEl.style.display = "none"; } if(typeof this.onglide == "function") this.onglide(); if(typeof this.onglideend == "function") this.onglideend(); this.dragDone({}); } else { if(this.constraint != 2) this.moveToX(this.startX + d * this.rx); if(this.constraint != 1) this.moveToY(this.startY + d * this.ry); if(typeof this.onglide == "function") this.onglide(); } }, /** Starts gliding the draggable back to its original x,y coords. * @param {Number} [x] x coordinate to start gliding from. * @param {Number} [y] y coordinate to start gliding from. */ animateBack : function(x, y) { this.glideStart(x||this.x, y||this.y); }, /** A dragObj does not check search for containing block each time its grabbed/dragged. * Instead, it reuses the container. If the container must change, this must be done * manually, via dragObj.setContainer(newEl); */ setContainer : function(el) { this.container = el; }, /** * Removes a drop target. * @param {HTMLElement|DropTarget} element or DropTarget to remove. * @return {HTMLElement} the removed dropTarget element. */ removeDropTarget : function(el){ el = document.getElementById(el.id); for(var i = 0, len = this.dropTargets.length; i < len; i++) { if(this.dropTargets[i].el === el) { this.dropTargets.splice(i, 1); return el; } } return null; }, /** * @fires this.ondragend() * fires the ondragend handler. */ dragDone : function(e) { if(this.activeDragClassName) APE.dom.removeClass(this.el, this.activeDragClassName); if(typeof this.ondragend == "function" && this.hasBeenDragged) { this.ondragend(e); } if(this.copyEl) { // in case user does some appending of el, et c. this.el.parentNode.insertBefore(this.copyEl, this.el); } this.hasBeenDragged = false; }, // For some browsers (IE and Safari), the currentStyle/computedStyle // for top/left will be "auto" when bottom and right values are set. _setIeTopLeft : function() { // For IE, set top/left values when declared values are auto // and right/bottom values are given. var dv = document.defaultView, el = this.el, s = el.style, cs = el.currentStyle || (dv.getComputedStyle && dv.getComputedStyle(el,"")) || s, cb = APE.dom.getContainingBlock(el), curL = cs.left, curR = cs.right, curT = cs.top, curB = cs.bottom; // Calculate left when right is given pixel value and left is "auto". if((curL == "" || curL == "auto")) { curR = parseInt(curR); if(isFinite(curR)) s.left = cb.clientWidth - el.offsetWidth - curR + "px"; else s.left = "0"; } // Calculate top when bottom is given pixel value and top is "auto". if((curT == "" || curT == "auto")) { curB = parseInt(curB); if(isFinite(curB)) { s.top = cb.clientHeight - el.offsetHeight - curB + "px"; } else s.top = "0"; } }, toString : function() { return "APE.drag.Draggable(id=" +this.id +")"; } }; /** @type {Number} * @description a higher z-index is assigned beforedragstart. */ APE.drag.Draggable.highestZIndex = 1000; /** @type {Object} * @description Internal map of draggables */ APE.drag.Draggable.draggableList = { }; /** * called before dragstart, this function checks to see if there are any droptargets * that need mousemove consideration. For example, if the droptarget has a * dragOverClassName, or has an ondragover handler. * @private */ APE.drag.Draggable._setUpDragOver = function(dO) { // subset for ondragover, to help speed up dragging // with multiple drop targets. dO._dragOverTargets = []; var dropTargets = dO.dropTargets, dt