diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c04ce1..2078216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# 2.1.0 (2017-01-15) + +## Changes + +- **Custom callbacks with dnd-callback**: The new `dnd-callback` attribute allows for communication between the source and target scopes. For example, this can be used to access information about the object being dragged during dragover, or to transfer objects without serialization, which allows to use complex objects that contain functions references and prototypes. [Demo](https://jsfiddle.net/Ldxffyod/1/) +- **Drop effects fixed and extended**: Most of the bugs around drop effect have been fixed. You can now use move, copy and link, or a combination of them. Drop effects can be restricted using `dnd-effect-allowed` on both the source and target element, and if there are multiple options, the user can choose one using modifier keys (Ctrl or Alt). See the [design document](https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Drop-Effects-Design) for more details. Drop effects don't work on IE9. They do work accross browser tabs if `dnd-external-sources` is activated, although the source restrictions are lost in Safari and IE. +- **New dragleave handler**: Previously, the dragleave handler used a timeout to determine whether the placeholder needs to be removed from a `dnd-list`. The new implementation utilizes `document.elementFromPoint` to determine whether the mouse cursor is outside the target list. +- **Remove dndDraggingSource without timeout**: Fixes problems with ngAnimate (#121). + +## Tested browsers + +- Chrome 55 (Mac, Ubuntu & Windows 7) +- Firefox 50 (Ubuntu) +- Safari 10 (MacOS) +- Microsoft Edge 20 (Windows 10 emulator) +- Internet Explorer 11 (Windows 7) +- Internet Explorer 9 (Windows 7 emulator) + + # 2.0.0 (2016-12-25) ## Changes @@ -5,7 +24,7 @@ There have been some major changes to how the directive works internally, although these changes should not affect users of the library. - **Simpler placeholder positioning algorithm**: The logic for placeholder positiong is unchanged, i.e. the placeholder will be placed after an element if the mouse cursor is in the second half of the element it is hovering over, otherwise it is placed before it. However, the implementation of this algorithm was massively simplified by using `getBoundingClientRect`. As a result, developers are no longer required to have `position: relative` on the list and list item elements. -- **New dataTransfer algorithm**: The directive now uses custom mime types in modern browsers, and falls back to using `Text` in non-standard comform browsers. As a result, dragged elements can no longer be dropped into arbitrary input fields. More details on how this works can be found in the [design document](https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design). +- **New dataTransfer algorithm**: The directive now uses custom mime types in modern browsers, and falls back to using `Text` in non-standard comform browsers. As a result, dragged elements can no longer be dropped into arbitrary input fields. More details on how this works can be found in the [design document](https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design). **Breaking change:** As mime types are used, all dnd-type attributes are automatically converted to lower case. - **Internal test infrastructure**: The mocks used for drag and drop events in unit tests are now much nicer. ## Tested browsers diff --git a/README.md b/README.md index ff26785..426f172 100644 --- a/README.md +++ b/README.md @@ -26,24 +26,19 @@ Use the dnd-draggable directive to make your element draggable **Attributes** * `dnd-draggable` Required attribute. The value has to be an object that represents the data of the element. In case of a drag and drop operation the object will be serialized and unserialized on the receiving end. -* `dnd-effect-allowed` Use this attribute to limit the operations that can be performed. Options are: - * `move` The drag operation will move the element. This is the default - * `copy` The drag operation will copy the element. There will be a copy cursor. - * `copyMove` The user can choose between copy and move by pressing the ctrl or shift key. - * *Not supported in IE:* In Internet Explorer this option will be the same as `copy`. - * *Not fully supported in Chrome on Windows:* In the Windows version of Chrome the cursor will always be the move cursor. However, when the user drops an element and has the ctrl key pressed, we will perform a copy anyways. - * HTML5 also specifies the `link` option, but this library does not actively support it yet, so use it at your own risk. - * [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) +* `dnd-effect-allowed` Use this attribute to limit the operations that can be performed. Valid options are `move`, `copy` and `link`, as well as `all`, `copyMove`, `copyLink` and `linkMove`, while `move` is the default value. The semantics of these operations are up to you and have to be implemented using the callbacks described below. If you allow multiple options, the user can choose between them by using the modifier keys (OS specific). The cursor will be changed accordingly, expect for IE and Edge, where this is not supported. Note that the implementation of this attribute is very buggy in IE9. This attribute works together with `dnd-external-sources` except on Safari and IE, where the restriction will be lost when dragging accross browser tabs. [Design document](https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Drop-Effects-Design) [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-type` Use this attribute if you have different kinds of items in your application and you want to limit which items can be dropped into which lists. Combine with dnd-allowed-types on the dnd-list(s). This attribute must be a lower case string. Upper case characters can be used, but will be converted to lower case automatically. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) * `dnd-disable-if` You can use this attribute to dynamically disable the draggability of the element. This is useful if you have certain list items that you don't want to be draggable, or if you want to disable drag & drop completely without having two different code branches (e.g. only allow for admins). [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) **Callbacks** +* `dnd-dragstart` Callback that is invoked when the element was dragged. The original dragstart event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-moved` Callback that is invoked when the element was moved. Usually you will remove your element from the original list in this callback, since the directive is not doing that for you automatically. The original dragend event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-copied` Same as dnd-moved, just that it is called when the element was copied instead of moved. The original dragend event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) +* `dnd-linked` Same as dnd-moved, just that it is called when the element was linked instead of moved. The original dragend event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-canceled` Callback that is invoked if the element was dragged, but the operation was canceled and the element was not dropped. The original dragend event will be provided in the local event variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) -* `dnd-dragstart` Callback that is invoked when the element was dragged. The original dragstart event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-dragend` Callback that is invoked when the drag operation ended. Available local variables are `event` and `dropEffect`. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-selected` Callback that is invoked when the element was clicked but not dragged. The original click event will be provided in the local `event` variable. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/nested) +* `dnd-callback` Custom callback that is passed to dropzone callbacks and can be used to communicate between source and target scopes. The dropzone can pass user defined variables to this callback. This can be used to transfer objects without serialization, see [Demo](https://jsfiddle.net/Ldxffyod/1/). **CSS classes** * `dndDragging` This class will be added to the element while the element is being dragged. It will affect both the element you see while dragging and the source element that stays at it's position. Do not try to hide the source element with this class, because that will abort the drag operation. @@ -56,6 +51,7 @@ Use the dnd-list attribute to make your list element a dropzone. Usually you wil **Attributes** * `dnd-list` Required attribute. The value has to be the array in which the data of the dropped element should be inserted. The value can be blank if used with a custom dnd-drop handler that handles the insertion on its own. * `dnd-allowed-types` Optional array of allowed item types. When used, only items that had a matching dnd-type attribute will be dropable. Upper case characters will automatically be converted to lower case. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) +* `dnd-effect-allowed` Optional string expression that limits the drop effects that can be performed on the list. See dnd-effect-allowed on dnd-draggable for more details on allowed options. The default value is `all`. * `dnd-disable-if` Optional boolean expression. When it evaluates to true, no dropping into the list is possible. Note that this also disables rearranging items inside the list. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) * `dnd-horizontal-list` Optional boolean expression. When it evaluates to true, the positioning algorithm will use the left and right halfs of the list items instead of the upper and lower halfs. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-external-sources` Optional boolean expression. When it evaluates to true, the list accepts drops from sources outside of the current browser tab, which allows to drag and drop accross different browser tabs. The only major browser for which this is currently not working is Microsoft Edge. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) @@ -66,6 +62,8 @@ Use the dnd-list attribute to make your list element a dropzone. Usually you wil * `index` The position in the list at which the element would be dropped. * `type` The `dnd-type` set on the dnd-draggable, or undefined if unset. Will be null for drops from external sources in IE and Edge, since we don't know the type in those cases. * `external` Whether the element was dragged from an external source. See `dnd-external-sources`. + * `dropEffect` The dropEffect that is going to be performed, see dnd-effect-allowed. + * `callback` If dnd-callback was set on the source element, this is a function reference to the callback. The callback can be invoked with custom variables like this: `callback({var1: value1, var2: value2})`. The callback will be executed on the scope of the source element. If dnd-external-sources was set and external is true, this callback will not be available. * [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-drop` Optional expression that is invoked when an element is dropped on the list. The same variables as for dnd-dragover will be available, with the exception that type is always known and therefore never null. There will also be an `item` variable, which is the transferred object. The return value determines the further handling of the drop: * `falsy` The drop will be canceled and the element won't be inserted. @@ -120,7 +118,7 @@ If this doesn't fit your requirements, check out one of the other awesome drag & Copyright (c) 2014 [Marcel Juenemann](mailto:marcel@juenemann.cc) -Copyright (c) 2014-2016 Google Inc. +Copyright (c) 2014-2017 Google Inc. This is not an official Google product (experimental or otherwise), it is just code that happens to be owned by Google. diff --git a/angular-drag-and-drop-lists.js b/angular-drag-and-drop-lists.js index c835748..8a07ce3 100644 --- a/angular-drag-and-drop-lists.js +++ b/angular-drag-and-drop-lists.js @@ -1,8 +1,8 @@ /** - * angular-drag-and-drop-lists v2.0.0 + * angular-drag-and-drop-lists v2.1.0 * * Copyright (c) 2014 Marcel Juenemann marcel@juenemann.cc - * Copyright (c) 2014-2016 Google Inc. + * Copyright (c) 2014-2017 Google Inc. * https://github.com/marceljuenemann/angular-drag-and-drop-lists * * License: MIT @@ -16,6 +16,9 @@ var EDGE_MIME_TYPE = 'application/json'; var MSIE_MIME_TYPE = 'Text'; + // All valid HTML5 drop effects, in the order in which we prefer to use them. + var ALL_EFFECTS = ['move', 'copy', 'link']; + /** * Use the dnd-draggable attribute to make your element draggable * @@ -23,33 +26,13 @@ * - dnd-draggable Required attribute. The value has to be an object that represents the data * of the element. In case of a drag and drop operation the object will be * serialized and unserialized on the receiving end. - * - dnd-selected Callback that is invoked when the element was clicked but not dragged. - * The original click event will be provided in the local event variable. - * - dnd-effect-allowed Use this attribute to limit the operations that can be performed. Options: - * - "move": The drag operation will move the element. This is the default. - * - "copy": The drag operation will copy the element. Shows a copy cursor. - * - "copyMove": The user can choose between copy and move by pressing the - * ctrl or shift key. *Not supported in IE:* In Internet Explorer this - * option will be the same as "copy". *Not fully supported in Chrome on - * Windows:* In the Windows version of Chrome the cursor will always be the - * move cursor. However, when the user drops an element and has the ctrl - * key pressed, we will perform a copy anyways. - * - HTML5 also specifies the "link" option, but this library does not - * actively support it yet, so use it at your own risk. - * - dnd-moved Callback that is invoked when the element was moved. Usually you will - * remove your element from the original list in this callback, since the - * directive is not doing that for you automatically. The original dragend - * event will be provided in the local event variable. - * - dnd-canceled Callback that is invoked if the element was dragged, but the operation was - * canceled and the element was not dropped. The original dragend event will - * be provided in the local event variable. - * - dnd-copied Same as dnd-moved, just that it is called when the element was copied - * instead of moved. The original dragend event will be provided in the local - * event variable. - * - dnd-dragstart Callback that is invoked when the element was dragged. The original - * dragstart event will be provided in the local event variable. - * - dnd-dragend Callback that is invoked when the drag operation ended. Available local - * variables are event and dropEffect. + * - dnd-effect-allowed Use this attribute to limit the operations that can be performed. Valid + * options are "move", "copy" and "link", as well as "all", "copyMove", + * "copyLink" and "linkMove". The semantics of these operations are up to you + * and have to be implemented using the callbacks described below. If you + * allow multiple options, the user can choose between them by using the + * modifier keys (OS specific). The cursor will be changed accordingly, + * expect for IE and Edge, where this is not supported. * - dnd-type Use this attribute if you have different kinds of items in your * application and you want to limit which items can be dropped into which * lists. Combine with dnd-allowed-types on the dnd-list(s). This attribute @@ -60,6 +43,28 @@ * to be draggable, or if you want to disable drag & drop completely without * having two different code branches (e.g. only allow for admins). * + * Callbacks: + * - dnd-dragstart Callback that is invoked when the element was dragged. The original + * dragstart event will be provided in the local event variable. + * - dnd-moved Callback that is invoked when the element was moved. Usually you will + * remove your element from the original list in this callback, since the + * directive is not doing that for you automatically. The original dragend + * event will be provided in the local event variable. + * - dnd-copied Same as dnd-moved, just that it is called when the element was copied + * instead of moved, so you probably want to implement a different logic. + * - dnd-linked Same as dnd-moved, just that it is called when the element was linked + * instead of moved, so you probably want to implement a different logic. + * - dnd-canceled Callback that is invoked if the element was dragged, but the operation was + * canceled and the element was not dropped. The original dragend event will + * be provided in the local event variable. + * - dnd-dragend Callback that is invoked when the drag operation ended. Available local + * variables are event and dropEffect. + * - dnd-selected Callback that is invoked when the element was clicked but not dragged. + * The original click event will be provided in the local event variable. + * - dnd-callback Custom callback that is passed to dropzone callbacks and can be used to + * communicate between source and target scopes. The dropzone can pass user + * defined variables to this callback. + * * CSS classes: * - dndDragging This class will be added to the element while the element is being * dragged. It will affect both the element you see while dragging and the @@ -93,10 +98,14 @@ if (element.attr('draggable') == 'false') return true; // Initialize global state. - dndState.dropEffect = "none"; dndState.isDragging = true; dndState.itemType = attr.dndType && scope.$eval(attr.dndType).toLowerCase(); + // Set the allowed drop effects. See below for special IE handling. + dndState.dropEffect = "none"; + dndState.effectAllowed = attr.dndEffectAllowed || ALL_EFFECTS[0]; + event.dataTransfer.effectAllowed = dndState.effectAllowed; + // Internet Explorer and Microsoft Edge don't support custom mime types, see design doc: // https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design var item = scope.$eval(attr.dndDraggable); @@ -104,17 +113,20 @@ try { event.dataTransfer.setData(mimeType, angular.toJson(item)); } catch (e) { + // Setting a custom MIME type did not work, we are probably in IE or Edge. var data = angular.toJson({item: item, type: dndState.itemType}); try { event.dataTransfer.setData(EDGE_MIME_TYPE, data); } catch (e) { + // We are in Internet Explorer and can only use the Text MIME type. Also note that IE + // does not allow changing the cursor in the dragover event, therefore we have to choose + // the one we want to display now by setting effectAllowed. + var effectsAllowed = filterEffects(ALL_EFFECTS, dndState.effectAllowed); + event.dataTransfer.effectAllowed = effectsAllowed[0]; event.dataTransfer.setData(MSIE_MIME_TYPE, data); } } - // Only allow actions specified in dnd-effect-allowed attribute. - event.dataTransfer.effectAllowed = attr.dndEffectAllowed || "move"; - // Add CSS classes. See documentation above. element.addClass("dndDragging"); $timeout(function() { element.addClass("dndDraggingSource"); }, 0); @@ -124,7 +136,13 @@ event.dataTransfer.setDragImage(element[0], 0, 0); } + // Invoke dragstart callback and prepare extra callback for dropzone. $parse(attr.dndDragstart)(scope, {event: event}); + if (attr.dndCallback) { + var callback = $parse(attr.dndCallback); + dndState.callback = function(params) { return callback(scope, params || {}); }; + } + event.stopPropagation(); }); @@ -140,27 +158,22 @@ // the used effect, but Chrome has not implemented that field correctly. On Windows // it always sets it to 'none', while Chrome on Linux sometimes sets it to something // else when it's supposed to send 'none' (drag operation aborted). - var dropEffect = dndState.dropEffect; scope.$apply(function() { - switch (dropEffect) { - case "move": - $parse(attr.dndMoved)(scope, {event: event}); - break; - case "copy": - $parse(attr.dndCopied)(scope, {event: event}); - break; - case "none": - $parse(attr.dndCanceled)(scope, {event: event}); - break; - } + var dropEffect = dndState.dropEffect; + var cb = {copy: 'dndCopied', link: 'dndLinked', move: 'dndMoved', none: 'dndCanceled'}; + $parse(attr[cb[dropEffect]])(scope, {event: event}); $parse(attr.dndDragend)(scope, {event: event, dropEffect: dropEffect}); }); // Clean up - element.removeClass("dndDragging"); - $timeout(function() { element.removeClass("dndDraggingSource"); }, 0); dndState.isDragging = false; + dndState.callback = undefined; + element.removeClass("dndDragging"); + element.removeClass("dndDraggingSource"); event.stopPropagation(); + + // In IE9 it is possible that the timeout from dragstart triggers after the dragend handler. + $timeout(function() { element.removeClass("dndDraggingSource"); }, 0); }); /** @@ -201,12 +214,21 @@ * - dnd-allowed-types Optional array of allowed item types. When used, only items that had a * matching dnd-type attribute will be dropable. Upper case characters will * automatically be converted to lower case. + * - dnd-effect-allowed Optional string expression that limits the drop effects that can be + * performed in the list. See dnd-effect-allowed on dnd-draggable for more + * details on allowed options. The default value is all. * - dnd-disable-if Optional boolean expresssion. When it evaluates to true, no dropping * into the list is possible. Note that this also disables rearranging * items inside the list. * - dnd-horizontal-list Optional boolean expresssion. When it evaluates to true, the positioning * algorithm will use the left and right halfs of the list items instead of * the upper and lower halfs. + * - dnd-external-sources Optional boolean expression. When it evaluates to true, the list accepts + * drops from sources outside of the current browser tab. This allows to + * drag and drop accross different browser tabs. The only major browser + * that does not support this is currently Microsoft Edge. + * + * Callbacks: * - dnd-dragover Optional expression that is invoked when an element is dragged over the * list. If the expression is set, but does not return true, the element is * not allowed to be dropped. The following variables will be available: @@ -215,7 +237,14 @@ * - type: The dnd-type set on the dnd-draggable, or undefined if non was * set. Will be null for drops from external sources in IE and Edge, * since we don't know the type in those cases. + * - dropEffect: One of move, copy or link, see dnd-effect-allowed. * - external: Whether the element was dragged from an external source. + * - callback: If dnd-callback was set on the source element, this is a + * function reference to the callback. The callback can be invoked with + * custom variables like this: callback({var1: value1, var2: value2}). + * The callback will be executed on the scope of the source element. If + * dnd-external-sources was set and external is true, this callback will + * not be available. * - dnd-drop Optional expression that is invoked when an element is dropped on the * list. The same variables as for dnd-dragover will be available, with the * exception that type is always known and therefore never null. There @@ -232,10 +261,6 @@ * dnd-drop will be available. Note that for reorderings inside the same * list the old element will still be in the list due to the fact that * dnd-moved was not called yet. - * - dnd-external-sources Optional boolean expression. When it evaluates to true, the list accepts - * drops from sources outside of the current browser tab. This allows to - * drag and drop accross different browser tabs. The only major browser - * that does not support this is currently Microsoft Edge. * * CSS classes: * - dndPlaceholder When an element is dragged over the list, a new placeholder child @@ -244,7 +269,7 @@ * by creating a child element with dndPlaceholder class. * - dndDragover Will be added to the list while an element is dragged over the list. */ - dndLists.directive('dndList', ['$parse', '$timeout', function($parse, $timeout) { + dndLists.directive('dndList', ['$parse', function($parse) { return function(scope, element, attr) { // While an element is dragged over the list, this placeholder element is inserted // at the location where the element would be inserted after dropping. @@ -316,14 +341,27 @@ } } + // In IE we set a fake effectAllowed in dragstart to get the correct cursor, we therefore + // ignore the effectAllowed passed in dataTransfer. We must also not access dataTransfer for + // drops from external sources, as that throws an exception. + var ignoreDataTransfer = mimeType == MSIE_MIME_TYPE; + var dropEffect = getDropEffect(event, ignoreDataTransfer); + if (dropEffect == 'none') return stopDragover(); + // At this point we invoke the callback, which still can disallow the drop. // We can't do this earlier because we want to pass the index of the placeholder. - if (attr.dndDragover && !invokeCallback(attr.dndDragover, event, itemType)) { + if (attr.dndDragover && !invokeCallback(attr.dndDragover, event, dropEffect, itemType)) { return stopDragover(); } - element.addClass("dndDragover"); + // Set dropEffect to modify the cursor shown by the browser, unless we're in IE, where this + // is not supported. This must be done after preventDefault in Firefox. event.preventDefault(); + if (!ignoreDataTransfer) { + event.dataTransfer.dropEffect = dropEffect; + } + + element.addClass("dndDragover"); event.stopPropagation(); return false; }); @@ -359,33 +397,31 @@ if (!isDropAllowed(itemType)) return stopDragover(); } + // Special handling for internal IE drops, see dragover handler. + var ignoreDataTransfer = mimeType == MSIE_MIME_TYPE; + var dropEffect = getDropEffect(event, ignoreDataTransfer); + if (dropEffect == 'none') return stopDragover(); + // Invoke the callback, which can transform the transferredObject and even abort the drop. var index = getPlaceholderIndex(); if (attr.dndDrop) { - data = invokeCallback(attr.dndDrop, event, itemType, index, data); + data = invokeCallback(attr.dndDrop, event, dropEffect, itemType, index, data); if (!data) return stopDragover(); } + // The drop is definitely going to happen now, store the dropEffect. + dndState.dropEffect = dropEffect; + if (!ignoreDataTransfer) { + event.dataTransfer.dropEffect = dropEffect; + } + // Insert the object into the array, unless dnd-drop took care of that (returned true). if (data !== true) { scope.$apply(function() { scope.$eval(attr.dndList).splice(index, 0, data); }); } - invokeCallback(attr.dndInserted, event, itemType, index, data); - - // In Chrome on Windows the dropEffect will always be none... - // We have to determine the actual effect manually from the allowed effects - if (event.dataTransfer.dropEffect === "none") { - if (event.dataTransfer.effectAllowed === "copy" || - event.dataTransfer.effectAllowed === "move") { - dndState.dropEffect = event.dataTransfer.effectAllowed; - } else { - dndState.dropEffect = event.ctrlKey ? "copy" : "move"; - } - } else { - dndState.dropEffect = event.dataTransfer.dropEffect; - } + invokeCallback(attr.dndInserted, event, dropEffect, itemType, index, data); // Clean up stopDragover(); @@ -396,20 +432,19 @@ /** * We have to remove the placeholder when the element is no longer dragged over our list. The * problem is that the dragleave event is not only fired when the element leaves our list, - * but also when it leaves a child element -- so practically it's fired all the time. As a - * workaround we wait a few milliseconds and then check if the dndDragover class was added - * again. If it is there, dragover must have been called in the meantime, i.e. the element - * is still dragging over the list. If you know a better way of doing this, please tell me! + * but also when it leaves a child element. Therefore, we determine whether the mouse cursor + * is still pointing to an element inside the list or not. */ element.on('dragleave', function(event) { event = event.originalEvent || event; - element.removeClass("dndDragover"); - $timeout(function() { - if (!element.hasClass("dndDragover")) { - placeholder.remove(); - } - }, 100); + var newTarget = document.elementFromPoint(event.clientX, event.clientY); + if (listNode.contains(newTarget) && !event._dndPhShown) { + // Signalize to potential parent lists that a placeholder is already shown. + event._dndPhShown = true; + } else { + stopDragover(); + } }); /** @@ -449,6 +484,36 @@ return itemType && listSettings.allowedTypes.indexOf(itemType) != -1; } + /** + * Determines which drop effect to use for the given event. In Internet Explorer we have to + * ignore the effectAllowed field on dataTransfer, since we set a fake value in dragstart. + * In those cases we rely on dndState to filter effects. Read the design doc for more details: + * https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Data-Transfer-Design + */ + function getDropEffect(event, ignoreDataTransfer) { + var effects = ALL_EFFECTS; + if (!ignoreDataTransfer) { + effects = filterEffects(effects, event.dataTransfer.effectAllowed); + } + if (dndState.isDragging) { + effects = filterEffects(effects, dndState.effectAllowed); + } + if (attr.dndEffectAllowed) { + effects = filterEffects(effects, attr.dndEffectAllowed); + } + // MacOS automatically filters dataTransfer.effectAllowed depending on the modifier keys, + // therefore the following modifier keys will only affect other operating systems. + if (!effects.length) { + return 'none'; + } else if (event.ctrlKey && effects.indexOf('copy') != -1) { + return 'copy'; + } else if (event.altKey && effects.indexOf('link') != -1) { + return 'link'; + } else { + return effects[0]; + } + } + /** * Small helper function that cleans up if we aborted a drop. */ @@ -461,12 +526,14 @@ /** * Invokes a callback with some interesting parameters and returns the callbacks return value. */ - function invokeCallback(expression, event, itemType, index, item) { + function invokeCallback(expression, event, dropEffect, itemType, index, item) { return $parse(expression)(scope, { + callback: dndState.callback, + dropEffect: dropEffect, event: event, + external: !dndState.isDragging, index: index !== undefined ? index : getPlaceholderIndex(), item: item || undefined, - external: !dndState.isDragging, type: itemType }); } @@ -556,11 +623,24 @@ }; }); + /** + * Filters an array of drop effects using a HTML5 effectAllowed string. + */ + function filterEffects(effects, effectAllowed) { + if (effectAllowed == 'all') return effects; + return effects.filter(function(effect) { + return effectAllowed.toLowerCase().indexOf(effect) != -1; + }); + } + /** * For some features we need to maintain global state. This is done here, with these fields: + * - callback: A callback function set at dragstart that is passed to internal dropzone handlers. * - dropEffect: Set in dragstart to "none" and to the actual value in the drop handler. We don't * rely on the dropEffect passed by the browser, since there are various bugs in Chrome and * Safari, and Internet Explorer defaults to copy if effectAllowed is copyMove. + * - effectAllowed: Set in dragstart based on dnd-effect-allowed. This is needed for IE because + * setting effectAllowed on dataTransfer might result in an undesired cursor. * - isDragging: True between dragstart and dragend. Falsy for drops from external sources. * - itemType: The item type of the dragged element set via dnd-type. This is needed because IE * and Edge don't support custom mime types that we can use to transfer this information. diff --git a/angular-drag-and-drop-lists.min.js b/angular-drag-and-drop-lists.min.js index 9e360b3..0d957a6 100644 --- a/angular-drag-and-drop-lists.min.js +++ b/angular-drag-and-drop-lists.min.js @@ -1,46 +1,49 @@ /** - * angular-drag-and-drop-lists v2.0.0 + * angular-drag-and-drop-lists v2.1.0 * * Copyright (c) 2014 Marcel Juenemann marcel@juenemann.cc - * Copyright (c) 2014-2016 Google Inc. + * Copyright (c) 2014-2017 Google Inc. * https://github.com/marceljuenemann/angular-drag-and-drop-lists * * License: MIT */ -!function(e){var n="application/x-dnd",a="application/json",r="Text" -e.directive("dndDraggable",["$parse","$timeout",function(e,d){return function(o,i,l){i.attr("draggable","true"),l.dndDisableIf&&o.$watch(l.dndDisableIf,function(e){i.attr("draggable",!e)}),i.on("dragstart",function(s){if(s=s.originalEvent||s,"false"==i.attr("draggable"))return!0 -t.dropEffect="none",t.isDragging=!0,t.itemType=l.dndType&&o.$eval(l.dndType).toLowerCase() -var f=o.$eval(l.dndDraggable),g=n+(t.itemType?"-"+t.itemType:"") -try{s.dataTransfer.setData(g,angular.toJson(f))}catch(c){var u=angular.toJson({item:f,type:t.itemType}) -try{s.dataTransfer.setData(a,u)}catch(c){s.dataTransfer.setData(r,u)}}s.dataTransfer.effectAllowed=l.dndEffectAllowed||"move",i.addClass("dndDragging"),d(function(){i.addClass("dndDraggingSource")},0),s._dndHandle&&s.dataTransfer.setDragImage&&s.dataTransfer.setDragImage(i[0],0,0),e(l.dndDragstart)(o,{event:s}),s.stopPropagation()}),i.on("dragend",function(n){n=n.originalEvent||n -var a=t.dropEffect -o.$apply(function(){switch(a){case"move":e(l.dndMoved)(o,{event:n}) -break -case"copy":e(l.dndCopied)(o,{event:n}) -break -case"none":e(l.dndCanceled)(o,{event:n})}e(l.dndDragend)(o,{event:n,dropEffect:a})}),i.removeClass("dndDragging"),d(function(){i.removeClass("dndDraggingSource")},0),t.isDragging=!1,n.stopPropagation()}),i.on("click",function(n){l.dndSelected&&(n=n.originalEvent||n,o.$apply(function(){e(l.dndSelected)(o,{event:n})}),n.stopPropagation())}),i.on("selectstart",function(){this.dragDrop&&this.dragDrop()})}}]),e.directive("dndList",["$parse","$timeout",function(e,d){return function(o,i,l){function s(e){if(!e)return r -for(var t=0;t")}var D=v() -D.remove() -var y=D[0],T=i[0],m={} -i.on("dragenter",function(e){e=e.originalEvent||e -var n=l.dndAllowedTypes&&o.$eval(l.dndAllowedTypes) -m={allowedTypes:angular.isArray(n)&&n.join("|").toLowerCase().split("|"),disabled:l.dndDisableIf&&o.$eval(l.dndDisableIf),externalSources:l.dndExternalSources&&o.$eval(l.dndExternalSources),horizontal:l.dndHorizontalList&&o.$eval(l.dndHorizontalList)} -var a=s(e.dataTransfer.types) -return a&&g(f(a))?void e.preventDefault():!0}),i.on("dragover",function(e){e=e.originalEvent||e -var n=s(e.dataTransfer.types),a=f(n) +!function(e){function n(e,n){return"all"==n?e:e.filter(function(e){return-1!=n.toLowerCase().indexOf(e)})}var a="application/x-dnd",r="application/json",t="Text",d=["move","copy","link"] +e.directive("dndDraggable",["$parse","$timeout",function(e,i){return function(l,f,c){f.attr("draggable","true"),c.dndDisableIf&&l.$watch(c.dndDisableIf,function(e){f.attr("draggable",!e)}),f.on("dragstart",function(s){if(s=s.originalEvent||s,"false"==f.attr("draggable"))return!0 +o.isDragging=!0,o.itemType=c.dndType&&l.$eval(c.dndType).toLowerCase(),o.dropEffect="none",o.effectAllowed=c.dndEffectAllowed||d[0],s.dataTransfer.effectAllowed=o.effectAllowed +var g=l.$eval(c.dndDraggable),u=a+(o.itemType?"-"+o.itemType:"") +try{s.dataTransfer.setData(u,angular.toJson(g))}catch(p){var v=angular.toJson({item:g,type:o.itemType}) +try{s.dataTransfer.setData(r,v)}catch(p){var D=n(d,o.effectAllowed) +s.dataTransfer.effectAllowed=D[0],s.dataTransfer.setData(t,v)}}if(f.addClass("dndDragging"),i(function(){f.addClass("dndDraggingSource")},0),s._dndHandle&&s.dataTransfer.setDragImage&&s.dataTransfer.setDragImage(f[0],0,0),e(c.dndDragstart)(l,{event:s}),c.dndCallback){var y=e(c.dndCallback) +o.callback=function(e){return y(l,e||{})}}s.stopPropagation()}),f.on("dragend",function(n){n=n.originalEvent||n,l.$apply(function(){var a=o.dropEffect,r={copy:"dndCopied",link:"dndLinked",move:"dndMoved",none:"dndCanceled"} +e(c[r[a]])(l,{event:n}),e(c.dndDragend)(l,{event:n,dropEffect:a})}),o.isDragging=!1,o.callback=void 0,f.removeClass("dndDragging"),f.removeClass("dndDraggingSource"),n.stopPropagation(),i(function(){f.removeClass("dndDraggingSource")},0)}),f.on("click",function(n){c.dndSelected&&(n=n.originalEvent||n,l.$apply(function(){e(c.dndSelected)(l,{event:n})}),n.stopPropagation())}),f.on("selectstart",function(){this.dragDrop&&this.dragDrop()})}}]),e.directive("dndList",["$parse",function(e){return function(i,l,f){function c(e){if(!e)return t +for(var n=0;n")}var T=y() +T.remove() +var h=T[0],m=l[0],E={} +l.on("dragenter",function(e){e=e.originalEvent||e +var n=f.dndAllowedTypes&&i.$eval(f.dndAllowedTypes) +E={allowedTypes:angular.isArray(n)&&n.join("|").toLowerCase().split("|"),disabled:f.dndDisableIf&&i.$eval(f.dndDisableIf),externalSources:f.dndExternalSources&&i.$eval(f.dndExternalSources),horizontal:f.dndHorizontalList&&i.$eval(f.dndHorizontalList)} +var a=c(e.dataTransfer.types) +return a&&g(s(a))?void e.preventDefault():!0}),l.on("dragover",function(e){e=e.originalEvent||e +var n=c(e.dataTransfer.types),a=s(n) +if(!n||!g(a))return!0 +if(h.parentNode!=m&&l.append(T),e.target!=m){for(var r=e.target;r.parentNode!=m&&r.parentNode;)r=r.parentNode +if(r.parentNode==m&&r!=h){var d=r.getBoundingClientRect() +if(E.horizontal)var o=e.clientX" diff --git a/demo/advanced/advanced-frame.html b/demo/advanced/advanced-frame.html index e37bbed..366cc57 100644 --- a/demo/advanced/advanced-frame.html +++ b/demo/advanced/advanced-frame.html @@ -1,22 +1,23 @@

