diff --git a/UI/WebServerResources/js/Common/sgDraggable.directive.js b/UI/WebServerResources/js/Common/sgDraggable.directive.js
new file mode 100644
index 000000000..2dd0ca192
--- /dev/null
+++ b/UI/WebServerResources/js/Common/sgDraggable.directive.js
@@ -0,0 +1,155 @@
+/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+(function() {
+ /* jshint validthis: true */
+ 'use strict';
+
+ /*
+ * sgDraggable - Make an element (usually a folder of elements) draggable.
+ * @memberof SOGo.Common
+ * @restrict attribute
+ * @param {Object=} sgDraggable - the object to be exposed to the droppable target.
+ * @param {expression} sgDragStart - dragging will only start if this expression returns true.
+ * @param {expression} sgDragCount - the number of items being dragged; this number appears inside
+ * the sg-draggable-helper element that follows the mouse cursor.
+ *
+ * @example:
+
+
+ email
+
+
+
+
+ */
+ sgDraggable.$inject = ['$parse', '$rootScope', '$document', '$timeout', '$log'];
+ function sgDraggable($parse, $rootScope, $document, $timeout, $log) {
+ return {
+ restrict: 'A',
+ link: link
+ };
+
+ function link(scope, element, attrs) {
+ var o;
+
+ $timeout(function() {
+ var folder, dragStart, count;
+
+ folder = $parse(attrs.sgDraggable)(scope);
+ dragStart = attrs.sgDragStart? $parse(attrs.sgDragStart) : null;
+ count = attrs.sgDragCount? $parse(attrs.sgDragCount) : null;
+ o = new sgDraggableObject(element, folder, dragStart, count);
+ });
+
+ scope.$on('$destroy', function() {
+ o.$destroy();
+ });
+
+ function sgDraggableObject($element, folder, dragStart, count) {
+ this.$element = $element;
+ this.folder = folder;
+ this.dragStart = dragStart;
+ this.count = count;
+ this.helper = $document.find('sg-draggable-helper');
+
+ if (!this.helper) {
+ throw Error('sg-draggable requires a sg-draggable-helper element.');
+ }
+
+ this.bindedOnDragDetect = angular.bind(this, this.onDragDetect);
+ this.bindedOnDrag = angular.bind(this, this.onDrag);
+
+ // Register the mousedown event that can trigger the dragging action
+ this.$element.on('mousedown', this.bindedOnDragDetect);
+ }
+
+ /**
+ * sgDraggableObject is an object that wraps the logic to emit the folder:dragstart and
+ * folder:dragend custom events.
+ */
+ sgDraggableObject.prototype = {
+
+ dragHasStarted: false,
+
+ $destroy: function() {
+ this.$element.off('mousedown', this.bindedOnDragDetect);
+ },
+
+ getDistanceFromStart: function(event) {
+ var delta = {
+ x: this.startPosition.clientX - event.clientX,
+ y: this.startPosition.clientY - event.clientY
+ };
+
+ return Math.sqrt(delta.x * delta.x + delta.y * delta.y);
+ },
+
+
+ // Start dragging on mousedown
+ onDragDetect: function(ev) {
+ var dragMode, pointerHandler;
+
+ ev.stopPropagation();
+
+ if (!this.dragStart || this.dragStart(scope)) {
+ // Listen to mousemove and start dragging when mouse has moved from at least 3 pixels
+ $document.on('mousemove', this.bindedOnDrag);
+ // Stop dragging on the next "mouseup"
+ $document.one('mouseup', angular.bind(this, this.onDragEnd));
+ }
+ },
+
+ //
+ onDrag: function(ev) {
+ var counter;
+
+ if (!this.startPosition) {
+ this.startPosition = { clientX: ev.clientX, clientY: ev.clientY };
+ }
+ else if (!this.dragHasStarted && this.getDistanceFromStart(ev) > 10) {
+ counter = this.helper.find('sg-draggable-helper-counter');
+ this.dragHasStarted = true;
+
+ this.helper.removeClass('ng-hide');
+ if (this.count && this.count(scope) > 1)
+ counter.text(this.count(scope)).removeClass('ng-hide');
+ else
+ counter.addClass('ng-hide');
+
+ $log.debug('emit folder:dragstart');
+ $rootScope.$emit('folder:dragstart', this.folder);
+ }
+ if (this.dragHasStarted) {
+ if (ev.shiftKey)
+ this.helper.addClass('sg-draggable-helper--copy');
+ else
+ this.helper.removeClass('sg-draggable-helper--copy');
+ this.helper.css({ top: (ev.pageY + 5) + 'px', left: (ev.pageX + 5) + 'px' });
+ }
+ },
+
+
+ onDragEnd: function(ev) {
+ this.startPosition = null;
+ $document.off('mousemove', this.bindedOnDrag);
+
+ if (this.dragHasStarted) {
+ $log.debug('emit folder:dragend');
+ $rootScope.$emit('folder:dragend', this.folder, ev.shiftKey?'copy':'move');
+ this.dragHasStarted = false;
+ this.helper.addClass('ng-hide');
+ }
+ }
+
+ };
+
+ }
+ }
+
+ angular
+ .module('SOGo.Common')
+ .directive('sgDraggable', sgDraggable);
+})();
+
diff --git a/UI/WebServerResources/js/Common/sgDroppable.directive.js b/UI/WebServerResources/js/Common/sgDroppable.directive.js
new file mode 100644
index 000000000..4f75c35af
--- /dev/null
+++ b/UI/WebServerResources/js/Common/sgDroppable.directive.js
@@ -0,0 +1,78 @@
+/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+(function() {
+ /* jshint validthis: true */
+ 'use strict';
+
+ /*
+ * sgDroppable - Make an element a possible destination while dragging
+ * @memberof SOGo.Common
+ * @restrict attribute
+ * @param {expression} sgDroppable - dropping is accepted only if this expression returs true.
+ * One variables is exposed: dragFolder.
+ * @param {expression} sgDrop - called when dropping ends on the element.
+ * Two variables are exposed: dragFolder and dragMode.
+ *
+ * @example:
+
+
+ */
+ sgDroppable.$inject = ['$parse', '$rootScope', '$document', '$timeout', '$log'];
+ function sgDroppable($parse, $rootScope, $document, $timeout, $log) {
+ return {
+ restrict: 'A',
+ link: link
+ };
+
+ function link(scope, element, attrs) {
+ var overElement = false, dropAction, droppable,
+ deregisterFolderDragStart, deregisterFolderDragEnd;
+
+ if (!attrs.sgDrop) {
+ throw Error('sg-droppable requires a sg-drop action.');
+ }
+
+ overElement = false;
+ droppable = $parse(attrs.sgDroppable);
+ dropAction = $parse(attrs.sgDrop);
+
+ // Register listeners of custom events on root scope
+ deregisterFolderDragStart = $rootScope.$on('folder:dragstart', function(event, folder) {
+ if (droppable(scope, { dragFolder: folder })) {
+ element.on('mouseenter', onEnter);
+ element.on('mouseleave', onLeave);
+ }
+ });
+ deregisterFolderDragEnd = $rootScope.$on('folder:dragend', function(event, folder, mode) {
+ element.off('mouseenter');
+ element.off('mouseleave');
+ if (overElement) {
+ angular.bind(element[0], onLeave)(event);
+ dropAction(scope, { dragFolder: folder, dragMode: mode });
+ }
+ });
+
+ scope.$on('destroy', function() {
+ deregisterFolderDragStart();
+ deregisterFolderDragEnd();
+ });
+
+ function onEnter(event) {
+ overElement = true;
+ element.addClass('sg-droppable-over');
+ }
+
+ function onLeave(event) {
+ overElement = false;
+ this.classList.remove('sg-droppable-over');
+ element.off('mousemove');
+ }
+ }
+ }
+
+ angular
+ .module('SOGo.Common')
+ .directive('sgDroppable', sgDroppable);
+})();
+
diff --git a/UI/WebServerResources/scss/components/draggable-droppable/draggable.scss b/UI/WebServerResources/scss/components/draggable-droppable/draggable.scss
new file mode 100644
index 000000000..86c4b97f3
--- /dev/null
+++ b/UI/WebServerResources/scss/components/draggable-droppable/draggable.scss
@@ -0,0 +1,29 @@
+/// draggable.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
+
+sg-draggable-helper {
+ min-width: $icon-button-width;
+ min-height: $icon-button-width;
+ position: absolute;
+ top: -200px;
+ left: -200px;
+ z-index: $z-index-toast + 1;
+ &.sg-draggable-helper--copy md-icon {
+ color: rgba(0,0,0,0.34);
+ text-shadow: 3px 3px 0px rgba(0, 0, 0, 0.54);
+ }
+ md-icon {
+ margin: $icon-button-margin;
+ }
+ sg-draggable-helper-counter {
+ border-radius: 50%;
+ font-size: 13px;
+ line-height: $icon-badge-size;
+ min-height: $icon-badge-size;
+ min-width: $icon-badge-size;
+ position: absolute;
+ text-align: center;
+ right: ($icon-size - $icon-badge-size) / 2;
+ top: ($icon-size - $icon-badge-size) / 2;
+ }
+}
+
diff --git a/UI/WebServerResources/scss/components/draggable-droppable/droppable.scss b/UI/WebServerResources/scss/components/draggable-droppable/droppable.scss
new file mode 100644
index 000000000..376558503
--- /dev/null
+++ b/UI/WebServerResources/scss/components/draggable-droppable/droppable.scss
@@ -0,0 +1,7 @@
+/// droppable.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
+
+.sg-droppable-over {
+ @extend .md-whiteframe-3dp;
+ background-color: #fff;
+ cursor: pointer;
+}
diff --git a/UI/WebServerResources/scss/styles.scss b/UI/WebServerResources/scss/styles.scss
index 957fe140a..85d3e6977 100755
--- a/UI/WebServerResources/scss/styles.scss
+++ b/UI/WebServerResources/scss/styles.scss
@@ -66,6 +66,8 @@
@import 'components/whiteframe/whiteframe';
// Inverse components
+@import 'components/draggable-droppable/draggable';
+@import 'components/draggable-droppable/droppable';
@import 'components/ripple/ripple';
@import 'components/timepicker/timepicker';