From b7dd3db3b173e102292619a0af0d4bf42885d522 Mon Sep 17 00:00:00 2001 From: Dave Ackerman Date: Thu, 26 Mar 2015 15:12:08 -0400 Subject: [PATCH 1/2] Update angular-sortable-view.js --- src/angular-sortable-view.js | 360 ++++++++++++++++++----------------- 1 file changed, 187 insertions(+), 173 deletions(-) diff --git a/src/angular-sortable-view.js b/src/angular-sortable-view.js index 808fe40..1ae4980 100644 --- a/src/angular-sortable-view.js +++ b/src/angular-sortable-view.js @@ -3,21 +3,24 @@ // angular-sortable-view v0.0.13 2015/01/13 // -;(function(window, angular){ +; +(function(window, angular) { 'use strict'; /* jshint eqnull:true */ /* jshint -W041 */ /* jshint -W030 */ var module = angular.module('angular-sortable-view', []); - module.directive('svRoot', [function(){ - function shouldBeAfter(elem, pointer, isGrid){ + module.directive('svRoot', [function() { + function shouldBeAfter(elem, pointer, isGrid) { return isGrid ? elem.x - pointer.x < 0 : elem.y - pointer.y < 0; } - function getSortableElements(key){ + + function getSortableElements(key) { return ROOTS_MAP[key]; } - function removeSortableElements(key){ + + function removeSortableElements(key) { delete ROOTS_MAP[key]; } @@ -27,19 +30,25 @@ return { restrict: 'A', - controller: ['$scope', '$attrs', '$interpolate', '$parse', function($scope, $attrs, $interpolate, $parse){ + controller: ['$scope', '$attrs', '$interpolate', '$parse', function($scope, $attrs, $interpolate, $parse) { var mapKey = $interpolate($attrs.svRoot)($scope) || $scope.$id; - if(!ROOTS_MAP[mapKey]) ROOTS_MAP[mapKey] = []; - - var that = this; - var candidates; // set of possible destinations - var $placeholder;// placeholder element - var options; // sortable options - var $helper; // helper element - the one thats being dragged around with the mouse pointer - var $original; // original element - var $target; // last best candidate - var isGrid = false; - var onSort = $parse($attrs.svOnSort); + if (!ROOTS_MAP[mapKey]) ROOTS_MAP[mapKey] = []; + + var that = this; + var candidates; // set of possible destinations + var $placeholder; // placeholder element + var options; // sortable options + var $helper; // helper element - the one thats being dragged around with the mouse pointer + var $original; // original element + var $target; // last best candidate + var isGrid = false; + var isDisabled = false; + var onSort = $parse($attrs.svOnSort); + + // not sure if this is efficient? + $scope.$watch($parse($attrs.svDisabled), function(newVal, oldVal) { + isDisabled = newVal; + }) // ----- hack due to https://github.com/angular/angular.js/issues/8044 $attrs.svOnStart = $attrs.$$element[0].attributes['sv-on-start']; @@ -52,41 +61,45 @@ var onStart = $parse($attrs.svOnStart); var onStop = $parse($attrs.svOnStop); - this.sortingInProgress = function(){ + + this.sortingInProgress = function() { return sortingInProgress; }; - if($attrs.svGrid){ // sv-grid determined explicite + this.sortingDisabled = function() { + return isDisabled; + }; + + if ($attrs.svGrid) { // sv-grid determined explicite isGrid = $attrs.svGrid === "true" ? true : $attrs.svGrid === "false" ? false : null; - if(isGrid === null) + if (isGrid === null) throw 'Invalid value of sv-grid attribute'; - } - else{ + } else { // check if at least one of the lists have a grid like layout - $scope.$watchCollection(function(){ + $scope.$watchCollection(function() { return getSortableElements(mapKey); - }, function(collection){ + }, function(collection) { isGrid = false; - var array = collection.filter(function(item){ + var array = collection.filter(function(item) { return !item.container; - }).map(function(item){ + }).map(function(item) { return { part: item.getPart().id, y: item.element[0].getBoundingClientRect().top }; }); var dict = Object.create(null); - array.forEach(function(item){ - if(dict[item.part]) + array.forEach(function(item) { + if (dict[item.part]) dict[item.part].push(item.y); else dict[item.part] = [item.y]; }); - Object.keys(dict).forEach(function(key){ + Object.keys(dict).forEach(function(key) { dict[key].sort(); - dict[key].forEach(function(item, index){ - if(index < dict[key].length - 1){ - if(item > 0 && item === dict[key][index + 1]){ + dict[key].forEach(function(item, index) { + if (index < dict[key].length - 1) { + if (item > 0 && item === dict[key][index + 1]) { isGrid = true; } } @@ -95,22 +108,21 @@ }); } - this.$moveUpdate = function(opts, mouse, svElement, svOriginal, svPlaceholder, originatingPart, originatingIndex){ + this.$moveUpdate = function(opts, mouse, svElement, svOriginal, svPlaceholder, originatingPart, originatingIndex) { var svRect = svElement[0].getBoundingClientRect(); - if(opts.tolerance === 'element') + if (opts.tolerance === 'element') mouse = { - x: ~~(svRect.left + svRect.width/2), - y: ~~(svRect.top + svRect.height/2) + x: ~~(svRect.left + svRect.width / 2), + y: ~~(svRect.top + svRect.height / 2) }; sortingInProgress = true; candidates = []; - if(!$placeholder){ - if(svPlaceholder){ // custom placeholder + if (!$placeholder) { + if (svPlaceholder) { // custom placeholder $placeholder = svPlaceholder.clone(); $placeholder.removeClass('ng-hide'); - } - else{ // default placeholder + } else { // default placeholder $placeholder = svOriginal.clone(); $placeholder.addClass('sv-visibility-hidden'); $placeholder.addClass('sv-placeholder'); @@ -139,38 +151,37 @@ // ----- move the element $helper[0].reposition({ - x: mouse.x + document.body.scrollLeft - mouse.offset.x*svRect.width, - y: mouse.y + document.body.scrollTop - mouse.offset.y*svRect.height + x: mouse.x + document.body.scrollLeft - mouse.offset.x * svRect.width, + y: mouse.y + document.body.scrollTop - mouse.offset.y * svRect.height }); // ----- manage candidates - getSortableElements(mapKey).forEach(function(se, index){ - if(opts.containment != null){ + getSortableElements(mapKey).forEach(function(se, index) { + if (opts.containment != null) { // TODO: optimize this since it could be calculated only once when the moving begins - if( - !elementMatchesSelector(se.element, opts.containment) && + if (!elementMatchesSelector(se.element, opts.containment) && !elementMatchesSelector(se.element, opts.containment + ' *') ) return; // element is not within allowed containment } var rect = se.element[0].getBoundingClientRect(); var center = { - x: ~~(rect.left + rect.width/2), - y: ~~(rect.top + rect.height/2) + x: ~~(rect.left + rect.width / 2), + y: ~~(rect.top + rect.height / 2) }; - if(!se.container && // not the container element - (se.element[0].scrollHeight || se.element[0].scrollWidth)){ // element is visible + if (!se.container && // not the container element + (se.element[0].scrollHeight || se.element[0].scrollWidth)) { // element is visible candidates.push({ element: se.element, - q: (center.x - mouse.x)*(center.x - mouse.x) + (center.y - mouse.y)*(center.y - mouse.y), + q: (center.x - mouse.x) * (center.x - mouse.x) + (center.y - mouse.y) * (center.y - mouse.y), view: se.getPart(), targetIndex: se.getIndex(), after: shouldBeAfter(center, mouse, isGrid) }); } - if(se.container && !se.element[0].querySelector('[sv-element]:not(.sv-placeholder):not(.sv-source)')){ // empty container + if (se.container && !se.element[0].querySelector('[sv-element]:not(.sv-placeholder):not(.sv-source)')) { // empty container candidates.push({ element: se.element, - q: (center.x - mouse.x)*(center.x - mouse.x) + (center.y - mouse.y)*(center.y - mouse.y), + q: (center.x - mouse.x) * (center.x - mouse.x) + (center.y - mouse.y) * (center.y - mouse.y), view: se.getPart(), targetIndex: 0, container: true @@ -179,40 +190,38 @@ }); var pRect = $placeholder[0].getBoundingClientRect(); var pCenter = { - x: ~~(pRect.left + pRect.width/2), - y: ~~(pRect.top + pRect.height/2) + x: ~~(pRect.left + pRect.width / 2), + y: ~~(pRect.top + pRect.height / 2) }; candidates.push({ - q: (pCenter.x - mouse.x)*(pCenter.x - mouse.x) + (pCenter.y - mouse.y)*(pCenter.y - mouse.y), + q: (pCenter.x - mouse.x) * (pCenter.x - mouse.x) + (pCenter.y - mouse.y) * (pCenter.y - mouse.y), element: $placeholder, placeholder: true }); - candidates.sort(function(a, b){ + candidates.sort(function(a, b) { return a.q - b.q; }); - candidates.forEach(function(cand, index){ - if(index === 0 && !cand.placeholder && !cand.container){ + candidates.forEach(function(cand, index) { + if (index === 0 && !cand.placeholder && !cand.container) { $target = cand; cand.element.addClass('sv-candidate'); - if(cand.after) + if (cand.after) cand.element.after($placeholder); else insertElementBefore(cand.element, $placeholder); - } - else if(index === 0 && cand.container){ + } else if (index === 0 && cand.container) { $target = cand; cand.element.append($placeholder); - } - else + } else cand.element.removeClass('sv-candidate'); }); }; - this.$drop = function(originatingPart, index, options){ - if(!$placeholder) return; + this.$drop = function(originatingPart, index, options) { + if (!$placeholder) return; - if(options.revert){ + if (options.revert) { var placeholderRect = $placeholder[0].getBoundingClientRect(); var helperRect = $helper[0].getBoundingClientRect(); var distance = Math.sqrt( @@ -220,11 +229,11 @@ Math.pow(helperRect.left - placeholderRect.left, 2) ); - var duration = +options.revert*distance/200; // constant speed: duration depends on distance + var duration = +options.revert * distance / 200; // constant speed: duration depends on distance duration = Math.min(duration, +options.revert); // however it's not longer that options.revert - ['-webkit-', '-moz-', '-ms-', '-o-', ''].forEach(function(prefix){ - if(typeof $helper[0].style[prefix + 'transition'] !== "undefined") + ['-webkit-', '-moz-', '-ms-', '-o-', ''].forEach(function(prefix) { + if (typeof $helper[0].style[prefix + 'transition'] !== "undefined") $helper[0].style[prefix + 'transition'] = 'all ' + duration + 'ms ease'; }); setTimeout(afterRevert, duration); @@ -232,11 +241,10 @@ 'top': placeholderRect.top + document.body.scrollTop + 'px', 'left': placeholderRect.left + document.body.scrollLeft + 'px' }); - } - else + } else afterRevert(); - function afterRevert(){ + function afterRevert() { sortingInProgress = false; $placeholder.remove(); $helper.remove(); @@ -255,18 +263,18 @@ $item: originatingPart.model(originatingPart.scope)[index] }); - if($target){ + if ($target) { $target.element.removeClass('sv-candidate'); var spliced = originatingPart.model(originatingPart.scope).splice(index, 1); var targetIndex = $target.targetIndex; - if($target.view === originatingPart && $target.targetIndex > index) + if ($target.view === originatingPart && $target.targetIndex > index) targetIndex--; - if($target.after) + if ($target.after) targetIndex++; $target.view.model($target.view.scope).splice(targetIndex, 0, spliced[0]); // sv-on-sort callback - if($target.view !== originatingPart || index !== targetIndex) + if ($target.view !== originatingPart || index !== targetIndex) onSort($scope, { $partTo: $target.view.model($target.view.scope), $partFrom: originatingPart.model(originatingPart.scope), @@ -282,15 +290,15 @@ } }; - this.addToSortableElements = function(se){ + this.addToSortableElements = function(se) { getSortableElements(mapKey).push(se); }; - this.removeFromSortableElements = function(se){ + this.removeFromSortableElements = function(se) { var elems = getSortableElements(mapKey); var index = elems.indexOf(se); - if(index > -1){ + if (index > -1) { elems.splice(index, 1); - if(elems.length === 0) + if (elems.length === 0) removeSortableElements(mapKey); } }; @@ -298,24 +306,24 @@ }; }]); - module.directive('svPart', ['$parse', function($parse){ + module.directive('svPart', ['$parse', function($parse) { return { restrict: 'A', require: '^svRoot', - controller: ['$scope', function($scope){ + controller: ['$scope', function($scope) { $scope.$ctrl = this; - this.getPart = function(){ + this.getPart = function() { return $scope.part; }; - this.$drop = function(index, options){ + this.$drop = function(index, options) { $scope.$sortableRoot.$drop($scope.part, index, options); }; }], scope: true, - link: function($scope, $element, $attrs, $sortable){ - if(!$attrs.svPart) throw new Error('no model provided'); + link: function($scope, $element, $attrs, $sortable) { + if (!$attrs.svPart) throw new Error('no model provided'); var model = $parse($attrs.svPart); - if(!model.assign) throw new Error('model not assignable'); + if (!model.assign) throw new Error('model not assignable'); $scope.part = { id: $scope.$id, @@ -331,37 +339,37 @@ container: true }; $sortable.addToSortableElements(sortablePart); - $scope.$on('$destroy', function(){ + $scope.$on('$destroy', function() { $sortable.removeFromSortableElements(sortablePart); }); } }; }]); - module.directive('svElement', ['$parse', function($parse){ + module.directive('svElement', ['$parse', function($parse) { return { restrict: 'A', require: ['^svPart', '^svRoot'], - controller: ['$scope', function($scope){ + controller: ['$scope', function($scope) { $scope.$ctrl = this; }], - link: function($scope, $element, $attrs, $controllers){ + link: function($scope, $element, $attrs, $controllers) { var sortableElement = { element: $element, getPart: $controllers[0].getPart, - getIndex: function(){ + getIndex: function() { return $scope.$index; } }; $controllers[1].addToSortableElements(sortableElement); - $scope.$on('$destroy', function(){ + $scope.$on('$destroy', function() { $controllers[1].removeFromSortableElements(sortableElement); }); var handle = $element; handle.on('mousedown touchstart', onMousedown); - $scope.$watch('$ctrl.handle', function(customHandle){ - if(customHandle){ + $scope.$watch('$ctrl.handle', function(customHandle) { + if (customHandle) { handle.off('mousedown touchstart', onMousedown); handle = customHandle; handle.on('mousedown touchstart', onMousedown); @@ -369,15 +377,15 @@ }); var helper; - $scope.$watch('$ctrl.helper', function(customHelper){ - if(customHelper){ + $scope.$watch('$ctrl.helper', function(customHelper) { + if (customHelper) { helper = customHelper; } }); var placeholder; - $scope.$watch('$ctrl.placeholder', function(customPlaceholder){ - if(customPlaceholder){ + $scope.$watch('$ctrl.placeholder', function(customPlaceholder) { + if (customPlaceholder) { placeholder = customPlaceholder; } }); @@ -387,11 +395,15 @@ var moveExecuted; - function onMousedown(e){ + function onMousedown(e) { touchFix(e); - if($controllers[1].sortingInProgress()) return; - if(e.button != 0 && e.type === 'mousedown') return; + if ($controllers[1].sortingInProgress()) return; + + // do nothing if sorting is disabled + if ($controllers[1].sortingDisabled()) return; + + if (e.button != 0 && e.type === 'mousedown') return; moveExecuted = false; var opts = $parse($attrs.svElement)($scope); @@ -400,7 +412,7 @@ revert: 200, containment: 'html' }, opts); - if(opts.containment){ + if (opts.containment) { var containmentRect = closestElement.call($element, opts.containment)[0].getBoundingClientRect(); } @@ -408,9 +420,9 @@ var clientRect = $element[0].getBoundingClientRect(); var clone; - if(!helper) helper = $controllers[0].helper; - if(!placeholder) placeholder = $controllers[0].placeholder; - if(helper){ + if (!helper) helper = $controllers[0].helper; + if (!placeholder) placeholder = $controllers[0].placeholder; + if (helper) { clone = helper.clone(); clone.removeClass('ng-hide'); clone.css({ @@ -418,8 +430,7 @@ 'top': clientRect.top + document.body.scrollTop + 'px' }); target.addClass('sv-visibility-hidden'); - } - else{ + } else { clone = target.clone(); clone.addClass('sv-helper').css({ 'left': clientRect.left + document.body.scrollLeft + 'px', @@ -428,21 +439,21 @@ }); } - clone[0].reposition = function(coords){ + clone[0].reposition = function(coords) { var targetLeft = coords.x; var targetTop = coords.y; var helperRect = clone[0].getBoundingClientRect(); var body = document.body; - if(containmentRect){ - if(targetTop < containmentRect.top + body.scrollTop) // top boundary + if (containmentRect) { + if (targetTop < containmentRect.top + body.scrollTop) // top boundary targetTop = containmentRect.top + body.scrollTop; - if(targetTop + helperRect.height > containmentRect.top + body.scrollTop + containmentRect.height) // bottom boundary + if (targetTop + helperRect.height > containmentRect.top + body.scrollTop + containmentRect.height) // bottom boundary targetTop = containmentRect.top + body.scrollTop + containmentRect.height - helperRect.height; - if(targetLeft < containmentRect.left + body.scrollLeft) // left boundary + if (targetLeft < containmentRect.left + body.scrollLeft) // left boundary targetLeft = containmentRect.left + body.scrollLeft; - if(targetLeft + helperRect.width > containmentRect.left + body.scrollLeft + containmentRect.width) // right boundary + if (targetLeft + helperRect.width > containmentRect.left + body.scrollLeft + containmentRect.width) // right boundary targetLeft = containmentRect.left + body.scrollLeft + containmentRect.width - helperRect.width; } this.style.left = targetLeft - body.scrollLeft + 'px'; @@ -450,69 +461,73 @@ }; var pointerOffset = { - x: (e.clientX - clientRect.left)/clientRect.width, - y: (e.clientY - clientRect.top)/clientRect.height + x: (e.clientX - clientRect.left) / clientRect.width, + y: (e.clientY - clientRect.top) / clientRect.height }; html.addClass('sv-sorting-in-progress'); - html.on('mousemove touchmove', onMousemove).on('mouseup touchend touchcancel', function mouseup(e){ + html.on('mousemove touchmove', onMousemove).on('mouseup touchend touchcancel', function mouseup(e) { html.off('mousemove touchmove', onMousemove); html.off('mouseup touchend', mouseup); html.removeClass('sv-sorting-in-progress'); - if(moveExecuted) + if (moveExecuted) $controllers[0].$drop($scope.$index, opts); else $element.removeClass('sv-visibility-hidden'); }); // onMousemove(e); - function onMousemove(e){ + function onMousemove(e) { + touchFix(e); - if(!moveExecuted){ + if (!moveExecuted) { $element.parent().prepend(clone); moveExecuted = true; } + $controllers[1].$moveUpdate(opts, { x: e.clientX, y: e.clientY, offset: pointerOffset }, clone, $element, placeholder, $controllers[0].getPart(), $scope.$index); + + } } } }; }]); - module.directive('svHandle', function(){ + module.directive('svHandle', function() { return { require: '?^svElement', - link: function($scope, $element, $attrs, $ctrl){ - if($ctrl) + link: function($scope, $element, $attrs, $ctrl) { + if ($ctrl) $ctrl.handle = $element.add($ctrl.handle); // support multiple handles } }; }); - module.directive('svHelper', function(){ + module.directive('svHelper', function() { return { require: ['?^svPart', '?^svElement'], - link: function($scope, $element, $attrs, $ctrl){ + link: function($scope, $element, $attrs, $ctrl) { $element.addClass('sv-helper').addClass('ng-hide'); - if($ctrl[1]) + if ($ctrl[1]) $ctrl[1].helper = $element; - else if($ctrl[0]) + else if ($ctrl[0]) $ctrl[0].helper = $element; } }; }); - module.directive('svPlaceholder', function(){ + module.directive('svPlaceholder', function() { return { require: ['?^svPart', '?^svElement'], - link: function($scope, $element, $attrs, $ctrl){ + link: function($scope, $element, $attrs, $ctrl) { $element.addClass('sv-placeholder').addClass('ng-hide'); - if($ctrl[1]) + if ($ctrl[1]) $ctrl[1].placeholder = $element; - else if($ctrl[0]) + else if ($ctrl[0]) $ctrl[0].placeholder = $element; } }; @@ -521,32 +536,32 @@ angular.element(document.head).append([ '' ].join('')); - function touchFix(e){ - if(!('clientX' in e) && !('clientY' in e)) { + function touchFix(e) { + if (!('clientX' in e) && !('clientY' in e)) { var touches = e.touches || e.originalEvent.touches; - if(touches && touches.length) { + if (touches && touches.length) { e.clientX = touches[0].clientX; e.clientY = touches[0].clientY; } @@ -554,52 +569,51 @@ } } - function getPreviousSibling(element){ + function getPreviousSibling(element) { element = element[0]; - if(element.previousElementSibling) + if (element.previousElementSibling) return angular.element(element.previousElementSibling); - else{ + else { var sib = element.previousSibling; - while(sib != null && sib.nodeType != 1) + while (sib != null && sib.nodeType != 1) sib = sib.previousSibling; return angular.element(sib); } } - function insertElementBefore(element, newElement){ + function insertElementBefore(element, newElement) { var prevSibl = getPreviousSibling(element); - if(prevSibl.length > 0){ + if (prevSibl.length > 0) { prevSibl.after(newElement); - } - else{ + } else { element.parent().prepend(newElement); } } var dde = document.documentElement, - matchingFunction = dde.matches ? 'matches' : - dde.matchesSelector ? 'matchesSelector' : - dde.webkitMatches ? 'webkitMatches' : - dde.webkitMatchesSelector ? 'webkitMatchesSelector' : - dde.msMatches ? 'msMatches' : - dde.msMatchesSelector ? 'msMatchesSelector' : - dde.mozMatches ? 'mozMatches' : - dde.mozMatchesSelector ? 'mozMatchesSelector' : null; - if(matchingFunction == null) + matchingFunction = dde.matches ? 'matches' : + dde.matchesSelector ? 'matchesSelector' : + dde.webkitMatches ? 'webkitMatches' : + dde.webkitMatchesSelector ? 'webkitMatchesSelector' : + dde.msMatches ? 'msMatches' : + dde.msMatchesSelector ? 'msMatchesSelector' : + dde.mozMatches ? 'mozMatches' : + dde.mozMatchesSelector ? 'mozMatchesSelector' : null; + if (matchingFunction == null) throw 'This browser doesn\'t support the HTMLElement.matches method'; - function elementMatchesSelector(element, selector){ - if(element instanceof angular.element) element = element[0]; - if(matchingFunction !== null) + function elementMatchesSelector(element, selector) { + if (element instanceof angular.element) element = element[0]; + if (matchingFunction !== null) return element[matchingFunction](selector); } - var closestElement = angular.element.prototype.closest || function (selector){ + var closestElement = angular.element.prototype.closest || function(selector) { var el = this[0].parentNode; - while(el !== document.documentElement && !el[matchingFunction](selector)) + while (el !== document.documentElement && !el[matchingFunction](selector)) el = el.parentNode; - if(el[matchingFunction](selector)) + if (el[matchingFunction](selector)) return angular.element(el); else return angular.element(); @@ -608,18 +622,18 @@ /* Simple implementation of jQuery's .add method */ - if(typeof angular.element.prototype.add !== 'function'){ - angular.element.prototype.add = function(elem){ + if (typeof angular.element.prototype.add !== 'function') { + angular.element.prototype.add = function(elem) { var i, res = angular.element(); elem = angular.element(elem); - for(i=0;i Date: Thu, 26 Mar 2015 15:20:53 -0400 Subject: [PATCH 2/2] bump package.json --- package.json | 2 +- src/angular-sortable-view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7e9281d..10dfd83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-sortable-view", - "version": "0.0.13", + "version": "0.0.14", "description": "Fully declarative (multi)sortable for AngularJS", "homepage": "http://kamilkp.github.io/angular-sortable-view", "author": { diff --git a/src/angular-sortable-view.js b/src/angular-sortable-view.js index 1ae4980..a95752e 100644 --- a/src/angular-sortable-view.js +++ b/src/angular-sortable-view.js @@ -1,6 +1,6 @@ // // Copyright Kamil Pękala http://github.com/kamilkp -// angular-sortable-view v0.0.13 2015/01/13 +// angular-sortable-view v0.0.14 2015/01/13 // ;