diff --git a/README.md b/README.md index e6cd482..31c3140 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,48 @@ -# Draggable.js # -##### Make your dom elements draggable easily. ##### - -### Examples -DOM: - -
-
-
- -To make the whole element draggable: - - var elementToDrag = document.getElementById('elementToDrag'); - draggable(elementToDrag); - -To make it draggable only when dragging the handle element: - - var elementToDrag = document.getElementById('elementToDrag'); - var handle = elementToDrag.getElementsByClassName('handle')[0]; - draggable(elementToDrag, handle); - -#### Notes -* You have to provide the raw element, not the one wrapped by your favorite dom query lib. Using jQuery, for example, you'd need to do something like `var elementToDrag = $('#elementToDrag').get(0);` -* If you are using AMD (e.g. require.js) this lib becomes a module. Otherwise it'll create a global `draggable`. - -### Browser Compatibility -I've ran the tests in Chrome and Firefox. -If you find any incompatibility or want to support other browsers, please do a pull request with the fix! :-) - -### License -This is licensed under the feel-free-to-do-whatever-you-want-to-do license. \ No newline at end of file +# Draggable.js # +##### Make your dom elements draggable easily. ##### + +### Examples +DOM: + +
+
+
+ +To make the whole element draggable: + + var elementToDrag = document.getElementById('elementToDrag'); + draggable(elementToDrag); + +To make it draggable only when dragging the handle element: + + var elementToDrag = document.getElementById('elementToDrag'); + var handle = elementToDrag.getElementsByClassName('handle')[0]; + draggable(elementToDrag, handle); + +### Notes +* You have to provide the raw element, not the one wrapped by your favorite dom query lib. Using jQuery, for example, you'd need to do something like `var elementToDrag = $('#elementToDrag').get(0);` +* If you are using AMD (e.g. require.js) this lib becomes a module. Otherwise it'll create a global `draggable`. + +### Browser Compatibility +* Chrome +* Firefox +* Internet Explorer 7+ (probably 6+, but untested) +* Safari +* Opera + +### To Do +* Add iOS support +* Defer to CSS3 transitions for hardware accelerated animation in modern browsers +* Defer to HTML5 drag and drop API in modern browsers + +### License +This is licensed under the feel-free-to-do-whatever-you-want-to-do license. + +### Changelog + +v2.0 (modified by Boris Cherny) +* Minimized repaints by eliminating style queries on every step of the drag +* Generally improved performance +* Improved browser compatibility +* Standardized event names (start -> dragstart, stop -> dragend) +* Lots of bugfixes (eliminated outline dragging, fixed border/margin/zindex querying) \ No newline at end of file diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..d1fc7ed --- /dev/null +++ b/demo.html @@ -0,0 +1,34 @@ + + + + draggable.js > Demo + + + + + +
+ + + + + \ No newline at end of file diff --git a/draggable.js b/draggable.js index 2886b11..1fab2e8 100644 --- a/draggable.js +++ b/draggable.js @@ -1,144 +1,212 @@ -!(function(moduleName, definition) { - // Whether to expose Draggable as an AMD module or to the global object. - if (typeof define === 'function' && typeof define.amd === 'object') define(definition); - else this[moduleName] = definition(); - -})('draggable', function definition() { - var currentElement; - var fairlyHighZIndex = '10'; - - function draggable(element, handle) { - handle = handle || element; - setPositionType(element); - setDraggableListeners(element); - handle.addEventListener('mousedown', function(event) { - startDragging(event, element); - }); - } - - function setPositionType(element) { - element.style.position = 'absolute'; - } - - function setDraggableListeners(element) { - element.draggableListeners = { - start: [], - drag: [], - stop: [] - }; - element.whenDragStarts = addListener(element, 'start'); - element.whenDragging = addListener(element, 'drag'); - element.whenDragStops = addListener(element, 'stop'); - } - - function startDragging(event, element) { - currentElement && sendToBack(currentElement); - currentElement = bringToFront(element); - - - var initialPosition = getInitialPosition(currentElement); - currentElement.style.left = inPixels(initialPosition.left); - currentElement.style.top = inPixels(initialPosition.top); - currentElement.lastXPosition = event.clientX; - currentElement.lastYPosition = event.clientY; - - var okToGoOn = triggerEvent('start', { x: initialPosition.left, y: initialPosition.top, mouseEvent: event }); - if (!okToGoOn) return; - - addDocumentListeners(); - } - - function addListener(element, type) { - return function(listener) { - element.draggableListeners[type].push(listener); - }; - } - - function triggerEvent(type, args) { - var result = true; - var listeners = currentElement.draggableListeners[type]; - for (var i = listeners.length - 1; i >= 0; i--) { - if (listeners[i](args) === false) result = false; - }; - return result; - } - - function sendToBack(element) { - var decreasedZIndex = fairlyHighZIndex - 1; - element.style['z-index'] = decreasedZIndex; - element.style['zIndex'] = decreasedZIndex; - } - - function bringToFront(element) { - element.style['z-index'] = fairlyHighZIndex; - element.style['zIndex'] = fairlyHighZIndex; - return element; - } - - function addDocumentListeners() { - document.addEventListener('selectstart', cancelDocumentSelection); - document.addEventListener('mousemove', repositionElement); - document.addEventListener('mouseup', removeDocumentListeners); - } - - function getInitialPosition(element) { - var top = 0; - var left = 0; - var currentElement = element; - do { - top += currentElement.offsetTop; - left += currentElement.offsetLeft; - } while (currentElement = currentElement.offsetParent); - - var computedStyle = getComputedStyle? getComputedStyle(element) : false; - if (computedStyle) { - left = left - (parseInt(computedStyle['margin-left']) || 0) - (parseInt(computedStyle['border-left']) || 0); - top = top - (parseInt(computedStyle['margin-top']) || 0) - (parseInt(computedStyle['border-top']) || 0); - } - - return { - top: top, - left: left - }; - } - - function inPixels(value) { - return value + 'px'; - } - - function cancelDocumentSelection(event) { - event.preventDefault && event.preventDefault(); - event.stopPropagation && event.stopPropagation(); - event.returnValue = false; - return false; - } - - function repositionElement(event) { - var style = currentElement.style; - var elementXPosition = parseInt(style.left, 10); - var elementYPosition = parseInt(style.top, 10); - - var elementNewXPosition = elementXPosition + (event.clientX - currentElement.lastXPosition); - var elementNewYPosition = elementYPosition + (event.clientY - currentElement.lastYPosition); - - style.left = inPixels(elementNewXPosition); - style.top = inPixels(elementNewYPosition); - - currentElement.lastXPosition = event.clientX; - currentElement.lastYPosition = event.clientY; - - triggerEvent('drag', { x: elementNewXPosition, y: elementNewYPosition, mouseEvent: event }); - } - - function removeDocumentListeners(event) { - document.removeEventListener('selectstart', cancelDocumentSelection); - document.removeEventListener('mousemove', repositionElement); - document.removeEventListener('mouseup', removeDocumentListeners); - - var left = parseInt(currentElement.style.left, 10); - var top = parseInt(currentElement.style.top, 10); - triggerEvent('stop', { x: left, y: top, mouseEvent: event }); - } - - return draggable; -}); \ No newline at end of file +function Draggable(element, handle, opts) { + + 'use strict'; + + // options + this.options = { + setCursor: true, // change cursor to reflect draggable? + setPosition: true, // change draggable position to absolute? + direction: { // which directions to enable drag in + x: true, // true|false + y: true // true|false + }, + limit: { // limit the drag bounds + x: null, // [minimum position, maximum position] + y: null // [minimum position, maximum position] + }, + onDrag: null, // function(element, X position, Y position, event) + onDragStart: null, // function(element, X position, Y position, event) + onDragEnd: null // function(element, X position, Y position, event) + }; + + var options = this.options; + + // set user-defined options + + for (var opt in opts) { + if (options.hasOwnProperty(opt)) { + options[opt] = opts[opt]; + } + } + + // internal vars + var cursorOffsetX, cursorOffsetY, elementHeight, elementWidth; + var self = this; + var isIE = navigator.appName === 'Microsoft Internet Explorer'; + var hasTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + + // public vars + this.element = handle || element; + this.X = 0; + this.Y = 0; + + this.move = function(x ,y) { + var style = this.element.style; + var direction = options.direction; + var limit = options.limit; + var lowIsOk, highIsOk; + + if (direction.x) { + if (limit.x !== null) { + lowIsOk = x > limit.x[0]; + highIsOk = x + elementWidth <= limit.x[1]; + } + else lowIsOk = highIsOk = 1; + + self.X = x; + style.left = + lowIsOk && highIsOk ? x + 'px' : (!lowIsOk ? 0 : (limit.x[1]-elementWidth) + 'px'); + } + + if (direction.y) { + if (limit.y !== null) { + lowIsOk = y > limit.y[0], + highIsOk = y + elementHeight <= limit.y[1]; + } + else lowIsOk = highIsOk = 1; + + self.Y = y; + style.top = lowIsOk && highIsOk ? y + 'px' : (!lowIsOk ? 0 : (limit.y[1]-elementHeight) + 'px'); + } + }; + + var init = function() { + // set the element + var element = self.element; + if (!element) throw new Error('Invalid element passed to draggable: ' + element); + + // get element dimensions + var compStyle = getStyle(element); + elementHeight = nopx(compStyle.height); + elementWidth = nopx(compStyle.width); + + // optional styling + var style = element.style; + if (options.setPosition) { + style.left = element.offsetLeft + 'px'; + style.top = element.offsetTop + 'px'; + style.right = 'auto'; + style.bottom = 'auto'; + style.position = 'absolute'; + } + if (options.setCursor) style.cursor = 'move'; + + // attach mousedown event + addEvent(element, (hasTouch ? 'touchstart' : 'mousedown'), start); + }; + + var start = function(e) { + // cross-browser event + var ev = e || window.event; + + // prevent browsers from visually dragging the element's outline + stopEvent(ev); + + // set a high z-index, just in case + var element = self.element; + element.oldZindex = element.style.zIndex; + element.style.zIndex = 10000; + + // set initial position + var initialPosition = getInitialPosition(element); + cursorOffsetX = (self.X=initialPosition.x) - (hasTouch ? ev.targetTouches[0] : ev).clientX; + cursorOffsetY = (self.Y=initialPosition.y) - (hasTouch ? ev.targetTouches[0] : ev).clientY; + + // add event listeners + var doc = document; + addEvent(doc, 'selectstart', stopEvent); + if (hasTouch) { + addEvent(doc, 'touchmove', drag); + addEvent(doc, 'touchend', stop); + } + else { + addEvent(doc, 'mousemove', drag); + addEvent(doc, 'mouseup', stop); + } + + // trigger start event + if (options.onDragStart) options.onDragStart(element, self.X, self.Y, ev); + }; + + var drag = function(e) { + // cross-browser event + var ev = e || window.event; + + // compute new coordinates + var x = (hasTouch ? ev.targetTouches[0] : ev).clientX + cursorOffsetX; + var y = (hasTouch ? ev.targetTouches[0] : ev).clientY + cursorOffsetY; + + // move the element + self.move(x, y); + + // trigger drag event + if (options.onDrag) options.onDrag(self.element, x, y, ev); + }; + + var stop = function(e) { + // cross-browser event + var ev = e || window.event; + + // remove event listeners + var doc = document; + removeEvent(doc, 'selectstart', stopEvent); + if (hasTouch) { + removeEvent(doc, 'touchmove', drag); + removeEvent(doc, 'touchend', stop); + } + else { + removeEvent(doc, 'mousemove', drag); + removeEvent(doc, 'mouseup', stop); + } + + // resent element's z-index + self.element.style.zIndex = self.element.oldZindex; + + // trigger dragend event + if (options.onDragEnd) options.onDragEnd(self.element, self.X, self.Y, ev); + }; + + var getInitialPosition = function(element) { + + var top = 0; + var left = 0; + var i = element; + + // compute element offset relative to the window + do { + top += i.offsetTop; + left += i.offsetLeft; + } while (i = i.offsetParent && !getStyle(i.parentNode).position); + + // subtract margin and border widths + var style = getStyle(element); + if (style) { + left -= (nopx(style.marginLeft) || 0) - (nopx(style.borderLeftWidth) || 0); + top -= (nopx(style.marginTop) || 0) - (nopx(style.borderTopWidth) || 0); + } + + return {x: left, y: top}; + }; + + function addEvent(element, e, func) { + return isIE ? element.attachEvent('on'+e, func) : element.addEventListener(e, func, false); + } + + function removeEvent(element, e, func) { + return isIE ? element.detachEvent('on'+e, func) : element.removeEventListener(e, func, false); + } + + function getStyle(element) { + return isIE ? element.currentStyle : getComputedStyle(element); + } + + var nopx = function(string) { + return parseInt(string, 10); + }; + + var stopEvent = function(e) { + isIE ? e.returnValue = false : e.preventDefault(); + }; + + init(); +} \ No newline at end of file diff --git a/draggable.min.js b/draggable.min.js index fd78f8b..da68329 100644 --- a/draggable.min.js +++ b/draggable.min.js @@ -1,4 +1 @@ -!function(h,g){"function"===typeof define&&"object"===typeof define.amd?define(g):this[h]=g()}("draggable",function(){function h(a){a.draggableListeners={start:[],drag:[],stop:[]};a.whenDragStarts=g(a,"start");a.whenDragging=g(a,"drag");a.whenDragStops=g(a,"stop")}function g(a,c){return function(d){a.draggableListeners[c].push(d)}}function i(a,b){for(var d=!0,e=c.draggableListeners[a],f=e.length-1;0<=f;f--)!1===e[f](b)&&(d=!1);return d}function j(a){a.preventDefault&&a.preventDefault();a.stopPropagation&& -a.stopPropagation();return a.returnValue=!1}function k(a){var b=c.style,d=parseInt(b.left,10),e=parseInt(b.top,10),d=d+(a.clientX-c.lastXPosition),e=e+(a.clientY-c.lastYPosition);b.left=d+"px";b.top=e+"px";c.lastXPosition=a.clientX;c.lastYPosition=a.clientY;i("drag",{x:d,y:e,mouseEvent:a})}function l(a){document.removeEventListener("selectstart",j);document.removeEventListener("mousemove",k);document.removeEventListener("mouseup",l);var b=parseInt(c.style.left,10),d=parseInt(c.style.top,10);i("stop", -{x:b,y:d,mouseEvent:a})}var c;return function(a,b){b=b||a;a.style.position="absolute";h(a);b.addEventListener("mousedown",function(d){var e,f;c&&(f=c,f.style["z-index"]=9,f.style.zIndex=9);a.style["z-index"]="10";a.style.zIndex="10";c=a;f=e=0;var b=c;do e+=b.offsetTop,f+=b.offsetLeft;while(b=b.offsetParent);if(b=getComputedStyle?getComputedStyle(c):!1)f=f-(parseInt(b["margin-left"])||0)-(parseInt(b["border-left"])||0),e=e-(parseInt(b["margin-top"])||0)-(parseInt(b["border-top"])||0);c.style.left= -f+"px";c.style.top=e+"px";c.lastXPosition=d.clientX;c.lastYPosition=d.clientY;i("start",{x:f,y:e,mouseEvent:d})&&(document.addEventListener("selectstart",j),document.addEventListener("mousemove",k),document.addEventListener("mouseup",l))})}}); +function Drag(a,u,i){this.options={setCursor:true,setPosition:true,direction:{x:true,y:true},limit:{x:null,y:null},onDrag:null,onDragStart:null,onDragEnd:null};var b=this.options;for(opt in i){if(b.hasOwnProperty(opt)){b[opt]=i[opt]}}var g,d,k,t;var l=this;var m=navigator.appName==="Microsoft Internet Explorer";this.element=u||a;this.X;this.Y;this.move=function(v,D){var z=this.element.style;var B=b.direction;var w=b.limit;if(B.x){if(w.x!==null){var A=v>w.x[0],C=v+t<=w.x[1]}else{var A=C=1}l.X=v;z.left=A&&C?v+"px":(!A?0:(w.x[1]-t)+"px")}if(B.y){if(w.y!==null){var A=D>w.y[0],C=D+k<=w.y[1]}else{var A=C=1}l.Y=D;z.top=A&&C?D+"px":(!A?0:(w.y[1]-k)+"px")}};var p=function(){var w=l.element;if(!w){throw new Error("Invalid element passed to draggable: "+w)}var v=c(w);k=h(v.height);t=h(v.width);if(b.setPosition){var x=w.style;x.left=w.offsetLeft+"px";x.top=w.offsetTop+"px";x.right="auto";x.bottom="auto";x.position="absolute"}if(b.setCursor){x.cursor="move"}o(w,"mousedown",e)};var e=function(y){var y=y||event;s(y);var w=l.element;w.oldZindex=w.style.zIndex;w.style.zIndex=10000;var v=j(w);g=(l.X=v.x)-y.clientX;d=(l.Y=v.y)-y.clientY;var x=document;o(x,"selectstart",s);o(x,"mousemove",r);o(x,"mouseup",n);if(f=b.onDragStart){f(w,l.X,l.Y,y)}};var r=function(w){var w=w||event;var v=w.clientX+g;var z=w.clientY+d;l.move(v,z);if(f=b.onDrag){f(l.element,v,z,w)}};var n=function(w){var w=w||event;var v=document;q(v,"mousemove",r);q(v,"mouseup",n);q(v,"selectstart",s);l.element.style.zIndex=l.element.oldZindex;if(f=b.onDragEnd){f(l.element,l.X,l.Y,w)}};var j=function(w){var y=0;var x=0;var v=w;do{y+=v.offsetTop;x+=v.offsetLeft}while(v=v.offsetParent&&!c(v.parentNode).position);if(style=c(w)){x-=(h(style.marginLeft)||0)-(h(style.borderLeftWidth)||0);y-=(h(style.marginTop)||0)-(h(style.borderTopWidth)||0)}return{x:x,y:y}};function o(v,x,w){return m?v.attachEvent("on"+x,w):v.addEventListener(x,w,false)}function q(v,x,w){return m?v.detachEvent("on"+x,w):v.removeEventListener(x,w,false)}function c(v){return m?v.currentStyle:getComputedStyle(v)}var h=function(v){return parseInt(v,10)};var s=function(v){m?v.returnValue=false:v.preventDefault()};p()}; \ No newline at end of file