diff --git a/.travis.yml b/.travis.yml index 12b1d0b..5d0efb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,19 @@ language: node_js node_js: - - 0.10 - +- 0.1 env: global: - secure: "f2WvNMs20+RQ3u4ZmRgEspL4RGgYVtY3t23aW8j6LdSl9XauFusexNKHYV0juEN3TlIrQS681yZHLsTHeTSXXFwUNycn2ka/H+cuwMx1vf9L/VQq5i6moTaYOTJIBQLUzwnqciLHTaL+HjAGRmdUxz3VTrae9Ub4gezxYxJRbcE=" - + secure: PPm5YZ1XHLUnuUSbiqpaOw4LZd8r/cWtxAozKzyE9NVmIe229Otld1QzECrtSK58JnDOd7MinNuLswcl2MLWcgQJ2Bfh522psnzsotRYul8luymUtBO/a0BsvqryYXp3+dsZoV8grHbOVS3ezPcnrIXwIFATu3xuomBV0YuYntg= install: - # Disable the spinner, it looks bad on Travis - - npm config set spin false - - npm config set loglevel http - - npm install - - npm install -g grunt-cli - - npm install karma-coveralls - - grunt bower - +- npm config set spin false +- npm config set loglevel http +- npm install +- npm install -g grunt-cli +- npm install karma-coveralls +- grunt bower before_script: - - scripts/travis/init_logs.sh - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - +- scripts/travis/init_logs.sh +- export DISPLAY=:99.0 +- sh -e /etc/init.d/xvfb start script: - - grunt test - +- grunt test diff --git a/angularDrop.js b/angularDrop.js index 5e6a26a..6c83fdd 100644 --- a/angularDrop.js +++ b/angularDrop.js @@ -1,6 +1,7 @@ angularDropFiles = { 'angularDropSrc': [ 'src/utils.js', + 'src/dndDOM.js', 'src/draggable.js', 'src/droppable.js', 'src/provider.js', diff --git a/src/dndDOM.js b/src/dndDOM.js new file mode 100644 index 0000000..a1f74d5 --- /dev/null +++ b/src/dndDOM.js @@ -0,0 +1,175 @@ +'use strict'; + +/** + * @ngdoc object + * @module ui.drop + * @name ui.drop.$dndDOM + * @requires $document + * @requires $window + * + * @description + * + * A set of utility methods that can be use to retrieve and manipulate DOM properties. + * + * (based on https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js) + * + */ +var $dndDOMFactory = ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + function contains(a, b) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && contains(adown, bup) ); + }; + + function swapCss(element, css, callback, args) { + var ret, prop, old = {}; + for (prop in css) { + old[prop] = element.style[prop]; + element.style[prop] = css[prop]; + } + + ret = callback.apply(element, args || []); + + for (prop in css) { + element.style[prop] = old[prop]; + } + + return ret; + }; + + var swapDisplay = /^(none|table(?!-c[ea]).+)/; + + var cssShow = { + position: 'absolute', + visibility: 'hidden', + display: 'block' + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + + var doc = element[0] && element[0].ownerDocument; + + if (!doc) { + return; + } + + doc = doc.documentElement; + + if (!contains(doc, element[0])) { + return { top: 0, left: 0 }; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * Partial implementation of https://api.jquery.com/closest/ + * @param element + * @param value + * @returns {angular.element} + */ + closest: function(element, value) { + element = angular.element(element); + if ($.fn && angular.isFunction($.fn.closest)) { + return element.closest(value); + } + // Otherwise, assume it's a tag name... + element = element[0]; + value = value.toLowerCase(); + do { + if (element.nodeName.toLowerCase() === value) { + return angular.element(element); + } + } while (element = element.parentNode); + }, + + size: function(element) { + var jq = angular.element(element); + element = element.nodeName ? element : element[0]; + if (element.offsetWidth === 0 && swapDisplay.test(jq.css('display'))) { + return swapCss(element, cssShow, getHeightAndWidth, [element]); + } + return getHeightAndWidth(element); + + function getHeightAndWidth(element) { + return { + width: element.offsetWidth, + height: element.offsetHeight + }; + } + }, + + keepSize: function(element) { + var css = this.size(element); + css.width = css.width + 'px'; + css.height = css.height + 'px'; + return css; + } + }; +}]; diff --git a/src/draggable.js b/src/draggable.js index 5abd959..ca340fa 100644 --- a/src/draggable.js +++ b/src/draggable.js @@ -60,7 +60,7 @@ var $dragProvider = function() { * a mechanism to drag-enable any arbitrary element, which allows it to be used in * custom directives, so that custom dragging behaviour can be achieved. */ - this.$get = ['$document', '$drop', function($document, $drop) { + this.$get = ['$document', '$drop', '$dndDOM', function($document, $drop, $dndDOM) { var $drag = { /** * @ngdoc method @@ -197,7 +197,7 @@ var $dragProvider = function() { currentDrag = self; self.cssDisplay = self.element.css('display'); - self.dimensions = DOM.size(self.element); + self.dimensions = $dndDOM.size(self.element); if (!self.hanging) { self.cssPosition = self.element.css("position"); @@ -206,7 +206,7 @@ var $dragProvider = function() { width: self.element[0].style['width'], height: self.element[0].style['height'] }; - self.element.css(DOM.keepSize(self.element)); + self.element.css($dndDOM.keepSize(self.element)); } } @@ -214,7 +214,7 @@ var $dragProvider = function() { self.constraints = dragConstraints(self.options.dragWithin, self.element); } - self.offset = self.positionAbs = DOM.offset(self.element); + self.offset = $dndDOM.position(self.element); self.offset.scroll = false; @@ -222,7 +222,7 @@ var $dragProvider = function() { click: { top: event.pageY - self.offset.top, left: event.pageX - self.offset.left - }, + } }); self.lastMouseY = event.pageY; @@ -232,8 +232,8 @@ var $dragProvider = function() { self.element.css({ position: 'absolute', - left: self.offset.left, - top: self.offset.top + left: self.offset.left + 'px', //jqlite does not support raw Number + top: self.offset.top + 'px' }); $document.on("mousemove", self.drag); @@ -354,6 +354,32 @@ var $dragProvider = function() { } }; + function dragConstraints(value, element) { + if (value.length === '') { + return; + } + if (/^(\.|#)/.test(value) && $.fn && angular.isFunction($.fn.closest)) { + // Find nearest element matching class + return constraintsFor(element.parent().closest(value)); + } + if (/^(\^|parent)$/.test(value)) { + return constraintsFor(element.parent()); + } + return constraintsFor($dndDOM.closest(element.parent(), value)); + }; + + function constraintsFor(element) { + if (typeof element === 'undefined' || element.length === 0) { + return; + } + // Use offset + width/height for now? + var constraints = $dndDOM.offset(element), + dimensions = $dndDOM.size(element); + constraints.right = constraints.left + dimensions.width; + constraints.bottom = constraints.top + dimensions.height; + return constraints; + }; + /** * @ngdoc property * @module ui.drop @@ -409,32 +435,6 @@ var $dragProvider = function() { return angular.isObject(thing) && thing.constructor === Draggable; } - function dragConstraints(value, element) { - if (value.length === '') { - return; - } - if (/^(\.|#)/.test(value) && $.fn && angular.isFunction($.fn.closest)) { - // Find nearest element matching class - return constraintsFor(element.parent().closest(value)); - } - if (/^(\^|parent)$/.test(value)) { - return constraintsFor(element.parent()); - } - return constraintsFor(DOM.closest(element.parent(), value)); - } - - function constraintsFor(element) { - if (typeof element === 'undefined' || element.length === 0) { - return; - } - // Use offset + width/height for now? - var constraints = DOM.offset(element), - dimensions = DOM.size(element); - constraints.right = constraints.left + dimensions.width; - constraints.bottom = constraints.top + dimensions.height; - return constraints; - } - function withinConstraints(c, x, y, w, h) { return x >= c.left && (x+w) <= c.right && y >= c.top && (y+h) <= c.bottom; } diff --git a/src/public.js b/src/public.js index 8d324d6..7a571cf 100644 --- a/src/public.js +++ b/src/public.js @@ -10,5 +10,7 @@ function publishExternalAPI() { }]).directive({ draggable: draggableDirective, droppable: droppableDirective + }).factory({ + $dndDOM: $dndDOMFactory }); } diff --git a/src/utils.js b/src/utils.js index 30fd4b8..2dc413b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -111,116 +111,4 @@ matchesSelector = function(node, selector) { } return matchesFn(node, selector); -}, - -DOM = { - nodeEq: function(node, name) { - return (typeof (node.nodeName === 'string' ? - node.nodeName.toLowerCase() : node[0].nodeName).toLowerCase() === name); - }, - - offset: function(node) { - node = node.length ? node[0] : node; - var win, box = { top: 0, left: 0 }, - doc = node && node.ownerDocument; - - if (!doc) { - return; - } - - doc = doc.documentElement; - - if (!DOM.contains(doc, node)) { - return box; - } - - if (angular.isFunction(node.getBoundingClientRect)) { - box = node.getBoundingClientRect(); - } - - win = DOM.window(doc); - - return { - top: box.top + win.pageYOffset - doc.clientTop, - left: box.left + win.pageXOffset - doc.clientLeft - }; - }, - - contains: function(a, b) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && DOM.contains(adown, bup) ); - }, - - window: function(node) { - node = typeof node.nodeName === 'undefined' && typeof node.length === 'number' ? - node[0] : node; - return DOM.isWindow(node) ? node : (node.nodeType === 9 && node.defaultView) || - (node.ownerDocument && (node.ownerDocument.defaultView || - node.ownerDocument.parentWindow)); - }, - - isWindow: function(obj) { - return obj && obj.document && obj.location && obj.alert && obj.setInterval; - }, - - swapCss: function (element, css, callback, args) { - var ret, prop, old = {}; - for (prop in css) { - old[prop] = element.style[prop]; - element.style[prop] = css[prop]; - } - - ret = callback.apply(element, args || []); - - for (prop in css) { - element.style[prop] = old[prop]; - } - - return ret; - }, - - swapDisplay: /^(none|table(?!-c[ea]).+)/, - - cssShow: { - position: 'absolute', - visibility: 'hidden', - display: 'block' - }, - - size: function(node) { - var jq = angular.element(node); - node = node.nodeName ? node : node[0]; - if (node.offsetWidth === 0 && DOM.swapDisplay.test(jq.css('display'))) { - return DOM.swapCss(node, DOM.cssShow, getHeightAndWidth, [node]); - } - return getHeightAndWidth(node); - - function getHeightAndWidth(element) { - return { - width: element.offsetWidth, - height: element.offsetHeight - }; - } - }, - keepSize: function(node) { - var css = DOM.size(node); - css.width = css.width + 'px'; - css.height = css.height + 'px'; - return css; - }, - closest: function(node, value) { - node = angular.element(node); - if ($.fn && angular.isFunction($.fn.closest)) { - return node.closest(value); - } - // Otherwise, assume it's a tag name... - node = node[0]; - value = value.toLowerCase(); - do { - if (node.nodeName.toLowerCase() === value) { - return angular.element(node); - } - } while (node = node.parentNode); - } }; diff --git a/test/provider/drag.spec.js b/test/provider/drag.spec.js index 93b180b..5574a01 100644 --- a/test/provider/drag.spec.js +++ b/test/provider/drag.spec.js @@ -79,5 +79,27 @@ describe('$drag', function() { expect(parseInt(style.left, 10)).toEqual(99); expect(parseInt(style.top, 10)).toEqual(99); }); + + it ('should offset element relative to nearest positioned ancestor', function() { + + parent.css({position:'relative', top:'100px', left:'100px'}); + var draggable = $drag.draggable(element, {keepSize:true}), + style = element.prop('style'); + draggable.dragStart(); + + expect(parseInt(style.left, 10)).toBe(0); + expect(parseInt(style.top, 10)).toBe(0); + }); + + it ('should offset element relative to body if no positioned ancestor', function() { + + parent.css({position:'static', margin:'100px'}); + var draggable = $drag.draggable(element, {keepSize:true}), + style = element.prop('style'); + draggable.dragStart(); + + expect(parseInt(style.left, 10)).toBe(100); + expect(parseInt(style.top, 10)).toBe(100); + }); }); });