├── .gitignore ├── demo ├── images │ └── GitHub-Mark-32px.png ├── demo.coffee ├── styles │ ├── style.scss │ └── reset.scss └── index.html ├── .editorconfig ├── bower.json ├── package.json ├── LICENSE ├── dist ├── angular-drag-to-reorder.min.js └── angular-drag-to-reorder.js ├── README.md ├── gulpfile.js └── src └── angular-drag-to-reorder.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | build/ 4 | gh-pages/ 5 | -------------------------------------------------------------------------------- /demo/images/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandly/angular-drag-to-reorder/HEAD/demo/images/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [**.sass] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /demo/demo.coffee: -------------------------------------------------------------------------------- 1 | demo = angular.module 'demo', ['mb-dragToReorder'] 2 | 3 | demo.controller 'BasicListCtrl', ($scope) -> 4 | $scope.planets = [ 5 | 'Mercury', 'Venus', 'Earth', 'Mars', 6 | 'Jupiter', 'Saturn', 'Uranus', 'Neptune' 7 | ] 8 | 9 | $scope.$on 'dragToReorder.reordered', ($event, reordered) -> 10 | console.log "Moved #{reordered.item} from #{reordered.from} to #{reordered.to}" 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-drag-to-reorder", 3 | "version": "0.0.2", 4 | "description": "Drag items in an ng-repeat to reorder them", 5 | "keywords": [ 6 | "angular", 7 | "drag", 8 | "reorder", 9 | "ng-repeat", 10 | "directive" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/brandly/angular-drag-to-reorder", 14 | "main": "dist/angular-drag-to-reorder.js" 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-drag-to-reorder", 3 | "description": "Drag items in an ng-repeat to reorder them", 4 | "author": "Matthew Brandly", 5 | "version": "0.0.2", 6 | "homepage": "http://brandly.github.io/angular-drag-to-reorder/", 7 | "private": true, 8 | "devDependencies": { 9 | "gulp": "~3.5.1", 10 | "gulp-concat": "^2.1.7", 11 | "gulp-htmlbuild": "^0.1.1", 12 | "gulp-util": "~2.2.14", 13 | "gulp-coffee": "~1.4.1", 14 | "gulp-sass": "^0.6.0", 15 | "gulp-uglify": "~0.2.0", 16 | "gulp-minify-css": "~0.3.0", 17 | "express": "^3.4.8", 18 | "path": "~0.4.9", 19 | "gulp-rename": "^1.2.0", 20 | "gulp-header": "^1.2.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Matthew Brandly. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /demo/styles/style.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | *, 3 | *:after, 4 | *:before { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 10 | text-align: center; 11 | color: #333; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | line-height: 40px; 17 | } 18 | 19 | .content { 20 | margin: 40px auto; 21 | max-width: 300px; 22 | } 23 | 24 | .example { 25 | margin: 20px 0; 26 | } 27 | 28 | $border-style: 1px solid #ddd; 29 | $base-padding: 8px; 30 | 31 | li { 32 | padding: $base-padding; 33 | border-bottom: $border-style; 34 | line-height: 1.2; 35 | 36 | &:first-child { 37 | border-top: $border-style; 38 | } 39 | } 40 | 41 | .octocat { 42 | display: block; 43 | width: 32px; 44 | margin: 0 auto; 45 | } 46 | 47 | /* 48 | * dragging stuff 49 | */ 50 | 51 | .dragging { 52 | opacity: 0.3; 53 | } 54 | 55 | .dropping { 56 | background-color: rgba(231, 237, 242, 0.63); 57 | } 58 | 59 | .dropping-above { 60 | padding-top: $base-padding * 2; 61 | } 62 | 63 | .dropping-below { 64 | padding-bottom: $base-padding * 2; 65 | } 66 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drag to Reorder 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Drag to Reorder

