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, i = 0, len = dropTargets.length; for(; i < len; i++) { dt = dropTargets[i]; dt.initCoords(); if(typeof dt.ondragover == "function" || typeof dt.ondragout == "function" || dt.dragOverClassName) dO._dragOverTargets.push(dt); } // set to false, for quicker access on drag over. if(dO._dragOverTargets.length === 0) dO._dragOverTargets = false; }; /** APE.drag.DropTarget * * @param{HTMLElement} el * private constructor - use draggable.addDropTarget(el); * @private */ APE.drag.DropTarget = function(el) { this.el = el; this.id = el.id; }; APE.drag.DropTarget.getByNode = APE.getByNode; APE.drag.DropTarget.prototype = { /** @type {Object} * @description {x,y} coords of DropTarget */ coords : undefined, /** @type {String} * @description the className to add when selected. */ dragOverClassName : "", initCoords : function() { if(!this.coords) this.coords = {}; APE.dom.getOffsetCoords(this.el, document, this.coords); this.coords.w = this.el.clientWidth; this.coords.h = this.el.clientHeight; }, /** checks to see if the coordinates * x and y are both inside dropTarget * @param {Object} curs {x,y} coordinates of the event. */ containsCoords : function(curs) { // check for x, then y. var dt_x = this.coords.x, dt_y = this.coords.y; return (curs.x >= dt_x && curs.x <= dt_x + this.coords.w) && // now check for y. (curs.y >= dt_y && curs.y <= dt_y + this.coords.h); }, /** * @event * Dragged over a droptarget */ ondragover : false, /** * @event * Dragged off a droptarget */ ondragout : undefined, /** * @event * Hit a drop target. Fires for each object being dragged. */ ondrop : undefined }; /** DragHandlers * * @memberOf APE.drag * * methods: * mouseDown - initializes dragging * * mouseMove - tracks the mouse position and updates dO * * mouseUp - releases any dO and calls ondragend, * passing the event. The event has a dropTarget * property, which may be null. * */ APE.drag.DragHandlers = { /**@type {HTMLElement} * @description the element being actively dragged. */ dO : null, /**@type {HTMLElement} * @description the element that has focus. */ focusedDO : null, /** * @function */ getEventCoords : APE.dom.Event.getCoords, /** Initializes event handlers for dragging. * this is called once when the first dragObj is created. */ init : function() { if(this.inited) return; var d = document, docEl = d.documentElement, ds = docEl.style, EventPublisher = APE.EventPublisher; EventPublisher.add(d, "onmousedown", this.mouseDown, this); EventPublisher.add(d, "onkeypress", this.keyPressed, this); EventPublisher.add(d, "onmousemove", this.mouseMove, this); EventPublisher.add(d, "onmouseup", this.mouseUp, this); // prevent text selection while dragging. if('onselectstart' in d) { EventPublisher.get(d, "onselectstart").addBefore(this.isInDrag, this); } else { EventPublisher.get(d, "onmousedown").addAfter(this.preventUserSelection, this); EventPublisher.get(d, "onmouseup").addAfter(this.preventUserSelection, this); } this.inited = true; this.userSelectType = "MozUserSelect"in ds ? "MozUserSelect" : "KhtmlUserSelect"in ds ? "KhtmlUserSelect" : "userSelect"in ds ? "userSelect" : ""; }, /** * @return true, if there is a draggable object. * we could have a focusedDO that has been released. In that case, it may still be * isBeingDragged = true; isBeingDragged is set to false in dragDone. This occurs after glideEnd. */ isInDrag : function() { return !this.dO && !this.focusedDO; }, /** * Prevents selection while user is dragging. */ preventUserSelection : function() { document.documentElement.style[this.userSelectType] = this.dO ? "none" : ""; }, /** * Called from grab() and from mousemove, when first started. */ dragObjGrabbed : function(e, dO) { if(typeof dO.onbeforedragstart == "function" && dO.onbeforedragstart(e) == false) return true; var DragHandlers = APE.drag.DragHandlers, dom = APE.dom, eventCoords = dom.Event.getCoords(e), elementPixelCoords;; DragHandlers.locked = true; DragHandlers.mousedownX = eventCoords.x; DragHandlers.mousedownY = eventCoords.y; elementPixelCoords = dom.getPixelCoords(dO.el); dO.origX = dO.grabX = elementPixelCoords.x; dO.origY = dO.grabY = elementPixelCoords.y; dO.isBeingDragged = false; }, /** * Called from dragStart. Sets initial x/y position values. */ setUpCoords : function(e, dO) { var dom = APE.dom, container = dO.container, el = dO.el, cb = dom.getContainingBlock(el), coords = dom.getOffsetCoords(cb, container), // subtract in-flow offsets. pixelCoords = dom.getPixelCoords(el), // Due to the AVK-CSSOM Mess, offsetTop/offsetLeft are broken - DO NOT USE offset*! // Instead, use getOffsetCoords(el, el.parentNode); offsetFromParent = dom.getOffsetCoords(el, el.parentNode), inFlowOffsetX = offsetFromParent.x - pixelCoords.x + coords.x, inFlowOffsetY = offsetFromParent.y - pixelCoords.y + coords.y; // Safari Bug: if el is inside a TD, safari adds the TD's offsetLeft to the // el's offsetLeft, even if the TD has position: relative. // Impl Note: Don't use margins for absolutely positioned elements for Safari. // Safari calculates offsetTop from parentNode border edge (not padding edge). // Safari 1.3 can't read style values from styleSheets. // Safari 1.3 also adds parentNode border-width to offsetLeft. // Safari 3 does not. TODO: test Safari 2. // Safari 1.3 adds padding-left and top to inFlowOffsets, Safari 3 does not. // Safari 1 can't read styles. TODO: test Safari 2. if(dO.keepInContainer) { dO.minX = 0 - inFlowOffsetX; dO.maxX = container.clientWidth - dO.el.offsetWidth - inFlowOffsetX; dO.minY = 0 - inFlowOffsetY; dO.maxY = container.clientHeight - dO.el.offsetHeight - inFlowOffsetY; } }, /** * Called on mousedown. */ mouseDown : function(e) { // TODO: This is too complicated. Focus/selection. DragMultiple. draggableList. // Need a way to encapsulate those complexities. if(!e) e = event; var dom = APE.dom, target = dom.Event.getTarget(e), dO = null, Draggable = APE.drag.Draggable, instances = Draggable.instances, testNode = target; for(;dO == null && testNode; testNode = dom.findAncestorWithAttribute(testNode, "id")) dO = instances[testNode.id]; var metaKey = e.metaKey || (/Win/.test(navigator.platform) && e.ctrlKey); if(dO) { // found. if(!dO.isDragEnabled) { if(!metaKey) { this.removeGroupSelection(); if(this.focusedDO) { // if we return false (we do, to preven focus), we must explicitly // call blur() for Firefox (this seems like a browser bug). // this.focusedDO.el.blur(); } } return false; // prevent focus. } // If it's got a handle, make sure user clicked the handle. if(!metaKey && dO.hasHandleSet && !dO.isInHandle( target ) ) { if(!this.locked) { this.removeGroupSelection(); this.dO = null; this.locked = false; } return; } else { if(!metaKey && !dO.isSelected) { // no metaKey, this.removeGroupSelection(); } e.returnValue = false; } // In Mozilla; the intrisinc focus event will not fire when the // mousedown calls preventDefault(). This is a bug in Mozilla. //else if(typeof e.preventDefault == "function") e.preventDefault(); } else { if(!this.locked) { if(!metaKey) { this.removeGroupSelection(); if(this.focusedDO) { this.focusedDO.setFocus(false, e); } } this.dO = null; this.locked = false; } return; } // User tried to add to selection, but can't. Just return. if(metaKey && this.hasGroupSelection() && !dO.dragMultiple) return false; // amend for Mozilla not gaining/losing focus from mouseDown return false. dO._fixFocus(e); if(!dO.dragMultiple) { if(!metaKey) this.removeGroupSelection(); // User tried to add to group. Just exit. else return false; } this.setGroupSelection(dO, metaKey); dO.style.zIndex = ++Draggable.highestZIndex; // User tried to drag a group and still had metaKey down. if(metaKey && dO.hasFocus) { ; } // Sets up dropTargets that have dragOverClassName | ondragover Draggable._setUpDragOver(dO); this.dragObjGrabbed(e, dO); for(var id in Draggable.draggableList) { this.dragObjGrabbed(e, Draggable.draggableList[id]); } this.dO = dO; return target.tagName != "IMG"; // Mozilla will prevent focus events for return false; }, setGroupSelection : function(dO, hasMetaKey) { var draggableList = APE.drag.Draggable.draggableList; if(hasMetaKey) { if(dO.id in draggableList) { // selected. this.deselectOnMouseup = true; } else { // not selected. dO.select(true); } } else if(!dO.isSelected) { // if not selected, deselect others. this.removeGroupSelection(); dO.select(true); } }, /** * When a draggable has been released (by ESC), it calls dragout from the relevant * droptargets and resets any active over droptargets. * @param {Event} e the event that triggered the release. This gets passed back to ondragout. * @param {Draggable} the draggable object that was released. */ dragObjReleased : function(e, dO) { dO.animateBack(); var APE = window.APE, removeClass = APE.dom.removeClass, draggableList = APE.drag.Draggable.draggableList, dt, i = 0, j = dO._dragOverTargets.length, id; if(dO._dragOverTargets !== false) { for(; i < j; i++) { dt = dO._dragOverTargets[i]; // Did we just move off dO dropTarget? if(dt.hasDropTargetOver) { if(typeof dt.ondragout == "function") dt.ondragout(e, dO); if(dt.dragOverClassName) removeClass(dt.el, dt.dragOverClassName); dt.hasDropTargetOver = false; } } } for(id in draggableList) { draggableList[id].animateBack(); } this.dO = null; }, /** * called on mousemove */ carryGroup : function(distX, distY) { var draggableList = APE.drag.Draggable.draggableList, o, id; for(id in draggableList) { o = draggableList[id]; if(distX != null) o.moveToX( o.origX + distX ); if(distY != null) o.moveToY( o.origY + distY ); } }, removeGroupSelection : function() { var id, drag = APE.drag, draggableList = drag.Draggable.draggableList; for(id in draggableList) { draggableList[id].select(false); } if(drag.DragHandlers.focusedDO) { drag.DragHandlers.focusedDO.select(false); } }, /** * returns true if there are any selected items. */ hasGroupSelection : function() { for(var id in APE.drag.Draggable.draggableList) return true; return false; }, /** * mousemove callback handler. */ mouseMove : function(e) { var dO = this.dO; if(dO == null) return; if(!e) e = event; var eventCoords = this.getEventCoords(e), ePageX = eventCoords.x, ePageY = eventCoords.y, distX = ePageX - this.mousedownX, distY = ePageY - this.mousedownY; // drag the bitch. if(dO.isBeingDragged == false) { dO.dragStart(e); var id, draggableList = APE.drag.Draggable.draggableList; for(id in draggableList) { draggableList[id].dragStart(e); } } dO.newX = dO.origX + distX; dO.newY = dO.origY + distY; dO.hasBeenDragged = (dO.hasBeenDragged || (distX || distY)); var constraints = APE.drag.Draggable.constraints, isLeft = dO.newX < dO.minX, isRight = dO.newX > dO.maxX, isAbove = dO.newY < dO.minY, isBelow = dO.newY > dO.maxY; if(typeof dO.onbeforedrag == "function" && dO.onbeforedrag(e) == false) return; var isOutsideContainer = dO.container != null, hasOnDrag = (typeof dO.ondrag == "function"), isBeforeExitContainerFunction = typeof dO.onbeforeexitcontainer == "function", planesStopped = 0; if(dO.constraint === constraints.NONE) { // no constraint. Life is hard. isOutsideContainer &= ( isLeft || isRight || isAbove || isBelow ); if(isOutsideContainer && (isBeforeExitContainerFunction || dO.onbeforeexitcontainer() == false)) { if(isLeft) { if(!dO.isAtLeft) { dO.moveToX( dO.minX ); // dO.minX - dO.origX = max possible negative distance to travel. this.carryGroup(dO.minX - dO.origX, null); if(hasOnDrag) dO.ondrag(e); dO.isAtRight = false; dO.isAtLeft = true; planesStopped += 1; } } else if(isRight) { if(!dO.isAtRight) { dO.moveToX( dO.maxX ); // dO.maxX - dO.origX = max possible positive distance to travel. this.carryGroup(dO.maxX - dO.origX, null); if(hasOnDrag) dO.ondrag(e); dO.isAtRight = true; dO.isAtLeft = false; planesStopped += 1; } } else { dO.isAtLeft = dO.isAtRight = false; dO.moveToX( dO.newX ); this.carryGroup(distX, null); } if(isAbove) { if(!dO.isAtTop) { dO.moveToY( dO.minY ); // dO.minY - dO.origY = max possible positive distance to travel. this.carryGroup(null, dO.minY - dO.origY); if(hasOnDrag) dO.ondrag(e); dO.isAtTop = true; dO.isAtBottom = false; planesStopped += 1; } } else if(isBelow) { if(!dO.isAtBottom) { if( dO.maxY > 0 ) dO.moveToY( dO.maxY ); // dO.maxY - dO.origY = max possible positive distance to travel. this.carryGroup(null, dO.maxY - dO.origY); if(hasOnDrag) dO.ondrag(e); dO.isAtTop = false; dO.isAtBottom = true; planesStopped += 1; } } else { dO.isAtTop = dO.isAtBottom = false; dO.moveToY( dO.newY ); this.carryGroup(null, distY); } dO.isDragStopped = planesStopped == 2; if(dO.isDragStopped && typeof dO.ondragstop == "function") dO.ondragstop(e); else if(hasOnDrag) dO.ondrag(e); } else { // In container. dO.isDragStopped = dO.isAtLeft = dO.isAtRight = dO.isAtTop = dO.isAtBottom = false; dO.moveToXY( dO.newX, dO.newY ); this.carryGroup(distX, distY); if(hasOnDrag) dO.ondrag(e); } } else { // A constraint. // A VERT type constraint? if(dO.constraint % 2 == 0) { isOutsideContainer &= (isAbove || isBelow); if(isOutsideContainer && (isBeforeExitContainerFunction || dO.onbeforeexitcontainer() == false)) { if(isAbove) { if(!dO.isAtTop) { dO.moveToY( dO.minY ); // dO.minY - dO.origY = max possible positive distance to travel. this.carryGroup(null, dO.minY - dO.origY); if(hasOnDrag) dO.ondrag(e); dO.isAtTop = !(dO.isAtBottom = false); } } else if(isBelow) { if(!dO.isAtBottom) { dO.moveToY( dO.maxY ); // dO.maxY - dO.origY = max possible positive distance to travel. this.carryGroup(null, dO.maxY - dO.origY); if(hasOnDrag) dO.ondrag(e); dO.isAtBottom = !(dO.isAtTop = false); } } if(!dO.isDragStopped) { if(typeof dO.ondragstop == "function") dO.ondragstop(e); dO.isDragStopped = true; } } else { // in container. dO.isAtTop = dO.isAtBottom = false; dO.isDragStopped = false; dO.moveToY( dO.newY ); this.carryGroup(null, distY); if(hasOnDrag) dO.ondrag(e); } } // A HORZ type constraint? else { isOutsideContainer &= (isLeft || isRight); if(isOutsideContainer && (isBeforeExitContainerFunction || dO.onbeforeexitcontainer() == false)) { if(isLeft) { if(!dO.isAtLeft) { dO.moveToX( dO.minX ); // dO.minX - dO.origX = max possible negative distance to travel. this.carryGroup(dO.minX - dO.origX, null); dO.isAtLeft = !(dO.isAtRight = false); } } else if(isRight) { if(!dO.isAtRight) { // dO.maxX - dO.origX = max possible negative distance to travel. this.carryGroup(dO.maxX - dO.origX, null); dO.moveToX( dO.maxX ); dO.isAtRight = !(dO.isAtLeft = false); } } if(!dO.isDragStopped) { if(typeof dO.ondragstop == "function") dO.ondragstop(e); dO.isDragStopped = true; } } else { // In container. dO.isAtLeft = dO.isAtRight = false; dO.isDragStopped = false; dO.moveToX( dO.newX ); this.carryGroup(distX, null); if(hasOnDrag) dO.ondrag(e); } } } // Handle dropTarget dragOver var _dragOverTargets = dO._dragOverTargets; if(_dragOverTargets !== false) { var coords = { x:ePageX, y:ePageY }, i = 0, j = _dragOverTargets.length, dt, dom = APE.dom, dragEvent = {domEvent:e, dragObj:dO}; for(; i < j; i++) { dt = _dragOverTargets[i], isInTarget = dt.containsCoords(coords); // Did we just move over this dropTarget? if(!dt.hasDropTargetOver && isInTarget) { dt.hasDropTargetOver = true; if(typeof dt.ondragover == "function") dt.ondragover(dragEvent); // typeof check now needed. if(dt.dragOverClassName) dom.addClass(dt.el, dt.dragOverClassName); } else { // Were we previously over this dropTarget? if(dt.hasDropTargetOver && !isInTarget) { if(typeof dt.ondragout == "function") dt.ondragout(dragEvent); if(dt.dragOverClassName) dom.removeClass(dt.el, dt.dragOverClassName); dt.hasDropTargetOver = false; } } } } return false; }, /** * mouseup callback handler. */ mouseUp : function(e) { // IE will fire this event twice when mouse was held. // IE fires mousemoves randomly, usually when the mouse is held. // For this case, just let dragDone run it's course and fire it's event. var isRandomMouseMoveEvent = (this.dO && this.dO.isBeingDragged && !this.dO.hasBeenDragged); if(this.dO == null || !this.dO.hasBeenDragged && !isRandomMouseMoveEvent) { if(this.dO && this.deselectOnMouseup && !this.dO.hasBeenDragged) { this.dO.select(false); } this.deselectOnMouseup = false; this.dO = null; this.locked = false; return; } if(!e) e = event; var dO = this.dO, draggableList = APE.drag.Draggable.draggableList, id, item; if(dO.copyEl) dO.retireClone(); for(id in draggableList) { item = draggableList[id]; if(item.copyEl) item.retireClone(); } // if it's been dragged onto a dropTarget, fire that event. var targets = dO.dropTargets, len = targets.length, o, x, y; if(len > 0) { var coords = this.getEventCoords(e), dropTarget, i = 0; for(; i < len; i++) { dropTarget = targets[i]; if(dropTarget.containsCoords(coords)) { dropTarget.containsCoords(coords); if(typeof dropTarget.ondrop == "function") dropTarget.ondrop({domEvent:e, dragObj:dO, dropTarget:dropTarget}); for(id in draggableList) { // Assume that draggable groups share dropTargets. if(id === dropTarget.id) continue; if(typeof dropTarget.ondrop == "function") { o = draggableList[id]; dropTarget.ondrop({domEvent:e, dragObj:o, dropTarget:dropTarget}); } } if(dropTarget.dragOverClassName) APE.dom.removeClass(dropTarget.el, dropTarget.dragOverClassName); break; } } } for(id in draggableList) { o = draggableList[id], x = o.x, y = o.y; if(x < o.minX) o.moveToX(o.minX); else if(x > o.maxX) o.moveToX(o.maxX); if(y < o.minY) o.moveToY(o.minY); else if(y > o.maxY) o.moveToY(o.maxY); if(o.hasBeenDragged) o.dragDone(e); } if(dO.hasBeenDragged) dO.dragDone(e); this.locked = false; this.dO = null; }, /** * Key event callback handler. * When ESC key is pressed, * draggables are released. */ keyPressed : function(e) { e=e||event; if(e.keyCode == 27) { // esc key. if(this.dO) { this.dO.release(e); } } }, toString : function() { return"[object DragHandlers]"; } }; if(typeof window.CollectGarbage == "function") (function(){ // After onunload, add instanceDestructor. // Also add instanceDestructor for DropTarget instances. var drag = APE.drag, Draggable = drag.Draggable, instanceDestructor = Draggable.instanceDestructor; APE.EventPublisher.get(window, "onunload").addAfter(instanceDestructor, Draggable). add(instanceDestructor, drag.DropTarget); })();