diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..65365be --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..bc1a1cc --- /dev/null +++ b/.jshintrc @@ -0,0 +1,53 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": false, + "eqeqeq": true, + "es3": false, + "forin": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "noarg": true, + "noempty": true, + "nonew": false, + "plusplus": false, + "quotmark": true, + "regexp": false, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "maxparams": 4, + "maxdepth": 2, + "maxstatements": 30, + "maxcomplexity": 10, + "maxlen": 110, + + "asi": false, + "boss": false, + "debug": false, + "eqnull": true, + "esnext": false, + "evil": false, + "expr": false, + "funcscope": false, + "globalstrict": false, + "iterator": false, + "lastsemic": false, + "laxbreak": false, + "laxcomma": false, + "loopfunc": false, + "moz": false, + "multistr": true, + "proto": false, + "scripturl": false, + "smarttabs": false, + "shadow": false, + "sub": false, + "supernew": false, + "validthis": true, + + "node": true +} diff --git a/README.md b/README.md index e78cddc..489fbf9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,28 @@ # angular-draganddrop [![Build Status](https://travis-ci.org/lemonde/angular-draganddrop.svg?branch=master)](https://travis-ci.org/lemonde/angular-draganddrop) -[![Dependency Status](https://david-dm.org/neoziro/angular-draganddrop.svg?theme=shields.io)](https://david-dm.org/neoziro/angular-draganddrop) -[![devDependency Status](https://david-dm.org/neoziro/angular-draganddrop/dev-status.svg?theme=shields.io)](https://david-dm.org/neoziro/angular-draganddrop#info=devDependencies) +[![Dependency Status](https://david-dm.org/lemonde/angular-draganddrop.svg?theme=shields.io)](https://david-dm.org/lemonde/angular-draganddrop) +[![devDependency Status](https://david-dm.org/lemonde/angular-draganddrop/dev-status.svg?theme=shields.io)](https://david-dm.org/lemonde/angular-draganddrop#info=devDependencies) Drag and drop directives for Angular using native HTML5 API. + +## introduction + +This module is a simple Angular wrapping of the native HTML5 drag & drop API + a few common utilities. +Callbacks are invoked in Angular context. + +It defines 2 directives : +* "draggable" for a draggable element +* "drop" for a drop zone +It handles data type control, adding a CSS class on drag and on hover and takes care of one HTML5 quirk. + +**CAUTION** The HTML5 drag&drop API is NOT trivial. +* If you know it and want to use it, proceed with this package +* If you don't know it, consider using a 3rd-party re-implementation : http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html +* Reference doc for drag&drop API : + * https://html.spec.whatwg.org/multipage/interaction.html#dnd + + ## Install ### Using bower @@ -19,9 +37,11 @@ bower install angular-draganddrop npm install angular-draganddrop ``` + ## Usage -HTML : +1. read the warning in introduction (really) +2. HTML : ```html @@ -39,7 +59,7 @@ HTML : ``` -JavaScript : +3. JavaScript : ```js angular.module('controllers.dragDrop', ['draganddrop']) @@ -65,22 +85,36 @@ angular.module('controllers.dragDrop', ['draganddrop']) ### "draggable" directive +Parameters : - "draggable" Make the element draggable. Accepts a boolean. - "effect-allowed" Allowed effects for the dragged element, see https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer#effectAllowed.28.29. Accepts a string. - "draggable-type" Type of data object attached to the dragged element, this type is prefixed by "json/". Accepts a string. - "draggable-data" Data attached to the dragged element, data are serialized in JSON. Accepts an Angular expression. +- "dragging-class" Class set during the drag. Accepts a string. +- "drag-start" (optional) an Angular expression to be evaluated on drag start ("dragstart" event). +- "drag-end" (optional) an Angular expression to be evaluated on drag end ("dragend" event). The draggable directive serializes data as JSON and prefix the specified type with "json/". ### "drop" directive +The drop directive automatically : +- calls the event.preventDefault() for dragenter and dragleave, as asked in the spec +- unserializes data with the "json" format, other data are not formatted +- throttles the dragover event (200ms) to avoid burning your CPU during the drag&drop + +Parameters : - "drop" Drop handler, executed on drop. Accepts an Angular expression. - "drop-effect" Drop effect to set, see https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer#dropEffect.28.29. Accepts a string. - "drop-accept" Types accepted or function to prevent unauthorized drag and drop. Accepts a string, an array, a function or a boolean. -- "drag-over" Drag over handler, executed on drag over. Accepts an Angular expression. - "drag-over-class" Class set on drag over, when the drag is authorized. Accepts a string. +- "drag-over" (optional) an Angular expression to be evaluated on drag over ("dragover" event). +- "drag-enter" (optional) an Angular expression to be evaluated on drag enter ("dragenter" event). +- "drag-leave" (optional) an Angular expression to be evaluated on drag leave ("dragleave" event). +- "drop" (optional) an Angular expression to be evaluated on drag over ("drop" event). + +Handlers are called with : `(data, event)` -The drop directive automatically unserializes data with the "json" format, other data are not formatted. ## Browsers support diff --git a/angular-draganddrop.js b/angular-draganddrop.js index 22156b3..8ec6b41 100644 --- a/angular-draganddrop.js +++ b/angular-draganddrop.js @@ -1,10 +1,15 @@ -/*! Angular draganddrop v0.2.1 | (c) 2013 Greg Bergé | License MIT */ - angular .module('draganddrop', []) .directive('draggable', ['$parse', draggableDirective]) .directive('drop', ['$parse', dropDirective]); + +// log hooks for developers +// @see https://github.com/lemonde/angular-draganddrop/wiki#possible-implementations-of-debug-hooks- +function debugDraggable(scope) {} +function debugDroppable(scope) {} + + /** * Draggable directive. * @@ -19,68 +24,102 @@ angular is prefixed by "json/". Accepts a string. * - "draggable-data" Data attached to the dragged element, data are serialized in JSON. Accepts an Angular expression. + * - "dragging-class" + * - "drag-start" + * - "drag-end" */ function draggableDirective($parse) { + 'use strict'; + return { restrict: 'A', link: function (scope, element, attrs) { + debugDraggable(scope, 'init'); + var domElement = element[0]; var effectAllowed; var draggableData; var draggableType; + var draggingClass = attrs.draggingClass; attrs.$observe('effectAllowed', function (val) { + debugDraggable(scope, 'observed effectAllowed change'); effectAllowed = val; }); attrs.$observe('draggableData', function (val) { + debugDraggable(scope, 'observed draggableData change'); draggableData = val; }); attrs.$observe('draggableType', function (val) { + debugDraggable(scope, 'observed draggableType change'); draggableType = val; }); - var dragStartHandler = $parse(attrs.dragStart); - var dragEndHandler = $parse(attrs.dragEnd); - attrs.$observe('draggable', function (draggable) { - domElement.draggable = draggable !== 'false'; + debugDraggable(scope, 'observed draggable change'); + domElement.draggable = (draggable !== 'false'); }); + var dragStartHandler = $parse(attrs.dragStart); + var dragEndHandler = $parse(attrs.dragEnd); + domElement.addEventListener('dragstart', dragStartListener); domElement.addEventListener('dragend', dragEndListener); scope.$on('$destroy', function () { + debugDraggable(scope, '$destroy'); domElement.removeEventListener('dragstart', dragStartListener); domElement.removeEventListener('dragend', dragEndListener); }); + // Convenience function to help the directive user. + // AngularJS default error is unclear. + function safeDraggableDataEval() { + try { + return scope.$eval(draggableData); + } + catch(e) { + // throw a clearer error + throw new Error('draggable-data can\'t be parsed by Angular : ' + + 'check your draggable directive invocation !'); + } + } + function dragStartListener(event) { + debugDraggable(scope, 'dragstart'); + // Restrict drag effect. event.dataTransfer.effectAllowed = effectAllowed || event.dataTransfer.effectAllowed; + if (draggingClass) element.addClass(draggingClass); + // Eval and serialize data. - var data = scope.$eval(draggableData); + var data = safeDraggableDataEval(); var jsonData = angular.toJson(data); - // Call dragStartHandler + // Set drag data and drag type. + event.dataTransfer.setData('json/' + draggableType, jsonData); + + // Call custom handler scope.$apply(function () { dragStartHandler(scope, { $data: data, $event: event }); }); - // Set drag data and drag type. - event.dataTransfer.setData('json/' + draggableType, jsonData); - event.stopPropagation(); } function dragEndListener(event) { + debugDraggable(scope, 'dragend'); + + element.removeClass(draggingClass); + // Eval and serialize data. - var data = scope.$eval(draggableData); + var data = safeDraggableDataEval(); - // Call dragEndHandler + // Call custom handler scope.$apply(function () { dragEndHandler(scope, { $data: data, $event: event }); }); @@ -109,24 +148,31 @@ function draggableDirective($parse) { */ function dropDirective($parse) { + 'use strict'; + return { restrict: 'A', link: function (scope, element, attrs) { + debugDroppable(scope, 'init'); + var domElement = element[0]; var dropEffect = attrs.dropEffect; var dropAccept = attrs.dropAccept; var dragOverClass = attrs.dragOverClass; var dragOverHandler = $parse(attrs.dragOver); + var dragEnterHandler = $parse(attrs.dragEnter); var dragLeaveHandler = $parse(attrs.dragLeave); var dropHandler = $parse(attrs.drop); domElement.addEventListener('dragover', dragOverListener); + domElement.addEventListener('dragenter', dragEnterListener); domElement.addEventListener('dragleave', dragLeaveListener); domElement.addEventListener('drop', dropListener); scope.$on('$destroy', function () { domElement.removeEventListener('dragover', dragOverListener); + domElement.removeEventListener('dragenter', dragEnterListener); domElement.removeEventListener('dragleave', dragLeaveListener); domElement.removeEventListener('drop', dropListener); }); @@ -134,70 +180,93 @@ function dropDirective($parse) { var throttledDragover = 0; function dragOverListener(event) { + debugDroppable(scope, 'dragover'); + // Check if type is accepted. - if (! accepts(scope.$eval(dropAccept), event)) return true; + if (! accepts(scope.$eval(dropAccept), event)) { + debugDroppable(scope, 'dragover rejected, draggable type not accepted'); + return true; + } - if (dragOverClass) element.addClass(dragOverClass); + // Prevent default to accept drag and drop. + event.preventDefault(); // Set up drop effect to link. event.dataTransfer.dropEffect = dropEffect || event.dataTransfer.dropEffect; - // Prevent default to accept drag and drop. - event.preventDefault(); + if (dragOverClass) element.addClass(dragOverClass); + // throttling to avoid loading CPU var now = new Date().getTime(); - if (now - throttledDragover < 200) return; + if (now - throttledDragover < 200) { + debugDroppable(scope, 'dragover throttled'); + return; + } throttledDragover = now; - if (!attrs.dragOver) return; + if (! attrs.dragOver) return; + // Call custom handler var data = getData(event); - - // Call dragOverHandler scope.$apply(function () { + debugDroppable(scope, 'dragover callback !'); dragOverHandler(scope, { $data: data, $event: event }); }); } - function dragLeaveListener(event) { + function dragEnterListener(event) { + debugDroppable(scope, 'dragenter'); + // Check if type is accepted. if (! accepts(scope.$eval(dropAccept), event)) return true; - removeDragOverClass(); + // Prevent default to accept drag and drop. + event.preventDefault(); - if (!attrs.dragLeave) return; + if (dragOverClass) element.addClass(dragOverClass); - var data = getData(event); + if (! attrs.dragEnter) return; - // Call dragLeaveHandler + // Call custom handler + var data = getData(event); scope.$apply(function () { - dragLeaveHandler(scope, { $data: data, $event: event }); + dragEnterHandler(scope, { $data: data, $event: event }); }); + } + + function dragLeaveListener(event) { + debugDroppable(scope, 'dragleave'); + + // Check if type is accepted. + if (! accepts(scope.$eval(dropAccept), event)) return true; // Prevent default to accept drag and drop. event.preventDefault(); - } - function dropListener(event) { - var data = getData(event); + element.removeClass(dragOverClass); - removeDragOverClass(); + if (! attrs.dragLeave) return; - // Call dropHandler + // Call custom handler + var data = getData(event); scope.$apply(function () { - dropHandler(scope, { $data: data, $event: event }); + dragLeaveHandler(scope, { $data: data, $event: event }); }); + } + + function dropListener(event) { + debugDroppable(scope, 'drop'); // Prevent default navigator behaviour. event.preventDefault(); - } - /** - * Remove the drag over class. - */ - - function removeDragOverClass() { element.removeClass(dragOverClass); + + // Call custom handler + var data = getData(event); + scope.$apply(function () { + dropHandler(scope, { $data: data, $event: event }); + }); } /** diff --git a/angular-draganddrop.min.js b/angular-draganddrop.min.js index 3e52119..c12e41a 100644 --- a/angular-draganddrop.min.js +++ b/angular-draganddrop.min.js @@ -1,3 +1,2 @@ -/*! Angular draganddrop v0.2.1 | (c) 2013 Greg Bergé | License MIT */ -function draggableDirective(a){return{restrict:"A",link:function(b,c,d){function e(a){a.dataTransfer.effectAllowed=g||a.dataTransfer.effectAllowed;var c=b.$eval(h),d=angular.toJson(c);b.$apply(function(){k(b,{$data:c,$event:a})}),a.dataTransfer.setData("json/"+i,d),a.stopPropagation()}function f(a){var c=b.$eval(h);b.$apply(function(){l(b,{$data:c,$event:a})}),a.stopPropagation()}var g,h,i,j=c[0];d.$observe("effectAllowed",function(a){g=a}),d.$observe("draggableData",function(a){h=a}),d.$observe("draggableType",function(a){i=a});var k=a(d.dragStart),l=a(d.dragEnd);d.$observe("draggable",function(a){j.draggable="false"!==a}),j.addEventListener("dragstart",e),j.addEventListener("dragend",f),b.$on("$destroy",function(){j.removeEventListener("dragstart",e),j.removeEventListener("dragend",f)})}}}function dropDirective(a){return{restrict:"A",link:function(b,c,d){function e(a){if(!i(b.$eval(n),a))return!0;o&&c.addClass(o),a.dataTransfer.dropEffect=m||a.dataTransfer.dropEffect,a.preventDefault();var e=(new Date).getTime();if(!(200>e-s)&&(s=e,d.dragOver)){var f=j(a);b.$apply(function(){p(b,{$data:f,$event:a})})}}function f(a){if(!i(b.$eval(n),a))return!0;if(h(),d.dragLeave){var c=j(a);b.$apply(function(){q(b,{$data:c,$event:a})}),a.preventDefault()}}function g(a){var c=j(a);h(),b.$apply(function(){r(b,{$data:c,$event:a})}),a.preventDefault()}function h(){c.removeClass(o)}function i(a,b){return"boolean"==typeof a?a:"string"==typeof a?i([a],b):Array.isArray(a)?i(function(b){return b.some(function(b){return b=b.split("/"),a.some(function(a){a=a.split("/");var c=!0;return b.forEach(function(b,d){"*"!==a[d]&&b!==a[d]&&(c=!1)}),c})})},b):"function"==typeof a?a(k(b.dataTransfer.types)):!1}function j(a){var b=k(a.dataTransfer.types);return b.reduce(function(b,c){var d=a.dataTransfer.getData(c),e=c.split("/")[0];return"json"===e&&d&&(d=angular.fromJson(d)),b[c]=d,c.split("/").reduce(function(a,c){return a.push(c),b[a.join("/")]=d,a},[]),b},{})}function k(a){return Array.prototype.slice.call(a)}var l=c[0],m=d.dropEffect,n=d.dropAccept,o=d.dragOverClass,p=a(d.dragOver),q=a(d.dragLeave),r=a(d.drop);l.addEventListener("dragover",e),l.addEventListener("dragleave",f),l.addEventListener("drop",g),b.$on("$destroy",function(){l.removeEventListener("dragover",e),l.removeEventListener("dragleave",f),l.removeEventListener("drop",g)});var s=0}}}angular.module("draganddrop",[]).directive("draggable",["$parse",draggableDirective]).directive("drop",["$parse",dropDirective]); +function debugDraggable(a){}function debugDroppable(a){}function draggableDirective(a){"use strict";return{restrict:"A",link:function(b,c,d){function e(){try{return b.$eval(i)}catch(a){throw new Error("draggable-data can't be parsed by Angular : check your draggable directive invocation !")}}function f(a){debugDraggable(b,"dragstart"),a.dataTransfer.effectAllowed=h||a.dataTransfer.effectAllowed,l&&c.addClass(l);var d=e(),f=angular.toJson(d);a.dataTransfer.setData("json/"+j,f),b.$apply(function(){m(b,{$data:d,$event:a})}),a.stopPropagation()}function g(a){debugDraggable(b,"dragend"),c.removeClass(l);var d=e();b.$apply(function(){n(b,{$data:d,$event:a})}),a.stopPropagation()}debugDraggable(b,"init");var h,i,j,k=c[0],l=d.draggingClass;d.$observe("effectAllowed",function(a){debugDraggable(b,"observed effectAllowed change"),h=a}),d.$observe("draggableData",function(a){debugDraggable(b,"observed draggableData change"),i=a}),d.$observe("draggableType",function(a){debugDraggable(b,"observed draggableType change"),j=a}),d.$observe("draggable",function(a){debugDraggable(b,"observed draggable change"),k.draggable="false"!==a});var m=a(d.dragStart),n=a(d.dragEnd);k.addEventListener("dragstart",f),k.addEventListener("dragend",g),b.$on("$destroy",function(){debugDraggable(b,"$destroy"),k.removeEventListener("dragstart",f),k.removeEventListener("dragend",g)})}}}function dropDirective(a){"use strict";return{restrict:"A",link:function(b,c,d){function e(a){if(debugDroppable(b,"dragover"),!i(b.$eval(n),a))return debugDroppable(b,"dragover rejected, draggable type not accepted"),!0;a.preventDefault(),a.dataTransfer.dropEffect=m||a.dataTransfer.dropEffect,o&&c.addClass(o);var e=(new Date).getTime();if(200>e-t)return void debugDroppable(b,"dragover throttled");if(t=e,d.dragOver){var f=j(a);b.$apply(function(){debugDroppable(b,"dragover callback !"),p(b,{$data:f,$event:a})})}}function f(a){if(debugDroppable(b,"dragenter"),!i(b.$eval(n),a))return!0;if(a.preventDefault(),o&&c.addClass(o),d.dragEnter){var e=j(a);b.$apply(function(){q(b,{$data:e,$event:a})})}}function g(a){if(debugDroppable(b,"dragleave"),!i(b.$eval(n),a))return!0;if(a.preventDefault(),c.removeClass(o),d.dragLeave){var e=j(a);b.$apply(function(){r(b,{$data:e,$event:a})})}}function h(a){debugDroppable(b,"drop"),a.preventDefault(),c.removeClass(o);var d=j(a);b.$apply(function(){s(b,{$data:d,$event:a})})}function i(a,b){return"boolean"==typeof a?a:"string"==typeof a?i([a],b):Array.isArray(a)?i(function(b){return b.some(function(b){return b=b.split("/"),a.some(function(a){a=a.split("/");var c=!0;return b.forEach(function(b,d){"*"!==a[d]&&b!==a[d]&&(c=!1)}),c})})},b):"function"==typeof a?a(k(b.dataTransfer.types)):!1}function j(a){var b=k(a.dataTransfer.types);return b.reduce(function(b,c){var d=a.dataTransfer.getData(c),e=c.split("/")[0];return"json"===e&&d&&(d=angular.fromJson(d)),b[c]=d,c.split("/").reduce(function(a,c){return a.push(c),b[a.join("/")]=d,a},[]),b},{})}function k(a){return Array.prototype.slice.call(a)}debugDroppable(b,"init");var l=c[0],m=d.dropEffect,n=d.dropAccept,o=d.dragOverClass,p=a(d.dragOver),q=a(d.dragEnter),r=a(d.dragLeave),s=a(d.drop);l.addEventListener("dragover",e),l.addEventListener("dragenter",f),l.addEventListener("dragleave",g),l.addEventListener("drop",h),b.$on("$destroy",function(){l.removeEventListener("dragover",e),l.removeEventListener("dragenter",f),l.removeEventListener("dragleave",g),l.removeEventListener("drop",h)});var t=0}}}angular.module("draganddrop",[]).directive("draggable",["$parse",draggableDirective]).directive("drop",["$parse",dropDirective]); //# sourceMappingURL=angular-draganddrop.min.map \ No newline at end of file diff --git a/angular-draganddrop.min.map b/angular-draganddrop.min.map index ec393b3..2c907d1 100644 --- a/angular-draganddrop.min.map +++ b/angular-draganddrop.min.map @@ -1 +1 @@ -{"version":3,"file":"angular-draganddrop.min.js","sources":["angular-draganddrop.js"],"names":["draggableDirective","$parse","restrict","link","scope","element","attrs","dragStartListener","event","dataTransfer","effectAllowed","data","$eval","draggableData","jsonData","angular","toJson","$apply","dragStartHandler","$data","$event","setData","draggableType","stopPropagation","dragEndListener","dragEndHandler","domElement","$observe","val","dragStart","dragEnd","draggable","addEventListener","$on","removeEventListener","dropDirective","dragOverListener","accepts","dropAccept","dragOverClass","addClass","dropEffect","preventDefault","now","Date","getTime","throttledDragover","dragOver","getData","dragOverHandler","dragLeaveListener","removeDragOverClass","dragLeave","dragLeaveHandler","dropListener","dropHandler","removeClass","type","Array","isArray","eventTypes","some","eventType","split","_type","match","forEach","eventTypeChunk","index","toArray","types","reduce","collection","format","fromJson","accumulator","chunk","push","join","prototype","slice","call","drop","module","directive"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"angular-draganddrop.min.js","sources":["angular-draganddrop.js"],"names":["debugDraggable","scope","debugDroppable","draggableDirective","$parse","restrict","link","element","attrs","safeDraggableDataEval","$eval","draggableData","e","Error","dragStartListener","event","dataTransfer","effectAllowed","draggingClass","addClass","data","jsonData","angular","toJson","setData","draggableType","$apply","dragStartHandler","$data","$event","stopPropagation","dragEndListener","removeClass","dragEndHandler","domElement","$observe","val","draggable","dragStart","dragEnd","addEventListener","$on","removeEventListener","dropDirective","dragOverListener","accepts","dropAccept","preventDefault","dropEffect","dragOverClass","now","Date","getTime","throttledDragover","dragOver","getData","dragOverHandler","dragEnterListener","dragEnter","dragEnterHandler","dragLeaveListener","dragLeave","dragLeaveHandler","dropListener","dropHandler","type","Array","isArray","eventTypes","some","eventType","split","_type","match","forEach","eventTypeChunk","index","toArray","types","reduce","collection","format","fromJson","accumulator","chunk","push","join","prototype","slice","call","drop","module","directive"],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 371d0b9..6f2cbf3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Drag and drop directives for Angular using native HTML5 API.", "main": "index.js", "scripts": { - "install": "bower install", - "test": "./node_modules/karma/bin/karma start --single-run" + "postinstall": "bower install", + "test": "karma start --single-run", + "build": "grunt" }, "repository": { "type": "git", @@ -17,23 +18,23 @@ "angular", "directive" ], - "author": "Greg Bergé ", + "author": "Team CMS ", "license": "MIT", "bugs": { - "url": "https://github.com/neoziro/angular-draganddrop/issues" + "url": "https://github.com/lemonde/angular-draganddrop/issues" }, "devDependencies": { - "bower": "~1.3.3", - "chai": "~1.9.1", - "chai-jquery": "~1.2.3", - "grunt": "~0.4.1", + "bower": "~1.4", + "chai": "~3.2", + "chai-jquery": "~2.0", + "grunt": "~0.4.5", "grunt-contrib-uglify": "~0.2.2", "karma": "~0.12.12", - "karma-chrome-launcher": "~0.1.3", + "karma-chrome-launcher": "~0.2", "karma-firefox-launcher": "~0.1.3", - "karma-mocha": "~0.1.3", - "mocha": "~1.20.1", - "sinon": "~1.10.3", - "sinon-chai": "~2.5.0" + "karma-mocha": "~0.2", + "mocha": "~2.2", + "sinon": "~1.15", + "sinon-chai": "~2.8" } } diff --git a/test/draggable.js b/test/draggable.js index 1b69853..bd406fd 100644 --- a/test/draggable.js +++ b/test/draggable.js @@ -1,79 +1,212 @@ +'use strict'; + var expect = chai.expect; describe('Draggable directive', function () { - var $compile, $rootScope, scope; + var $compile, $rootScope, $scope, element; + var testDragStartEvent, testDragEndEvent; beforeEach(module('draganddrop')); beforeEach(inject(function ($injector) { $compile = $injector.get('$compile'); $rootScope = $injector.get('$rootScope'); - scope = $rootScope.$new(); + $scope = $rootScope.$new(); })); - it('should enable drag', function () { - var element = $compile('
')(scope); - scope.$digest(); - expect(element).to.have.attr('draggable', 'true'); + beforeEach(function () { + var dragDataStore = {}; + + // simulated dragstart event + testDragStartEvent = document.createEvent('CustomEvent'); + testDragStartEvent.initCustomEvent('dragstart', true, false, false); + testDragStartEvent.dataTransfer = { + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } + }; + sinon.spy(testDragStartEvent.dataTransfer, 'setData'); + sinon.spy(testDragStartEvent.dataTransfer, 'getData'); + + // simulated dragend event + testDragEndEvent = document.createEvent('CustomEvent'); + testDragEndEvent.initCustomEvent('dragend', true, false, false); + testDragEndEvent.dataTransfer = { + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } + }; + sinon.spy(testDragEndEvent.dataTransfer, 'setData'); + sinon.spy(testDragEndEvent.dataTransfer, 'getData'); }); - it('should disable drag', function () { - var element = $compile('
')(scope); - expect(element).to.have.attr('draggable', 'false'); - }); + function dispatchEvent(element, event) { + element[0].dispatchEvent(event); + $scope.$digest(); + } - describe('width "effect-allowed", "draggable-data" and "draggable-type" attributes', function () { - var startDrag, dragEvent; + function createElement(tpl) { + element = $compile(tpl)($scope); + $scope.$digest(); + } - beforeEach(function () { - dragEvent = document.createEvent('CustomEvent'); - dragEvent.initCustomEvent('dragstart', true, false, false); - dragEvent.dataTransfer = { - setData: sinon.spy() - }; + describe('HTML5 draggable property', function() { + it('should be enabled by default', function () { + createElement('
'); + expect(element).to.have.attr('draggable', 'true'); + }); - startDrag = function (element) { - element[0].dispatchEvent(dragEvent); - }; + it('should be disabled when requested', function () { + createElement('
'); + expect(element).to.have.attr('draggable', 'false'); }); + }); + + describe('on dragstart when "effect-allowed", "draggable-data" and "draggable-type" attributes are set', function () { it('should set effect and data', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); + + dispatchEvent(element, testDragStartEvent); - scope.$digest(); - startDrag(element); + expect(testDragStartEvent.dataTransfer.effectAllowed).to.equal('link'); + expect(testDragStartEvent.dataTransfer.setData).to.have.been.calledWith('json/image', '{"foo":"bar"}'); + }); + + describe('error handling', function() { + var originalHandler; + + // mandatory to test uncaught + beforeEach(function() { + originalHandler = window.onerror; + }); + afterEach(function() { + window.onerror = originalHandler; + }); + + it('on wrongly formatted draggable data, should throw a meaningful error', function (done) { + var tpl = '
'; + createElement(tpl); + + var uncaughtErrorMessage = null; + window.onerror = function (message) { + if (uncaughtErrorMessage) { // only handle one time to avoid error loops + console.log('uncaught error !', message); + return originalHandler.apply(null, arguments); + } + uncaughtErrorMessage = message || 'no message ?'; + return true; + }; + + dispatchEvent(element, testDragStartEvent); + + expect(uncaughtErrorMessage) + .to.have.string('draggable-data can\'t be parsed by Angular : check your draggable directive invocation !'); + done(); + }); - expect(dragEvent.dataTransfer.effectAllowed).to.equal('link'); - expect(dragEvent.dataTransfer.setData).to.be.calledWith('json/image', '{"foo":"bar"}'); }); it('should listen for data changes', function () { var tpl = '
'; - scope.dragData = {foo: 'bar'}; - var element = $compile(tpl)(scope); + $scope.dragData = {foo: 'bar'}; + createElement(tpl); - scope.dragData = { bar: 'baz'}; - scope.$digest(); - startDrag(element); + $scope.dragData = { bar: 'baz'}; + $scope.$digest(); + dispatchEvent(element, testDragStartEvent); - expect(dragEvent.dataTransfer.effectAllowed).to.equal('link'); - expect(dragEvent.dataTransfer.setData).to.be.calledWith('json/image', '{"bar":"baz"}'); + expect(testDragStartEvent.dataTransfer.effectAllowed).to.equal('link'); + expect(testDragStartEvent.dataTransfer.setData).to.have.been.calledWith('json/image', '{"bar":"baz"}'); }); it('should prevent bubbling', function() { - var tpl = '
'+ - '
'+ - '
'; - var element = $compile(tpl)(scope); + var tpl = '
'+ + '
'+ + '
'; + createElement(tpl); + + // Bubbling can only be tested on firefox, due to a chrome bug where customEvent doesn't bubble + dispatchEvent(element.find('.sub'), testDragStartEvent); + + expect(testDragStartEvent.dataTransfer.setData).to.have.been.calledOnce; + }); + }); - // Bubbling can only be test on firefox, due to a chrome bug where customEvent don't bubble - startDrag(element.find('.sub')); + describe('dragging class', function() { + beforeEach(function() { + var tpl = '
'; + createElement(tpl); + }); + + it('should be added on dragstart', function() { + expect(element).to.not.have.class('foo'); + + dispatchEvent(element, testDragStartEvent); + + expect(element).to.have.class('foo'); + }); + + it('should be removed on dragend', function() { + dispatchEvent(element, testDragStartEvent); + dispatchEvent(element, testDragEndEvent); - expect(dragEvent.dataTransfer.setData).to.have.been.calledOnce; + expect(element).to.not.have.class('foo'); }); }); + + describe('handlers', function() { + + describe('dragstart', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); + + dispatchEvent(element, testDragStartEvent); + }); + + it('should be called on dragstart when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); + + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(testDragStartEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal({ + foo: 'bar' + }); + }); + }); + + describe('dragend', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); + + dispatchEvent(element, testDragStartEvent); + dispatchEvent(element, testDragEndEvent); + }); + + it('should be called on dragend when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); + + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(testDragEndEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal({ + foo: 'bar' + }); + }); + }); + }); + }); diff --git a/test/drop.js b/test/drop.js index 05c0d5c..975e357 100644 --- a/test/drop.js +++ b/test/drop.js @@ -1,14 +1,28 @@ +'use strict'; + var expect = chai.expect; describe('Drop directive', function () { - var $compile, $rootScope, scope, dragOver, dragLeave, dragOverEvent, drop, dropEvent; + var $compile, $rootScope, $scope, element; + var dragOverEvent, dropEvent, dragEnterEvent, dragLeaveEvent; beforeEach(module('draganddrop')); beforeEach(inject(function ($injector) { $compile = $injector.get('$compile'); $rootScope = $injector.get('$rootScope'); - scope = $rootScope.$new(); + $scope = $rootScope.$new(); + })); + + beforeEach(function () { + var testTypes = [ 'json/image', 'text/uri-list' ]; + var dragDataStore = { + // pre-filled with test data + 'json/image': {foo: 'bar'}, + 'json': {foo: 'bar'}, + 'text/uri-list': 'http://dragdrop.com', + 'text': 'http://dragdrop.com' + }; // "dragover" event. dragOverEvent = document.createEvent('CustomEvent'); @@ -16,11 +30,9 @@ describe('Drop directive', function () { sinon.spy(dragOverEvent, 'preventDefault'); dragOverEvent.dataTransfer = { dropEffect: 'none', - types: ['json/image', 'text/uri-list'], - getData: function (type) { - if (type === 'json/image') return '{"foo":"bar"}'; - if (type === 'text/uri-list') return 'http://dragdrop.com'; - } + types: testTypes, + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } }; // "drop" event. @@ -28,129 +40,119 @@ describe('Drop directive', function () { dropEvent.initCustomEvent('drop', false, false, false); sinon.spy(dropEvent, 'preventDefault'); dropEvent.dataTransfer = { - types: ['json/image', 'text/uri-list'], - getData: function (type) { - if (type === 'json/image') return '{"foo":"bar"}'; - if (type === 'text/uri-list') return 'http://dragdrop.com'; - } + types: testTypes, + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } + }; + + // "dragenter" event. + dragEnterEvent = document.createEvent('CustomEvent'); + dragEnterEvent.initCustomEvent('dragenter', false, false, false); + dragEnterEvent.dataTransfer = { + types: testTypes, + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } }; // "dragleave" event. - var dragLeaveEvent = document.createEvent('CustomEvent'); + dragLeaveEvent = document.createEvent('CustomEvent'); dragLeaveEvent.initCustomEvent('dragleave', false, false, false); dragLeaveEvent.dataTransfer = { - types: ['json/image', 'text/uri-list'], - getData: function (type) { - if (type === 'json/image') return '{"foo":"bar"}'; - if (type === 'text/uri-list') return 'http://dragdrop.com'; - } - }; - - dragOver = function (element) { - element[0].dispatchEvent(dragOverEvent); + types: testTypes, + setData: function(format, data) { dragDataStore[format] = data; }, + getData: function(format) { return dragDataStore[format]; } }; + }); - dragLeave = function (element) { - element[0].dispatchEvent(dragLeaveEvent); - }; + function dispatchEvent(element, event) { + element[0].dispatchEvent(event); + $scope.$digest(); + } - drop = function (element) { - element[0].dispatchEvent(dropEvent); - }; - })); + function createElement(tpl) { + element = $compile(tpl)($scope); + $scope.$digest(); + } describe('"drop-effect"', function () { it('should set dataTransfer.dropEffect', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); expect(dragOverEvent.dataTransfer.dropEffect).to.equal('link'); }); }); - describe('"drag-over"', function () { - it('should call "drag-over" with $event', function () { - scope.onDragOver = sinon.spy(); - - var tpl = '
'; - var element = $compile(tpl)(scope); - - dragOver(element); - - expect(scope.onDragOver).to.be.calledWith(dragOverEvent); - }); - }); - describe('"drop-accept"', function () { describe('string', function () { it('should accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.be.called; + expect(dragOverEvent.preventDefault).to.have.been.called; }); it('should not accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.not.be.called; + expect(dragOverEvent.preventDefault).to.not.have.been.called; }); }); describe('array', function () { it('should accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.be.called; + expect(dragOverEvent.preventDefault).to.have.been.called; }); it('should not accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.not.be.called; + expect(dragOverEvent.preventDefault).to.not.have.been.called; }); }); describe('function', function () { it('should accept type', function () { - scope.checkType = function (types) { + $scope.checkType = function (types) { expect(types).to.eql(['json/image', 'text/uri-list']); return true; }; var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.be.called; + expect(dragOverEvent.preventDefault).to.have.been.called; }); it('should not accept type', function () { - scope.checkType = function (types) { + $scope.checkType = function (types) { expect(types).to.eql(['json/image', 'text/uri-list']); return false; }; var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.not.be.called; + expect(dragOverEvent.preventDefault).to.not.have.been.called; }); it('should be compatible with DOMStringList that are not array', function () { @@ -160,37 +162,37 @@ describe('Drop directive', function () { length: 2 }; - scope.checkType = function (types) { + $scope.checkType = function (types) { expect(types).to.eql(['json/image', 'text/uri-list']); return false; }; var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.not.be.called; + expect(dragOverEvent.preventDefault).to.not.have.been.called; }); }); describe('boolean', function () { it('should accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.be.called; + expect(dragOverEvent.preventDefault).to.have.been.called; }); it('should not accept type', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); - expect(dragOverEvent.preventDefault).to.not.be.called; + expect(dragOverEvent.preventDefault).to.not.have.been.called; }); }); }); @@ -198,49 +200,127 @@ describe('Drop directive', function () { describe('"drag-over-class"', function () { it('should add class if accepted', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); + dispatchEvent(element, dragOverEvent); expect(element).to.have.class('dragover'); }); it('should remove class on drag leave', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); - dragLeave(element); + dispatchEvent(element, dragOverEvent); + dispatchEvent(element, dragLeaveEvent); expect(element).to.not.have.class('dragover'); }); it('should remove class on drop', function () { var tpl = '
'; - var element = $compile(tpl)(scope); + createElement(tpl); - dragOver(element); - drop(element); + dispatchEvent(element, dragOverEvent); + dispatchEvent(element, dropEvent); expect(element).to.not.have.class('dragover'); }); }); - describe('"drop"', function () { - it('should call "drop" with $data and $event', function () { - scope.onDrop = sinon.spy(); + describe('handlers', function() { + var expectedData = { + 'json/image': {foo: 'bar'}, + 'json': {foo: 'bar'}, + 'text/uri-list': 'http://dragdrop.com', + 'text': 'http://dragdrop.com' + }; + + describe('"drag-enter"', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); + + dispatchEvent(element, dragEnterEvent); + }); + + it('should be called on dragenter when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); + + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(dragEnterEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal(expectedData); + }); + }); + + describe('"drag-leave"', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); + + dispatchEvent(element, dragLeaveEvent); + }); + + it('should be called on dragleave when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); + + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(dragLeaveEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal(expectedData); + }); + }); + + describe('"drag-over"', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); + + dispatchEvent(element, dragOverEvent); + }); + + it('should be called on dragover when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); + + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(dragOverEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal(expectedData); + }); + }); - var tpl = '
'; - var element = $compile(tpl)(scope); + describe('"drop"', function() { + beforeEach(function() { + $scope.onDrag = sinon.spy(); + var tpl = '
'; + createElement(tpl); - drop(element); + dispatchEvent(element, dropEvent); + }); + + it('should be called on drop when present', function() { + expect($scope.onDrag).to.have.been.calledOnce; + }); - expect(scope.onDrop).to.be.calledWith({ - 'json/image': {foo: 'bar'}, - 'json': {foo: 'bar'}, - 'text/uri-list': 'http://dragdrop.com', - 'text': 'http://dragdrop.com' - }, dropEvent); + it('should provide correct $event and $data', function() { + expect($scope.onDrag.firstCall.args[0], '$event').to.equal(dropEvent); + expect($scope.onDrag.firstCall.args[1], '$data').to.deep.equal(expectedData); + }); }); }); + + describe('throttling', function() { + // https://github.com/lemonde/angular-draganddrop/issues/2 + it.skip('should limit the calls to the drag-over callback'); + }); + + describe('automatic handling of HTML5 API quirks', function() { + // https://github.com/lemonde/angular-draganddrop/issues/2 + it.skip('should call preventDefault() on selected events'); + }); + });