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';