Demo: Advanced Features

    -
  • Callbacks: The directives offer various callbacks, which in this example will log the events to the console. - Additionally, the callbacks on the dnd-list can prevent an element from being dropped. In this example you can't drop elements - after the 10th position, because we are preventing that in the dnd-dragover callback.
  • +
  • dnd-effect-allowed: This demo shows how to use dnd-effect-allowed to control which drop effects are allowed. + If the source and target elements have no drop effect that is allowed on both, then a drop is not possible. If there are multiple + possible drop effects, then the user can control the drop effect using modifier keys (Ctrl and Alt).
  • +
  • dnd-external-sources: Allows to drag and drop elements accross browser windows, which you can test in this + example. The downside to this is that the lists will accept arbitrary text to be dropped. To prevent that, the dnd-drop callback + verifies that the dropped element is of the desired format.
  • dnd-allowed-types in nested lists: We are using the dnd-allowed-types attribute to ensure that Containers only accept items, but not other containers.
  • dnd-horizontal-list: This attribute tells the positioning algorithm to drop incoming elements left or right of the existing elements, instead of above or below.
  • -
  • dnd-external-sources: Allows to drag and drop elements accross browser windows, which you can test in this - example. The downside to this is that the lists will accept arbitrary text to be dropped. To prevent that, the dnd-drop callback - verifies that the dropped element is of the desired format.
  • -
  • dnd-effect-allowed: This attribute is set to 'copyMove' in this example, which means that the user can choose - whether to copy or move an element by holding down the Ctrl key. Note that this doesn't work in Chrome very well.
  • +
  • Callbacks: The directives offer various callbacks, which in this example will log the events to the console. + Additionally, the callbacks on the dnd-list can prevent an element from being dropped. In this example you can't drop elements + after the 10th position, because we are preventing that in the dnd-dragover callback.