16 |
17 | 20 |
21 | 22 | GitHub 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /demo/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font-size: 100%; 11 | font: inherit; 12 | vertical-align: baseline; } 13 | 14 | /* HTML5 display-role reset for older browsers */ 15 | 16 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 17 | display: block; } 18 | 19 | body { 20 | line-height: 1; } 21 | 22 | ol, ul { 23 | list-style: none; } 24 | 25 | blockquote, q { 26 | quotes: none; } 27 | 28 | blockquote { 29 | &:before, &:after { 30 | content: ''; 31 | content: none; } } 32 | 33 | q { 34 | &:before, &:after { 35 | content: ''; 36 | content: none; } } 37 | 38 | table { 39 | border-collapse: collapse; 40 | border-spacing: 0; } 41 | -------------------------------------------------------------------------------- /dist/angular-drag-to-reorder.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | angular-drag-to-reorder v0.0.2 3 | http://brandly.github.io/angular-drag-to-reorder/ 4 | */ 5 | (function(){angular.module("mb-dragToReorder",[]).directive("dragToReorder",[function(){return{link:function(r,e,a){var o,n,t,d,s,i;if(null==r[a.dragToReorder])throw"Must specify the list to reorder";return n="dragging",e.attr("draggable",!0),e.on("dragstart",function(a){return e.addClass(n),a.dataTransfer.setData("text/plain",r.$index)}),e.on("dragend",function(){return e.removeClass(n)}),i="dropping",d="dropping-above",s="dropping-below",o=function(r){var a;return r.preventDefault(),a=r.offsetY||r.layerY,an)for(l=v=n;u>v;l=v+=1)g[l]=g[l+1];else if(n>u)for(l=p=n;p>u;l=p+=-1)g[l]=g[l-1];return g[u]=f,r.$apply(function(){return r.$emit("dragToReorder.reordered",{array:g,item:f,from:n,to:u})}),e.removeClass(i),e.removeClass(d),e.removeClass(s),e.off("drop",t)},e.on("dragenter",function(){return e.hasClass(n)?void 0:(e.addClass(i),e.on("dragover",o),e.on("drop",t))}),e.on("dragleave",function(){return e.removeClass(i),e.removeClass(d),e.removeClass(s),e.off("dragover",o),e.off("drop",t)})}}}])}).call(this); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Drag to Reorder 2 | 3 | > Drag items in an ng-repeat to reorder them 4 | 5 | ```shell 6 | $ bower install --save angular-drag-to-reorder 7 | ``` 8 | 9 | I wrote this for [another small project](http://brandly.github.io/thelist/#/), and I figured someone else might find it useful. It's probably far less stable than something like [UI Sortable](https://github.com/angular-ui/ui-sortable), but that has all kinds of dependencies. This just needs Angular. 10 | 11 | There aren't many frills, but it seems to do the job in up-to-date browsers. 12 | 13 | ## But how? 14 | 15 | Add `drag-to-reorder` alongside your `ng-repeat` and specify the name of the collection. 16 | 17 | ```html 18 | 21 | ``` 22 | 23 | And it should Just Work™. 24 | 25 | ## What else? 26 | 27 | ### Classes 28 | 29 | When dragging and dropping elements, some classes will be added to those elements, so you can style accordingly. 30 | 31 | The element being dragged will have a `dragging` class on it. 32 | 33 | The element that is being hovered over by a dragged element will have a `dropping` class. More specifically, you'll see a `dropping-above` or `dropping-below` class on there, depending on where the dragged element will end up after being dropped. 34 | 35 | ### Events 36 | 37 | There's just one. When the list gets reordered, `dragToReorder.reordered` will fire, passing you some relevant data. 38 | 39 | ```js 40 | $scope.$on('dragToReorder.reordered', function ($event, reordered) { 41 | // The list being reordered 42 | reordered.array 43 | 44 | // The item that was relocated 45 | reordered.item 46 | 47 | // The initial index of that item 48 | reordered.from 49 | 50 | // The index where it ended up 51 | reordered.to 52 | }); 53 | ``` 54 | 55 | ### Demo 56 | 57 | [Look!](http://brandly.github.io/angular-drag-to-reorder/) 58 | 59 | ### Development 60 | 61 | Get your dependencies 62 | ```shell 63 | $ npm install 64 | ``` 65 | 66 | And use `gulp` to build, watch, and host the project. 67 | ```shell 68 | $ gulp 69 | ``` 70 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var 2 | gulp = require('gulp'), 3 | coffee = require('gulp-coffee'), 4 | concat = require('gulp-concat'), 5 | sass = require('gulp-sass'), 6 | gutil = require('gulp-util'), 7 | uglify = require('gulp-uglify'), 8 | minify = require('gulp-minify-css'), 9 | rename = require('gulp-rename'), 10 | header = require('gulp-header'), 11 | path = require('path'), 12 | express = require('express'), 13 | package = require('./package.json'), 14 | 15 | build = gutil.env.gh ? './gh-pages/' : './build/', 16 | 17 | banner = [ 18 | '/*', 19 | ' <%= package.name %> v<%= package.version %>', 20 | ' <%= package.homepage %>', 21 | '*/', 22 | '' 23 | ].join('\n'), 24 | libFileName = 'angular-drag-to-reorder.js'; 25 | 26 | function onError(err) { 27 | gutil.log(err); 28 | gutil.beep(); 29 | this.emit('end'); 30 | } 31 | 32 | gulp.task('coffee:lib', function () { 33 | return gulp.src('src/angular-drag-to-reorder.coffee') 34 | .pipe(coffee()) 35 | .on('error', onError) 36 | .pipe(concat(libFileName)) 37 | .pipe(gulp.dest(build)) 38 | //dist 39 | .pipe(header(banner, {package: package})) 40 | .pipe(gulp.dest('dist/')) 41 | .pipe(uglify()) 42 | .pipe(rename(function (path) { 43 | path.basename += '.min'; 44 | })) 45 | .pipe(header(banner, {package: package})) 46 | .pipe(gulp.dest('dist/')); 47 | }); 48 | 49 | gulp.task('coffee:demo', function () { 50 | return gulp.src('demo/**/*.coffee') 51 | .pipe(coffee()) 52 | .on('error', onError) 53 | .pipe(concat('demo.js')) 54 | .pipe(gulp.dest(build)); 55 | }); 56 | 57 | gulp.task('coffee', ['coffee:lib', 'coffee:demo']); 58 | 59 | gulp.task('sass', function () { 60 | return gulp.src('demo/styles/style.scss') 61 | .pipe(sass()) 62 | .on('error', onError) 63 | .pipe(concat('demo.css')) 64 | .pipe(gulp.dest(build)); 65 | }); 66 | 67 | gulp.task('images', function () { 68 | return gulp.src('demo/images/*') 69 | .pipe(gulp.dest(build)); 70 | }); 71 | 72 | gulp.task('index', function () { 73 | return gulp.src('demo/index.html') 74 | .pipe(gulp.dest(build)); 75 | }); 76 | 77 | gulp.task('build', [ 78 | 'index', 79 | 'coffee', 80 | 'sass', 81 | 'images' 82 | ]); 83 | 84 | gulp.task('default', ['build'], function () { 85 | if (!gutil.env.gh) { 86 | gulp.watch(['src/**', 'demo/**'], ['build']); 87 | 88 | var 89 | app = express(), 90 | port = 8888; 91 | app.use(express.static(path.resolve(build))); 92 | app.listen(port, function() { 93 | gutil.log('Listening on', port); 94 | }); 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /src/angular-drag-to-reorder.coffee: -------------------------------------------------------------------------------- 1 | angular.module('mb-dragToReorder', []) 2 | .directive('dragToReorder', [ -> 3 | link: (scope, element, attrs) -> 4 | unless scope[attrs.dragToReorder]? 5 | throw 'Must specify the list to reorder' 6 | ### 7 | drag stuff 8 | ### 9 | 10 | draggingClassName = 'dragging' 11 | element.attr 'draggable', true 12 | 13 | element.on 'dragstart', (e) -> 14 | element.addClass draggingClassName 15 | e.dataTransfer.setData 'text/plain', scope.$index 16 | 17 | element.on 'dragend', -> 18 | element.removeClass draggingClassName 19 | 20 | ### 21 | drop stuff 22 | ### 23 | 24 | droppingClassName = 'dropping' 25 | droppingAboveClassName = 'dropping-above' 26 | droppingBelowClassName = 'dropping-below' 27 | 28 | dragOverHandler = (e) -> 29 | e.preventDefault() 30 | offsetY = e.offsetY or e.layerY 31 | 32 | # above halfway 33 | if offsetY < (@offsetHeight / 2) 34 | element.removeClass droppingBelowClassName 35 | element.addClass droppingAboveClassName 36 | # below 37 | else 38 | element.removeClass droppingAboveClassName 39 | element.addClass droppingBelowClassName 40 | 41 | dropHandler = (e) -> 42 | e.preventDefault() 43 | droppedItemIndex = parseInt e.dataTransfer.getData('text/plain'), 10 44 | theList = scope[attrs.dragToReorder] 45 | 46 | newIndex = null 47 | # dropping item above us 48 | if element.hasClass droppingAboveClassName 49 | if droppedItemIndex < scope.$index 50 | # dropped item was already above us, 51 | # so now it'll be one index above 52 | newIndex = scope.$index - 1 53 | else 54 | # since it's moving from below to above us, 55 | # it'll need to take our index 56 | newIndex = scope.$index 57 | 58 | # dropping item below us 59 | else 60 | if droppedItemIndex < scope.$index 61 | # moving above to below 62 | newIndex = scope.$index 63 | else 64 | # still below 65 | newIndex = scope.$index + 1 66 | 67 | itemToMove = theList[droppedItemIndex] 68 | # moving down the list 69 | if newIndex > droppedItemIndex 70 | # move all items below it... 71 | for i in [droppedItemIndex...newIndex] by 1 72 | # ...up one spot 73 | theList[i] = theList[i + 1] 74 | 75 | # moving up the list 76 | else if newIndex < droppedItemIndex 77 | for i in [droppedItemIndex...newIndex] by -1 78 | theList[i] = theList[i - 1] 79 | 80 | # put it in its new home 81 | theList[newIndex] = itemToMove 82 | # let angular rearrange the DOM 83 | scope.$apply -> 84 | # and let em know 85 | scope.$emit 'dragToReorder.reordered', 86 | array: theList 87 | item: itemToMove 88 | from: droppedItemIndex 89 | to: newIndex 90 | 91 | # cleanup 92 | element.removeClass droppingClassName 93 | element.removeClass droppingAboveClassName 94 | element.removeClass droppingBelowClassName 95 | element.off 'drop', dropHandler 96 | 97 | element.on 'dragenter', (e) -> 98 | # make sure we're not dropping on the dragged element 99 | return if element.hasClass draggingClassName 100 | 101 | element.addClass droppingClassName 102 | element.on 'dragover', dragOverHandler 103 | element.on 'drop', dropHandler 104 | 105 | element.on 'dragleave', (e) -> 106 | element.removeClass droppingClassName 107 | element.removeClass droppingAboveClassName 108 | element.removeClass droppingBelowClassName 109 | element.off 'dragover', dragOverHandler 110 | element.off 'drop', dropHandler 111 | ]) 112 | -------------------------------------------------------------------------------- /dist/angular-drag-to-reorder.js: -------------------------------------------------------------------------------- 1 | /* 2 | angular-drag-to-reorder v0.0.2 3 | http://brandly.github.io/angular-drag-to-reorder/ 4 | */ 5 | (function() { 6 | angular.module('mb-dragToReorder', []).directive('dragToReorder', [ 7 | function() { 8 | return { 9 | link: function(scope, element, attrs) { 10 | var dragOverHandler, draggingClassName, dropHandler, droppingAboveClassName, droppingBelowClassName, droppingClassName; 11 | if (scope[attrs.dragToReorder] == null) { 12 | throw 'Must specify the list to reorder'; 13 | } 14 | 15 | /* 16 | drag stuff 17 | */ 18 | draggingClassName = 'dragging'; 19 | element.attr('draggable', true); 20 | element.on('dragstart', function(e) { 21 | element.addClass(draggingClassName); 22 | return e.dataTransfer.setData('text/plain', scope.$index); 23 | }); 24 | element.on('dragend', function() { 25 | return element.removeClass(draggingClassName); 26 | }); 27 | 28 | /* 29 | drop stuff 30 | */ 31 | droppingClassName = 'dropping'; 32 | droppingAboveClassName = 'dropping-above'; 33 | droppingBelowClassName = 'dropping-below'; 34 | dragOverHandler = function(e) { 35 | var offsetY; 36 | e.preventDefault(); 37 | offsetY = e.offsetY || e.layerY; 38 | if (offsetY < (this.offsetHeight / 2)) { 39 | element.removeClass(droppingBelowClassName); 40 | return element.addClass(droppingAboveClassName); 41 | } else { 42 | element.removeClass(droppingAboveClassName); 43 | return element.addClass(droppingBelowClassName); 44 | } 45 | }; 46 | dropHandler = function(e) { 47 | var droppedItemIndex, i, itemToMove, newIndex, theList, _i, _j; 48 | e.preventDefault(); 49 | droppedItemIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); 50 | theList = scope[attrs.dragToReorder]; 51 | newIndex = null; 52 | if (element.hasClass(droppingAboveClassName)) { 53 | if (droppedItemIndex < scope.$index) { 54 | newIndex = scope.$index - 1; 55 | } else { 56 | newIndex = scope.$index; 57 | } 58 | } else { 59 | if (droppedItemIndex < scope.$index) { 60 | newIndex = scope.$index; 61 | } else { 62 | newIndex = scope.$index + 1; 63 | } 64 | } 65 | itemToMove = theList[droppedItemIndex]; 66 | if (newIndex > droppedItemIndex) { 67 | for (i = _i = droppedItemIndex; _i < newIndex; i = _i += 1) { 68 | theList[i] = theList[i + 1]; 69 | } 70 | } else if (newIndex < droppedItemIndex) { 71 | for (i = _j = droppedItemIndex; _j > newIndex; i = _j += -1) { 72 | theList[i] = theList[i - 1]; 73 | } 74 | } 75 | theList[newIndex] = itemToMove; 76 | scope.$apply(function() { 77 | return scope.$emit('dragToReorder.reordered', { 78 | array: theList, 79 | item: itemToMove, 80 | from: droppedItemIndex, 81 | to: newIndex 82 | }); 83 | }); 84 | element.removeClass(droppingClassName); 85 | element.removeClass(droppingAboveClassName); 86 | element.removeClass(droppingBelowClassName); 87 | return element.off('drop', dropHandler); 88 | }; 89 | element.on('dragenter', function(e) { 90 | if (element.hasClass(draggingClassName)) { 91 | return; 92 | } 93 | element.addClass(droppingClassName); 94 | element.on('dragover', dragOverHandler); 95 | return element.on('drop', dropHandler); 96 | }); 97 | return element.on('dragleave', function(e) { 98 | element.removeClass(droppingClassName); 99 | element.removeClass(droppingAboveClassName); 100 | element.removeClass(droppingBelowClassName); 101 | element.off('dragover', dragOverHandler); 102 | return element.off('drop', dropHandler); 103 | }); 104 | } 105 | }; 106 | } 107 | ]); 108 | 109 | }).call(this); 110 | --------------------------------------------------------------------------------