From b1ef886e4f228d4e73ae18fa086cbc15fc22ef00 Mon Sep 17 00:00:00 2001 From: Jeffrey Patterson Date: Tue, 20 Sep 2016 16:56:05 -0400 Subject: [PATCH] support for positioned parents --- bower.json | 2 +- dist/sortable.js | 571 ++++++++++++++++++++++--------------------- dist/sortable.min.js | 2 +- package.json | 2 +- src/sortable.js | 12 +- 5 files changed, 304 insertions(+), 285 deletions(-) diff --git a/bower.json b/bower.json index c5cfa81..583852c 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "blvd-sortable", - "version": "0.0.3", + "version": "0.0.4", "description": "Drag to reorder elements.", "license": "MIT", "authors": [ diff --git a/dist/sortable.js b/dist/sortable.js index 31d2bb3..36f3b0c 100644 --- a/dist/sortable.js +++ b/dist/sortable.js @@ -1,281 +1,290 @@ -/* - * Sortable - * https://github.com/Boulevard/sortable.git - * @license MIT - * v0.0.3 - */ - -(function (window, angular, undefined) { -'use strict'; - -var START_EVENT = 'ontouchstart' in document.documentElement ? 'touchstart' : 'mousedown'; -var MOVE_EVENT = 'ontouchmove' in document.documentElement ? 'touchmove' : 'mousemove'; -var END_EVENT = 'ontouchend' in document.documentElement ? 'touchend' : 'mouseup'; - -function Sortable($attrs, $element, $scope) { - var self = this; - - /* - * dropzone [string] - By default, an empty element will be created - * with the same tag name as the element being - * dragged. - * - * sameSize [bool] - Make the dropzone the same size as the element - * being dragged. - * - * threshold [float] - The minimum distance the cursor must travel to - * initiate a drag event. - */ - var options = { - dropzone: undefined, - sameSize: true, - threshold: 2 - }; - - var dropzone; - var selected; - var position = {}; - - if(self.options) { - angular.extend(options, self.options); - } - - if($attrs.sortable === '') { - self.sortable = true; - } - - function childCount() { - return $element.children().length; - } - - function createDropZone(element) { - if(angular.isString(options.dropzone)) { - dropzone = angular.element(options.dropzone); - } else { - dropzone = angular.element('<' + element.prop('localName') + '>'); - } - - dropzone.addClass('drop-zone'); - - if(options.sameSize) { - dropzone.css({ - height: element.prop('clientHeight') + 'px', - width: element.prop('clientWidth') + 'px' - }); - } - - element.after(dropzone); - } - - function floatElement(element, children) { - for(var i = 0; i < children.length; i++) { - var child = children.eq(i); - var childComputedStyle = child.data('sortable.computedStyle'); - - child.css({ - height: childComputedStyle.height, - width: childComputedStyle.width - }); - } - - var bounds = element[0].getBoundingClientRect(); - var computedStyle = element.data('sortable.computedStyle'); - - element.addClass('dragging').css({ - height: computedStyle.height, - left: bounds.left - computedStyle.marginLeft.slice(0, -2) + 'px', - pointerEvents: 'none', - position: 'absolute', - top: bounds.top - computedStyle.marginTop.slice(0, -2) + 'px', - width: computedStyle.width, - zIndex: 100 - }); - } - - function getChildFromPoint(point) { - var children = $element.children(); - var hovering = document.elementFromPoint(point.x, point.y); - - for(var i = 0; i < children.length; i++) { - if(children[i] === hovering || children[i].contains(hovering)) { - return children[i]; - } - } - } - - function indexOf(child) { - return Array.prototype.indexOf.call($element.children(), child[0]); - } - - function magnitude(vector) { - return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2)); - } - - function mouseDown(event) { - event.preventDefault(); - event.stopPropagation(); - - selected = angular.element(event.currentTarget); - - position.x = event.pageX; - position.y = event.pageY; - - document.addEventListener(MOVE_EVENT, mouseMove); - document.addEventListener(END_EVENT, mouseUp); - } - - function mouseMove(event) { - event.preventDefault(); - - var delta = { - x: event.pageX - position.x, - y: event.pageY - position.y - }; - - if(magnitude(delta) < options.threshold) { - return; - } - - if(!selected.hasClass('dragging')) { - onDragStart(selected, selected.children()); - } - - onDragMove(event, selected, delta); - } - - function mouseUp(event) { - document.removeEventListener(MOVE_EVENT, mouseMove); - document.removeEventListener(END_EVENT, mouseUp); - - if(selected.hasClass('dragging')) { - onDragEnd(event, selected, selected.children()); - } - } - - function onDragEnd(event, child, grandChildren) { - var oldIndex = child.data('sortable.index'); - var newIndex = indexOf(dropzone); - - // because the element being dragged is never removed - if(newIndex > oldIndex) { - newIndex--; - } - - restoreStyle(child.removeClass('dragging')); - - for(var i = 0; i < grandChildren.length; i++) { - restoreStyle(grandChildren.eq(i)); - } - - if(angular.isArray(self.sequence)) { - dropzone.remove(); - - $scope.$apply(function () { - self.sequence.splice(newIndex, 0, self.sequence.splice(oldIndex, 1)[0]); - }); - } else { - dropzone.replaceWith(child); - } - - if(angular.isFunction(self.dragEnd)) { - self.dragEnd(child, newIndex, oldIndex, self.sequence); - } - } - - function onDragMove(event, child, delta) { - child.css('transform', 'translate(' + delta.x + 'px, ' + delta.y+ 'px)'); - - var point = {x: event.pageX, y: event.pageY}; - var sibling = getChildFromPoint(point); - - if(sibling && sibling !== dropzone[0]) { - updateDropZone(point, sibling); - } - - if(angular.isFunction(self.dragMove)) { - self.dragMove(child); - } - } - - function onDragStart(child, grandChildren) { - remeberStyle(child); - - for(var i = 0; i < grandChildren.length; i++) { - remeberStyle(grandChildren.eq(i)); - } - - floatElement(child, grandChildren); - createDropZone(child.data('sortable.index', indexOf(child))); - - if(angular.isFunction(self.dragStart)) { - self.dragStart(child); - } - } - - function remeberStyle(element) { - element.data('sortable.style', element.attr('style') || ''); - - if(!element.data('sortable.computedStyle')) { - element.data('sortable.computedStyle', window.getComputedStyle(element[0])); - } - } - - function restoreStyle(element) { - element.attr('style', element.data('sortable.style')); - } - - function updateDropZone(cursor, sibling) { - var offsetY = cursor.y - sibling.getBoundingClientRect().top; - var midPoint = sibling.clientHeight >> 1; - - // if the cursor is past the midpoint of the element, insert - // the dropzone after the element - if(offsetY > midPoint) { - if(sibling.nextElementSibling !== dropzone[0]) { - $element[0].insertBefore(dropzone[0], sibling.nextElementSibling); - } - } else if(sibling.previousElementSibling !== dropzone[0]) { - $element[0].insertBefore(dropzone[0], sibling); - } - } - - $scope.$watch(childCount, function (childCount) { - if(childCount) { - - var children = $element.children(); - - if(self.sortable) { - children.on(START_EVENT, mouseDown); - } - } - }); - - $scope.$watch('$sortable.sortable', function (sortable) { - var children = $element.children(); - - if(sortable) { - children.on(START_EVENT, mouseDown); - } else { - children.off(START_EVENT, mouseDown); - } - }); -} - -Sortable.$inject = ['$attrs', '$element', '$scope']; - -angular.module('sortable', []).directive('sortable', function () { - return { - controller: Sortable, - controllerAs: '$sortable', - bindToController: { - options: '=?', - dragEnd: '=?', - dragMove: '=?', - dragStart: '=?', - sequence: '=?ngModel', - sortable: '=?' - } - }; -}); - -})(window, angular); \ No newline at end of file +/* + * Sortable + * https://github.com/Boulevard/sortable.git + * @license MIT + * v0.0.4 + */ + +(function (window, angular, undefined) { +'use strict'; + +var START_EVENT = 'ontouchstart' in document.documentElement ? 'touchstart' : 'mousedown'; +var MOVE_EVENT = 'ontouchmove' in document.documentElement ? 'touchmove' : 'mousemove'; +var END_EVENT = 'ontouchend' in document.documentElement ? 'touchend' : 'mouseup'; + +function Sortable($attrs, $element, $scope) { + var self = this; + + /* + * dropzone [string] - By default, an empty element will be created + * with the same tag name as the element being + * dragged. + * + * sameSize [bool] - Make the dropzone the same size as the element + * being dragged. + * + * threshold [float] - The minimum distance the cursor must travel to + * initiate a drag event. + */ + var options = { + dropzone: undefined, + sameSize: true, + threshold: 2 + }; + + var dropzone; + var selected; + var position = {}; + + if(self.options) { + angular.extend(options, self.options); + } + + if($attrs.sortable === '') { + self.sortable = true; + } + + function childCount() { + return $element.children().length; + } + + function createDropZone(element) { + if(angular.isString(options.dropzone)) { + dropzone = angular.element(options.dropzone); + } else { + dropzone = angular.element('<' + element.prop('localName') + '>'); + } + + dropzone.addClass('drop-zone'); + + if(options.sameSize) { + dropzone.css({ + height: element.prop('clientHeight') + 'px', + width: element.prop('clientWidth') + 'px' + }); + } + + element.after(dropzone); + } + + function floatElement(element, children) { + for(var i = 0; i < children.length; i++) { + var child = children.eq(i); + var childComputedStyle = child.data('sortable.computedStyle'); + + child.css({ + height: childComputedStyle.height, + width: childComputedStyle.width + }); + } + + var el = element[0]; + var bounds = { + left: el.offsetLeft, + top: el.offsetTop, + } + var offsetParent = el.offsetParent; // find true positioned parent, as offsetParent may be an unpositioned table + while (offsetParent && window.getComputedStyle(offsetParent).position === 'static') { + bounds.left += offsetParent.offsetLeft; + bounds.top += offsetParent.offsetTop; + offsetParent = offsetParent.offsetParent; + } var computedStyle = element.data('sortable.computedStyle'); + + element.addClass('dragging').css({ + height: computedStyle.height, + left: bounds.left - computedStyle.marginLeft.slice(0, -2) + 'px', + pointerEvents: 'none', + position: 'absolute', + top: bounds.top - computedStyle.marginTop.slice(0, -2) + 'px', + width: computedStyle.width, + zIndex: 100 + }); + } + + function getChildFromPoint(point) { + var children = $element.children(); + var hovering = document.elementFromPoint(point.x, point.y); + + for(var i = 0; i < children.length; i++) { + if(children[i] === hovering || children[i].contains(hovering)) { + return children[i]; + } + } + } + + function indexOf(child) { + return Array.prototype.indexOf.call($element.children(), child[0]); + } + + function magnitude(vector) { + return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2)); + } + + function mouseDown(event) { + event.preventDefault(); + event.stopPropagation(); + + selected = angular.element(event.currentTarget); + + position.x = event.pageX; + position.y = event.pageY; + + document.addEventListener(MOVE_EVENT, mouseMove); + document.addEventListener(END_EVENT, mouseUp); + } + + function mouseMove(event) { + event.preventDefault(); + + var delta = { + x: event.pageX - position.x, + y: event.pageY - position.y + }; + + if(magnitude(delta) < options.threshold) { + return; + } + + if(!selected.hasClass('dragging')) { + onDragStart(selected, selected.children()); + } + + onDragMove(event, selected, delta); + } + + function mouseUp(event) { + document.removeEventListener(MOVE_EVENT, mouseMove); + document.removeEventListener(END_EVENT, mouseUp); + + if(selected.hasClass('dragging')) { + onDragEnd(event, selected, selected.children()); + } + } + + function onDragEnd(event, child, grandChildren) { + var oldIndex = child.data('sortable.index'); + var newIndex = indexOf(dropzone); + + // because the element being dragged is never removed + if(newIndex > oldIndex) { + newIndex--; + } + + restoreStyle(child.removeClass('dragging')); + + for(var i = 0; i < grandChildren.length; i++) { + restoreStyle(grandChildren.eq(i)); + } + + if(angular.isArray(self.sequence)) { + dropzone.remove(); + + $scope.$apply(function () { + self.sequence.splice(newIndex, 0, self.sequence.splice(oldIndex, 1)[0]); + }); + } else { + dropzone.replaceWith(child); + } + + if(angular.isFunction(self.dragEnd)) { + self.dragEnd(child, newIndex, oldIndex, self.sequence); + } + } + + function onDragMove(event, child, delta) { + child.css('transform', 'translate(' + delta.x + 'px, ' + delta.y+ 'px)'); + + var point = {x: event.pageX, y: event.pageY}; + var sibling = getChildFromPoint(point); + + if(sibling && sibling !== dropzone[0]) { + updateDropZone(point, sibling); + } + + if(angular.isFunction(self.dragMove)) { + self.dragMove(child); + } + } + + function onDragStart(child, grandChildren) { + remeberStyle(child); + + for(var i = 0; i < grandChildren.length; i++) { + remeberStyle(grandChildren.eq(i)); + } + + floatElement(child, grandChildren); + createDropZone(child.data('sortable.index', indexOf(child))); + + if(angular.isFunction(self.dragStart)) { + self.dragStart(child); + } + } + + function remeberStyle(element) { + element.data('sortable.style', element.attr('style') || ''); + + if(!element.data('sortable.computedStyle')) { + element.data('sortable.computedStyle', window.getComputedStyle(element[0])); + } + } + + function restoreStyle(element) { + element.attr('style', element.data('sortable.style')); + } + + function updateDropZone(cursor, sibling) { + var offsetY = cursor.y - sibling.getBoundingClientRect().top; + var midPoint = sibling.clientHeight >> 1; + + // if the cursor is past the midpoint of the element, insert + // the dropzone after the element + if(offsetY > midPoint) { + if(sibling.nextElementSibling !== dropzone[0]) { + $element[0].insertBefore(dropzone[0], sibling.nextElementSibling); + } + } else if(sibling.previousElementSibling !== dropzone[0]) { + $element[0].insertBefore(dropzone[0], sibling); + } + } + + $scope.$watch(childCount, function (childCount) { + if(childCount) { + + var children = $element.children(); + + if(self.sortable) { + children.on(START_EVENT, mouseDown); + } + } + }); + + $scope.$watch('$sortable.sortable', function (sortable) { + var children = $element.children(); + + if(sortable) { + children.on(START_EVENT, mouseDown); + } else { + children.off(START_EVENT, mouseDown); + } + }); +} + +Sortable.$inject = ['$attrs', '$element', '$scope']; + +angular.module('sortable', []).directive('sortable', function () { + return { + controller: Sortable, + controllerAs: '$sortable', + bindToController: { + options: '=?', + dragEnd: '=?', + dragMove: '=?', + dragStart: '=?', + sequence: '=?ngModel', + sortable: '=?' + } + }; +}); + +})(window, angular); \ No newline at end of file diff --git a/dist/sortable.min.js b/dist/sortable.min.js index dafdb13..7ab0e52 100644 --- a/dist/sortable.min.js +++ b/dist/sortable.min.js @@ -1 +1 @@ -!function(e,t,n){"use strict";function o(o,l,s){function c(){return l.children().length}function d(e){q=t.isString(z.dropzone)?t.element(z.dropzone):t.element("<"+e.prop("localName")+">"),q.addClass("drop-zone"),z.sameSize&&q.css({height:e.prop("clientHeight")+"px",width:e.prop("clientWidth")+"px"}),e.after(q)}function u(e,t){for(var n=0;nr&&a--,S(n.removeClass("dragging"));for(var i=0;i>1;n>o?t.nextElementSibling!==q[0]&&l[0].insertBefore(q[0],t.nextElementSibling):t.previousElementSibling!==q[0]&&l[0].insertBefore(q[0],t)}var q,C,$=this,z={dropzone:n,sameSize:!0,threshold:2},M={};$.options&&t.extend(z,$.options),""===o.sortable&&($.sortable=!0),s.$watch(c,function(e){if(e){var t=l.children();$.sortable&&t.on(r,m)}}),s.$watch("$sortable.sortable",function(e){var t=l.children();e?t.on(r,m):t.off(r,m)})}var r="ontouchstart"in document.documentElement?"touchstart":"mousedown",a="ontouchmove"in document.documentElement?"touchmove":"mousemove",i="ontouchend"in document.documentElement?"touchend":"mouseup";o.$inject=["$attrs","$element","$scope"],t.module("sortable",[]).directive("sortable",function(){return{controller:o,controllerAs:"$sortable",bindToController:{options:"=?",dragEnd:"=?",dragMove:"=?",dragStart:"=?",sequence:"=?ngModel",sortable:"=?"}}})}(window,angular); \ No newline at end of file +!function(e,t,n){"use strict";function o(o,s,l){function c(){return s.children().length}function d(e){q=t.isString(z.dropzone)?t.element(z.dropzone):t.element("<"+e.prop("localName")+">"),q.addClass("drop-zone"),z.sameSize&&q.css({height:e.prop("clientHeight")+"px",width:e.prop("clientWidth")+"px"}),e.after(q)}function u(t,n){for(var o=0;or&&a--,E(n.removeClass("dragging"));for(var i=0;i>1;n>o?t.nextElementSibling!==q[0]&&s[0].insertBefore(q[0],t.nextElementSibling):t.previousElementSibling!==q[0]&&s[0].insertBefore(q[0],t)}var q,C,$=this,z={dropzone:n,sameSize:!0,threshold:2},L={};$.options&&t.extend(z,$.options),""===o.sortable&&($.sortable=!0),l.$watch(c,function(e){if(e){var t=s.children();$.sortable&&t.on(r,h)}}),l.$watch("$sortable.sortable",function(e){var t=s.children();e?t.on(r,h):t.off(r,h)})}var r="ontouchstart"in document.documentElement?"touchstart":"mousedown",a="ontouchmove"in document.documentElement?"touchmove":"mousemove",i="ontouchend"in document.documentElement?"touchend":"mouseup";o.$inject=["$attrs","$element","$scope"],t.module("sortable",[]).directive("sortable",function(){return{controller:o,controllerAs:"$sortable",bindToController:{options:"=?",dragEnd:"=?",dragMove:"=?",dragStart:"=?",sequence:"=?ngModel",sortable:"=?"}}})}(window,angular); \ No newline at end of file diff --git a/package.json b/package.json index 874e2d6..cf6b70d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blvd-sortable", - "version": "0.0.3", + "version": "0.0.4", "description": "Drag to reorder elements.", "license": "MIT", "author": { diff --git a/src/sortable.js b/src/sortable.js index 8372097..6b247ae 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -70,7 +70,17 @@ function Sortable($attrs, $element, $scope) { }); } - var bounds = element[0].getBoundingClientRect(); + var el = element[0]; + var bounds = { + left: el.offsetLeft, + top: el.offsetTop, + } + var offsetParent = el.offsetParent; // find true positioned parent, as offsetParent may be an unpositioned table + while (offsetParent && window.getComputedStyle(offsetParent).position === 'static') { + bounds.left += offsetParent.offsetLeft; + bounds.top += offsetParent.offsetTop; + offsetParent = offsetParent.offsetParent; + } var computedStyle = element.data('sortable.computedStyle'); element.addClass('dragging').css({