-
+
diff --git a/demo/advanced/advanced.css b/demo/advanced/advanced.css index 7dc50a4..2f53458 100644 --- a/demo/advanced/advanced.css +++ b/demo/advanced/advanced.css @@ -25,11 +25,10 @@ /** * The dndDraggingSource class will be applied to the source element of a drag - * operation. It makes sense to hide it to give the user the feeling that he's - * actually moving it. Note that the source element has also .dndDragging class. + * operation. */ .advancedDemo .dropzone .dndDraggingSource { - display: none; + opacity: 0.5; } /** @@ -59,7 +58,7 @@ margin: 5px; padding: 3px; text-align: center; - width: 50px; + width: 80px; } .advancedDemo .dropzone .container-element { diff --git a/demo/advanced/advanced.html b/demo/advanced/advanced.html index 7890331..856ee11 100644 --- a/demo/advanced/advanced.html +++ b/demo/advanced/advanced.html @@ -1,33 +1,33 @@

Dropzone {{$index + 1}}

    -
  • +
  • + dnd-moved="containers.splice($index, 1)" + dnd-callback="container.items.length">
    -

    Container

    -
      Container (effects allowed: {{container.effectAllowed}}) +
        -
      • + dnd-type="'item'" + dnd-effect-allowed="{{item.effectAllowed}}" + dnd-dragstart="logEvent('Started to drag an item')" + dnd-moved="container.items.splice($index, 1)" + dnd-dragend="logEvent('Drag operation ended. Drop effect: ' + dropEffect)"> {{item.label}}
      diff --git a/demo/advanced/advanced.js b/demo/advanced/advanced.js index 744b930..6f276c0 100644 --- a/demo/advanced/advanced.js +++ b/demo/advanced/advanced.js @@ -1,41 +1,40 @@ angular.module("demo").controller("AdvancedDemoController", function($scope) { - $scope.dragoverCallback = function(event, index, external, type) { - $scope.logListEvent('dragged over', event, index, external, type); - // Disallow dropping in the third row. Could also be done with dnd-disable-if. - return index < 10; + $scope.dragoverCallback = function(index, external, type, callback) { + $scope.logListEvent('dragged over', index, external, type); + // Invoke callback to origin for container types. + if (type == 'container' && !external) { + console.log('Container being dragged contains ' + callback() + ' items'); + } + return index < 10; // Disallow dropping in the third row. }; - $scope.dropCallback = function(event, index, item, external, type) { - $scope.logListEvent('dropped at', event, index, external, type); + $scope.dropCallback = function(index, item, external, type) { + $scope.logListEvent('dropped at', index, external, type); // Return false here to cancel drop. Return true if you insert the item yourself. return item; }; - $scope.logEvent = function(message, event) { - console.log(message, '(triggered by the following', event.type, 'event)'); - console.log(event); + $scope.logEvent = function(message) { + console.log(message); }; - $scope.logListEvent = function(action, event, index, external, type) { + $scope.logListEvent = function(action, index, external, type) { var message = external ? 'External ' : ''; - message += type + ' element is ' + action + ' position ' + index; - $scope.logEvent(message, event); + message += type + ' element was ' + action + ' position ' + index; + console.log(message); }; - $scope.model = []; - // Initialize model + $scope.model = [[], []]; var id = 10; - for (var i = 0; i < 3; ++i) { - $scope.model.push([]); - for (var j = 0; j < 2; ++j) { - $scope.model[i].push([]); - for (var k = 0; k < 7; ++k) { - $scope.model[i][j].push({label: 'Item ' + id++}); - } - } - } + angular.forEach(['all', 'move', 'copy', 'link', 'copyLink', 'copyMove'], function(effect, i) { + var container = {items: [], effectAllowed: effect}; + for (var k = 0; k < 7; ++k) { + container.items.push({label: effect + ' ' + id++, effectAllowed: effect}); + } + $scope.model[i % $scope.model.length].push(container); + }); $scope.$watch('model', function(model) { $scope.modelAsJson = angular.toJson(model, true); diff --git a/demo/types/types.html b/demo/types/types.html index de8a47c..6b3073e 100644 --- a/demo/types/types.html +++ b/demo/types/types.html @@ -7,7 +7,6 @@ dnd-type="person.type" dnd-disable-if="person.type == 'unknown'" dnd-moved="list.people.splice($index, 1)" - dnd-effect-allowed="move" class="background-{{person.type}}" > diff --git a/package.json b/package.json index 1c995ce..d979eaa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "angular-drag-and-drop-lists", "main": "angular-drag-and-drop-lists.js", - "version": "2.0.0", + "version": "2.1.0", "description": "Angular directives for sorting nested lists using the HTML5 Drag and Drop API", "repository": "https://github.com/marceljuenemann/angular-drag-and-drop-lists", "license": "MIT", diff --git a/test/dndDraggableSpec.js b/test/dndDraggableSpec.js index 95293bf..bcc5ba6 100644 --- a/test/dndDraggableSpec.js +++ b/test/dndDraggableSpec.js @@ -70,6 +70,11 @@ describe('dndDraggable', function() { expect(Dragstart.on(element).effectAllowed).toBe('copyMove'); }); + it('sets effectAllowed to single effect in IE', function() { + element = compileAndLink('
      '); + expect(Dragstart.on(element, {allowedMimeTypes: ['Text']}).effectAllowed).toBe('copy'); + }); + it('adds CSS classes to element', inject(function($timeout) { Dragstart.on(element); expect(element.hasClass('dndDragging')).toBe(true); @@ -118,30 +123,35 @@ describe('dndDraggable', function() { dragstart.dragend(element); expect(element.hasClass('dndDragging')).toBe(false); - expect(element.hasClass('dndDraggingSource')).toBe(true); + expect(element.hasClass('dndDraggingSource')).toBe(false); + })); + + it('removes dndDraggingSource after a timeout', inject(function($timeout) { + // IE 9 might not flush the $timeout before invoking the dragend handler. + expect(element.hasClass('dndDragging')).toBe(true); + expect(element.hasClass('dndDraggingSource')).toBe(false); + + dragstart.dragend(element); + expect(element.hasClass('dndDragging')).toBe(false); + expect(element.hasClass('dndDraggingSource')).toBe(false); $timeout.flush(0); + expect(element.hasClass('dndDragging')).toBe(false); expect(element.hasClass('dndDraggingSource')).toBe(false); })); - var dropEffects = {move: 'moved', copy: 'copied', none: 'canceled'}; + var dropEffects = {move: 'moved', copy: 'copied', link: 'linked', none: 'canceled'}; angular.forEach(dropEffects, function(callback, dropEffect) { it('calls callbacks for dropEffect ' + dropEffect, function() { - var html = '
      '; + var html = '
      '; var element = compileAndLink(html); + var target = compileAndLink('
      '); + Dragstart.on(element).dragover(target).drop(target).dragend(element); - var dragstart = Dragstart.on(element); - if (dropEffect != 'none') { - var target = compileAndLink('
      '); - var options = {dropEffect: dropEffect}; - dragstart.dragover(target, options).drop(target).dragend(element); - } else { - dragstart.dragend(element); - } - - expect(element.scope().ev).toEqual(jasmine.any(DragEventMock)); - expect(element.scope().de).toBe(dropEffect); + expect(element.scope().returnedEvent).toEqual(jasmine.any(DragEventMock)); + expect(element.scope().returnedDropEffect).toBe(dropEffect); }); }); }); diff --git a/test/dndListSpec.js b/test/dndListSpec.js index 3761883..b77a31c 100644 --- a/test/dndListSpec.js +++ b/test/dndListSpec.js @@ -171,6 +171,20 @@ describe('dndList', function() { expect(element.scope().dragover.external).toBe(true); }); + it('invokes dnd-dragover with undefined callback', function() { + element = createListWithItemsAndCallbacks(); + Dragstart.on(source).dragover(element); + expect(element.scope().dragover.callback).toBeUndefined(); + }); + + it('invokes dnd-dragover with callback set on dragstart', function() { + source = compileAndLink('
      '); + source.scope().a = 2; + element = compileAndLink('
        '); + Dragstart.on(source).dragover(element); + expect(element.scope().result).toBe(6) + }); + it('dnd-dragover callback can cancel the drop', function() { element = compileAndLink('
        '); verifyDropCancelled(Dragstart.on(source).dragover(element), element); @@ -291,6 +305,21 @@ describe('dndList', function() { expect(element.scope().list).toEqual([1, 2, 3]); }); + it('invokes dnd-drop with undefined callback', function() { + element = createListWithItemsAndCallbacks(); + Dragstart.on(source).dragover(element).drop(element); + expect(element.scope().drop.callback).toBeUndefined(); + }); + + it('invokes dnd-drop with callback set on dragstart', function() { + source = compileAndLink('
        '); + source.scope().a = 2; + element = compileAndLink('
          '); + element.scope().list = []; + Dragstart.on(source).dragover(element).drop(element); + expect(element.scope().list).toEqual([6]) + }); + it('invokes callbacks with correct type', function() { source = compileAndLink('
          '); Dragstart.on(source).dragover(element).drop(element); @@ -341,24 +370,6 @@ describe('dndList', function() { var dragenter = Dragenter.externalOn(element, {'application/x-dnd': 'Lorem ipsum'}); verifyDropCancelled(dragenter.dragover(element).drop(element), element, true, 3); }); - - describe('dropEffect calculation', function() { - testDropEffect('move', 'move'); - testDropEffect('blub', 'blub'); - testDropEffect('copy', 'none', 'copy'); - testDropEffect('move', 'none', 'move'); - testDropEffect('move', 'none', 'link'); - testDropEffect('copy', 'none', 'link', true); - - function testDropEffect(expected, dropEffect, effectAllowed, ctrlKey) { - it('stores ' + expected + ' for ' + [dropEffect, effectAllowed, ctrlKey], function() { - var src = compileAndLink('
          '); - var options = { dropEffect: dropEffect, effectAllowed: effectAllowed, ctrlKey: ctrlKey }; - Dragstart.on(src).dragover(element, options).drop(element).dragend(src); - expect(src.scope().eff).toBe(expected); - }); - } - }); }); describe('dragleave handler', function() { @@ -377,26 +388,142 @@ describe('dndList', function() { element.remove(); }); - it('removes the dndDragover class', function() { - var rect = element.children()[1].getBoundingClientRect(); - dragover.dragleave(element); + it('removes the placeholder and dndDragover class', function() { + var rect = element[0].getBoundingClientRect(); + dragover.dragleave(element, {clientX: rect.left - 2, clientY: rect.top - 2}); expect(element.hasClass('dndDragover')).toBe(false); + expect(element.children().length).toBe(3); }); - it('removes the placeholder after a timeout', inject(function($timeout) { - dragover.dragleave(element); - $timeout.flush(50); - expect(element.children().length).toBe(4); - $timeout.flush(50); + it('removes the placeholder and dndDragover if child placeholder is already set', function() { + var rect = element[0].getBoundingClientRect(); + dragover.dragleave(element, {clientX: rect.left + 2, clientY: rect.top + 2, phShown: true}); + expect(element.hasClass('dndDragover')).toBe(false); expect(element.children().length).toBe(3); - })); + }); - it('does not remove the placeholder if dndDragover was set again', inject(function($timeout) { - dragover.dragleave(element); - element.addClass('dndDragover') - $timeout.flush(200); + it('sets _dndPhShown if mouse is still inside', function() { + var rect = element[0].getBoundingClientRect(); + var result = dragover.dragleave(element, {clientX: rect.left + 2, clientY: rect.top + 2}); + expect(element.hasClass('dndDragover')).toBe(true); expect(element.children().length).toBe(4); - })); + expect(result.dndPhShownSet).toBe(true); + }); + }); + + describe('dropEffect', function() { + // This matrix shows the expected drop effect, given two effectAllowed values. + var ALL = [ 'all', 'move', 'copy', 'link', 'copyLink', 'copyMove', 'linkMove']; + var EXPECTED_MATRIX = { + move: ['move', 'move', 'none', 'none', 'none', 'move', 'move'], + copy: ['copy', 'none', 'copy', 'none', 'copy', 'copy', 'none'], + link: ['link', 'none', 'none', 'link', 'link', 'none', 'link'], + copyLink: ['copy', 'none', 'copy', 'link', 'copy', 'copy', 'link'], + copyMove: ['move', 'move', 'copy', 'none', 'copy', 'move', 'move'], + linkMove: ['move', 'move', 'none', 'link', 'link', 'move', 'move'], + all: ['move', 'move', 'copy', 'link', 'copy', 'move', 'move'], + '': ['move', 'move', 'copy', 'link', 'copy', 'move', 'move'], + }; + angular.forEach(ALL, function(sourceEffectAllowed, index) { + angular.forEach(EXPECTED_MATRIX, function(expected, targetEffectAllowed) { + expected = expected[index]; + it('is ' + expected + ' for effect-allowed ' + sourceEffectAllowed + + ' and ' + targetEffectAllowed, function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(false, targetEffectAllowed); + expect(Dragstart.on(src).effectAllowed).toBe(sourceEffectAllowed); + if (expected != 'none') { + // Verify dragover. + expect(Dragstart.on(src).dragover(target).dropEffect).toBe(expected); + expect(target.scope().dragover.dropEffect).toBe(expected); + // Verify drop. + expect(Dragstart.on(src).dragover(target).drop(target).dropEffect).toBe(expected); + expect(target.scope().drop.dropEffect).toBe(expected); + // Verify dragend. + Dragstart.on(src).dragover(target).drop(target).dragend(src); + expect(src.scope().result).toBe(expected); + } else { + verifyDropCancelled(Dragstart.on(src).dragover(target), target, false, 3); + verifyDropCancelled(Dragstart.on(src).dragover(target).drop(target), target, true, 3); + Dragstart.on(src).dragend(src); + expect(src.scope().result).toBe('none'); + } + }); + }); + }); + + // In Safari dataTransfer.effectAllowed is always 'all', ignoring the value set in dragstart. + it('is determined from internal state in Safari', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(false, 'copyLink'); + var options = {effectAllowed: 'all'}; + Dragstart.on(src).dragover(target, options).drop(target, options); + expect(target.scope().dragover.dropEffect).toBe('link'); + expect(target.scope().drop.dropEffect).toBe('link'); + }); + + // On MacOS, modifiers automatically limit the effectAllowed passed to dragover and drop. + it('is limited by modifier keys on MacOS', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + Dragstart.on(src).dragover(target, {effectAllowed: 'copyLink'}).drop(target); + expect(target.scope().dragover.dropEffect).toBe('copy'); + expect(target.scope().drop.dropEffect).toBe('copy'); + }); + + it('is copy if Ctrl key is pressed', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + Dragstart.on(src).dragover(target, {ctrlKey: true}).drop(target); + expect(target.scope().dragover.dropEffect).toBe('copy'); + expect(target.scope().drop.dropEffect).toBe('copy'); + }); + + it('is link if Alt key is pressed', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + Dragstart.on(src).dragover(target, {altKey: true}).drop(target); + expect(target.scope().dragover.dropEffect).toBe('link'); + expect(target.scope().drop.dropEffect).toBe('link'); + }); + + it('ignores Ctrl key if copy is not possible', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + Dragstart.on(src).dragover(target, {ctrlKey: true}).drop(target); + expect(target.scope().dragover.dropEffect).toBe('move'); + expect(target.scope().drop.dropEffect).toBe('move'); + }); + + it('respects effectAllowed from external drops', function() { + var target = createListWithItemsAndCallbacks(); + Dragenter.validExternalOn(target, {effectAllowed: 'copyLink'}).dragover(target).drop(target); + expect(target.scope().dragover.dropEffect).toBe('copy'); + expect(target.scope().drop.dropEffect).toBe('copy'); + }); + + it('respects effectAllowed from external drops in IE', function() { + var target = createListWithItemsAndCallbacks(); + Dragenter.externalOn(target, {'Text': '{}'}, {effectAllowed: 'copyLink'}) + .dragover(target).drop(target); + expect(target.scope().dragover.dropEffect).toBe('move'); + expect(target.scope().drop.dropEffect).toBe('move'); + }); + + it('ignores effectAllowed from internal drops in IE', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + Dragstart.on(src, {allowedMimeTypes: ['Text']}).dragover(target, {altKey: true}); + expect(target.scope().dragover.dropEffect).toBe('link'); + }); + + it('does not set dropEffect in IE', function() { + var src = compileAndLink('
          '); + var target = createListWithItemsAndCallbacks(); + var dragover = Dragstart.on(src, {allowedMimeTypes: ['Text']}).dragover(target); + expect(dragover.dropEffect).toBeUndefined(); + }); }); function verifyDropAccepted(result) { @@ -414,6 +541,7 @@ describe('dndList', function() { expect(result.returnValue).toBe(true); expect(result.propagationStopped).toBe(false); expect(result.defaultPrevented).toBe(opt_defaultPrevented || false); + expect(result.dropEffect).toBeUndefined(); expect(element.hasClass("dndDragover")).toBe(false); expect(element.children().length).toBe(opt_children || 0); } @@ -428,10 +556,12 @@ describe('dndList', function() { verify(drop, element); } - function createListWithItemsAndCallbacks(horizontal) { - var params = '{event: event, index: index, item: item, external: external, type: type}'; + function createListWithItemsAndCallbacks(horizontal, effectAllowed) { + var params = '{event: event, dropEffect: dropEffect, index: index, ' + + 'item: item, external: external, type: type, callback: callback}'; var element = compileAndLink('
            ' + diff --git a/test/mocks.js b/test/mocks.js index 3e8c49a..43ed307 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -50,8 +50,8 @@ class DropzoneDataTransfer extends DataTransferMock { this.$types = options.undefinedTypes ? undefined : Object.keys(data); } - get dropEffect() { return this.$dropEffect; } - set dropEffect(value) { throw "Unexcepted dropEffect setter invocation"; } + get dropEffect() { throw "Unexcepted dropEffect getter invocation"; } + set dropEffect(value) { this.$results.dropEffect = value; } get effectAllowed() { return this.$effectAllowed; } set effectAllowed(value) { throw "Unexcepted effectAllowed setter invocation"; } get types() { return this.$types; } @@ -73,11 +73,14 @@ class DragEventMock { get clientX() { return this.$options.clientX || 0; } get clientY() { return this.$options.clientY || 0; } get ctrlKey() { return this.$options.ctrlKey || false; } + get altKey() { return this.$options.altKey || false; } get dataTransfer() { return this.$dataTransfer; } get originalEvent() { return this; } get target() { return this.$options.target || undefined; } get type() { return this.$type; } get _dndHandle() { return this.$options.dndHandle || undefined; } + get _dndPhShown() { return this.$options.phShown || undefined; } + set _dndPhShown(value) { this.$results.setDndPhShown = value; } preventDefault() { this.$results.invokedPreventDefault = true; } stopPropagation() { this.$results.invokedStopPropagation = true; } @@ -95,7 +98,9 @@ class DragEventResult { get propagationStopped() { return !!this.$results.invokedStopPropagation; } get defaultPrevented() { return !!this.$results.invokedPreventDefault; } + get dndPhShownSet() { return this.$results.setDndPhShown || false; } get returnValue() { return this.$results.returnValue; } + get dropEffect() { return this.$results.dataTransfer.dropEffect; } get type() { return this.$type; } } @@ -109,7 +114,8 @@ class Dragstart extends DragEventResult { get effectAllowed() { return this.$results.dataTransfer.effectAllowed; } dragenter(element, opt_options) { - return new Dragenter(element, this.$results.dataTransfer.data, opt_options || {}); + var options = $.extend({effectAllowed: this.effectAllowed}, opt_options); + return new Dragenter(element, this.$results.dataTransfer.data, options); } dragover(element, opt_options) {