/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ (function() { 'use strict'; /* * sgDraggableCalendarBlock - Make an element draggable * @memberof SOGo.SchedulerUI * @restrict class or attribute * * @example:
*/ sgDraggableCalendarBlock.$inject = ['$rootScope', '$timeout', '$log', 'Preferences', 'Calendar', 'CalendarSettings', 'Component']; function sgDraggableCalendarBlock($rootScope, $timeout, $log, Preferences, Calendar, CalendarSettings, Component) { return { restrict: 'CA', require: '^sgCalendarDay', link: link }; function link(scope, element, attrs, calendarDayCtrl) { if (scope.block) { if (scope.block.component.editable && !scope.block.userState) { // Add dragging grips to existing event block initGrips(); } else { element.removeClass('sg-draggable-calendar-block'); return; } } // Start dragging on mousedown element.on('mousedown', onDragDetect); // Deregister listeners when removing the element from the DOM scope.$on('$destroy', function() { element.off('mousedown', onDragDetect); element.off('mousemove', onDrag); }); function initGrips() { var component, dayIndex, blockIndex, isFirstBlock, isLastBlock, dragGrip, leftGrip, rightGrip, topGrip, bottomGrip; // Don't show grips for blocks of less than 45 minutes if (scope.block.length < 3) return; component = scope.block.component; dayIndex = scope.block.dayIndex; blockIndex = _.findIndex(component.blocks, ['dayIndex', dayIndex]); isFirstBlock = (blockIndex === 0); isLastBlock = (blockIndex === component.blocks.length - 1); dragGrip = angular.element('
'); dragGrip.addClass('bdr-folder' + component.pid); if (component.c_isallday || element[0].parentNode.tagName === 'SG-CALENDAR-MONTH-DAY') { if (isFirstBlock) { leftGrip = angular.element('
').append(dragGrip); element.append(leftGrip); } if (isLastBlock) { rightGrip = angular.element('
').append(dragGrip.clone()); element.append(rightGrip); } } else { if (isFirstBlock) { topGrip = angular.element('
').append(dragGrip); element.append(topGrip); } if (isLastBlock) { bottomGrip = angular.element('
').append(dragGrip.clone()); element.append(bottomGrip); } } } function onDragDetect(ev) { var dragMode, pointerHandler, hasVerticalScrollbar, rect, scrollableZone; ev.stopPropagation(); hasVerticalScrollbar = ev.target.scrollHeight > ev.target.clientHeight + 1; if (hasVerticalScrollbar) { // Check if mouse click is inside scrollbar rect = ev.target.getBoundingClientRect(); scrollableZone = rect.left + rect.width - 18; if (ev.pageX > scrollableZone) return; } dragMode = 'move-event'; if (scope.block && scope.block.component) { // Move or resize existing component if (ev.target.className == 'dragGrip-top' || ev.target.className == 'dragGrip-left') dragMode = 'change-start'; else if (ev.target.className == 'dragGrip-bottom' || ev.target.className == 'dragGrip-right' ) dragMode = 'change-end'; } else { // Create new component from dragging dragMode = 'change-end'; } // Initialize pointer handler pointerHandler = new SOGoEventDragPointerHandler(dragMode); pointerHandler.initFromEvent(ev); // Update Component.$ghost Component.$ghost.pointerHandler = pointerHandler; // Stop dragging on the next "mouseup" angular.element(document).one('mouseup', onDragEnd); // Listen to mousemove and start dragging when mouse has moved from at least 3 pixels angular.element(document).on('mousemove', onDrag); } function dragStart(ev) { var block, eventType, isHourCell, isMonthly, startDate, newData, newComponent, pointerHandler, calendarData; isHourCell = element.hasClass('clickableHourCell'); isMonthly = (element[0].parentNode.tagName == 'SG-CALENDAR-MONTH-DAY') || element.hasClass('clickableDayCell'); calendarData = calendarDayCtrl.calendarData(); if (scope.block && scope.block.component) { // Move or resize existing component block = scope.block; } else { // Create new component from dragging startDate = calendarDayCtrl.dayString.parseDate(Preferences.$mdDateLocaleProvider, '%Y-%m-%e'); newData = { type: 'appointment', pid: calendarData? calendarData.pid : Calendar.$defaultCalendar(), summary: l('New Event'), startDate: startDate, isAllDay: isHourCell? 0 : 1 }; newComponent = new Component(newData); block = { component: newComponent, dayNumber: calendarDayCtrl.dayNumber, length: 0 }; block.component.blocks = [block]; } // Determine event type eventType = 'multiday'; if (isMonthly) eventType = 'monthly'; else if (block.component.c_isallday) eventType = 'multiday-allday'; // Mark all blocks as being dragged _.forEach(block.component.blocks, function(b) { b.dragging = true; }); // Update pointer handler pointerHandler = Component.$ghost.pointerHandler; pointerHandler.prepareWithEventType(eventType); pointerHandler.initFromBlock(block); if (calendarData) // When the day is associated to a calendar, the day number becomes the calendar index // among the active calendars pointerHandler.initFromCalendar(calendarData); // Update Component.$ghost Component.$ghost.component = block.component; $log.debug('emit calendar:dragstart ' + eventType); $rootScope.$emit('calendar:dragstart'); } function onDrag(ev) { var pointerHandler = Component.$ghost.pointerHandler; // Update // - currentCoordinates // - currentViewCoordinates // - currentEventCoordinates $timeout(function() { pointerHandler.updateFromEvent(ev); }); } function onDragEnd(ev) { var block, pointer; block = scope.block; pointer = Component.$ghost.pointerHandler; // Deregister mouse events angular.element(document).off('mousemove', onDrag); if (pointer.dragHasStarted) { $rootScope.$emit('calendar:dragend'); pointer.dragHasStarted = false; } // Unmark all blocks as being dragged if (block && block.component) _.forEach(block.component.blocks, function(b) { b.dragging = false; }); } /** * SOGoCoordinates */ function SOGoCoordinates() { } SOGoCoordinates.prototype = { x: -1, y: -1, getDelta: function SC_getDelta(otherCoordinates) { var delta = new SOGoCoordinates(); delta.x = this.x - otherCoordinates.x; delta.y = this.y - otherCoordinates.y; if (Calendar.$view) { delta.days = Calendar.$view.dayNumbers[this.x] - Calendar.$view.dayNumbers[otherCoordinates.x]; } return delta; }, getDistance: function SC_getDistance(otherCoordinates) { var delta = this.getDelta(otherCoordinates); return Math.sqrt(delta.x * delta.x + delta.y * delta.y); }, clone: function SC_clone() { var coordinates = new SOGoCoordinates(); coordinates.x = this.x; coordinates.y = this.y; return coordinates; } }; /** * SOGoEventDragEventCoordinates */ function SOGoEventDragEventCoordinates(eventType) { this.setEventType(eventType); } SOGoEventDragEventCoordinates.prototype = { dayNumber: -1, weekDay: -1, start: -1, duration: -1, eventType: null, setEventType: function(eventType) { this.eventType = eventType; }, initFromBlock: function(block) { var prevDayNumber = -1; if (this.eventType === 'monthly') { this.start = 0; this.duration = block.component.blocks.length * CalendarSettings.EventDragDayLength; } else { // Get the start (first quarter) from the event's first block // Compute overall length this.start = block.component.blocks[0].start; this.duration = _.sumBy(block.component.blocks, function(b) { var delta, currentDayNumber; currentDayNumber = b.dayNumber; if (prevDayNumber < 0) delta = 0; else delta = currentDayNumber - prevDayNumber - 1; prevDayNumber = currentDayNumber; return b.length + delta * CalendarSettings.EventDragDayLength; }); } }, initFromCalendar: function(calendarNumber) { this.dayNumber = calendarNumber; }, getDelta: function(otherCoordinates) { var delta = new SOGoEventDragEventCoordinates(); delta.dayNumber = (this.dayNumber - otherCoordinates.dayNumber); delta.start = (this.start - otherCoordinates.start); delta.duration = (this.duration - otherCoordinates.duration); return delta; }, _quartersToHM: function(quarters) { var minutes = quarters * 15; var hours = Math.floor(minutes / 60); if (hours < 10) hours = "0" + hours; var mins = minutes % 60; if (mins < 10) mins = "0" + mins; return "" + hours + ":" + mins; }, getStartTime: function() { return this._quartersToHM(this.start); }, getEndTime: function() { var end = (this.start + this.duration) % CalendarSettings.EventDragDayLength; return this._quartersToHM(end); }, clone: function() { var coordinates = new SOGoEventDragEventCoordinates(); coordinates.dayNumber = this.dayNumber; coordinates.start = this.start; coordinates.duration = this.duration; return coordinates; } }; /** * SOGoEventDragPointerHandler */ function SOGoEventDragPointerHandler(dragMode) { this.dragMode = dragMode; } SOGoEventDragPointerHandler.prototype = { // Pointer absolute xy coordinates within page originalCoordinates: null, currentCoordinates: null, // Pointer relative xy coordinates within view (row-column) originalViewCoordinates: null, currentViewCoordinates: null, // Event start-duration coordinates originalEventCoordinates: null, currentEventCoordinates: null, originalCalendar: null, dragHasStarted: false, // Function to return the day and quarter coordinates of the pointer cursor // within the day view getEventViewCoordinates: null, initFromBlock: function SEDPH_initFromBlock(block) { this.currentEventCoordinates = new SOGoEventDragEventCoordinates(this.eventType); this.originalEventCoordinates = new SOGoEventDragEventCoordinates(this.eventType); this.originalEventCoordinates.initFromBlock(block); }, initFromEvent: function SEDPH_initFromEvent(event) { this.currentCoordinates = new SOGoCoordinates(); this.updateFromEvent(event); this.originalCoordinates = this.currentCoordinates.clone(); }, initFromCalendar: function SEDPH_initFromCalendar(calendarData) { this.originalCalendar = calendarData; this.currentEventCoordinates.initFromCalendar(calendarData.index); this.originalEventCoordinates.initFromCalendar(calendarData.index); }, // Method continuously called while dragging updateFromEvent: function SEDPH_updateFromEvent(event) { // Event here is a DOM event, not a calendar event! this.currentCoordinates.x = event.pageX; this.currentCoordinates.y = event.pageY; // From SOGoEventDragGhostController.updateFromPointerHandler if (this.dragHasStarted && Calendar.$view) { var newEventCoordinates = this.getEventViewCoordinates(Calendar.$view); if (!this.originalViewCoordinates) { this.originalViewCoordinates = this.getEventViewCoordinates(Calendar.$view, this.originalCoordinates); if (Component.$ghost.component.isNew) { this.setTimeFromQuarters(Component.$ghost.component.start, this.originalViewCoordinates.y); $log.debug('new event start date ' + Component.$ghost.component.start); } } if (!this.currentViewCoordinates || !newEventCoordinates || newEventCoordinates.x != this.currentViewCoordinates.x || newEventCoordinates.y != this.currentViewCoordinates.y) { this.currentViewCoordinates = newEventCoordinates; if (this.originalViewCoordinates) { if (!newEventCoordinates) { this.currentViewCoordinates = this.originalViewCoordinates.clone(); } this.updateEventCoordinates(); } } } else if (this.originalCoordinates && this.currentCoordinates && !this.dragHasStarted) { var distance = this.getDistance(); if (distance > 3) { this.dragHasStarted = true; dragStart(event); } } }, // SOGoEventDragGhostController._updateCoordinates // Extend this.currentCoordinates with start, dayNumber and duration updateEventCoordinates: function SEDGC__updateCoordinates() { var newDuration; // Compute delta wrt to position of mouse at dragstart on the day/quarter grid var delta = this.currentViewCoordinates.getDelta(this.originalViewCoordinates); var deltaQuarters = delta.days * CalendarSettings.EventDragDayLength + delta.y; $log.debug('quarters delta ' + deltaQuarters); if (angular.isUndefined(this.originalEventCoordinates.start)) { // Creating new appointment from DnD this.originalEventCoordinates.dayNumber = Calendar.$view.dayNumbers[this.originalViewCoordinates.x]; this.originalEventCoordinates.start = this.originalViewCoordinates.y; } else if (this.originalEventCoordinates.dayNumber < 0) { this.originalEventCoordinates.dayNumber = Calendar.$view.dayNumbers[scope.block.component.blocks[0].dayIndex]; } // if (currentView == "multicolumndayview") // this._updateMulticolumnViewDayNumber_SEDGC(); // else this.currentEventCoordinates.dayNumber = this.originalEventCoordinates.dayNumber; if (this.dragMode == "move-event") { this.currentEventCoordinates.start = this.originalEventCoordinates.start + deltaQuarters; this.currentEventCoordinates.duration = this.originalEventCoordinates.duration; } else { if (this.dragMode == "change-start") { newDuration = this.originalEventCoordinates.duration - deltaQuarters; if (newDuration > 0) { this.currentEventCoordinates.start = this.originalEventCoordinates.start + deltaQuarters; this.currentEventCoordinates.duration = newDuration; } else if (newDuration < 0) { this.currentEventCoordinates.start = (this.originalEventCoordinates.start + this.originalEventCoordinates.duration); this.currentEventCoordinates.duration = -newDuration; } } else if (this.dragMode == "change-end") { newDuration = this.originalEventCoordinates.duration + deltaQuarters; if (newDuration > 0) { this.currentEventCoordinates.start = this.originalEventCoordinates.start; this.currentEventCoordinates.duration = newDuration; } else if (newDuration < 0) { this.currentEventCoordinates.start = this.originalEventCoordinates.start + newDuration; this.currentEventCoordinates.duration = -newDuration; } } } var deltaDays; if (this.currentEventCoordinates.start < 0) { deltaDays = Math.ceil(-this.currentEventCoordinates.start / CalendarSettings.EventDragDayLength); this.currentEventCoordinates.start += deltaDays * CalendarSettings.EventDragDayLength; this.currentEventCoordinates.dayNumber -= deltaDays; } else if (this.currentEventCoordinates.start >= CalendarSettings.EventDragDayLength) { deltaDays = Math.floor(this.currentEventCoordinates.start / CalendarSettings.EventDragDayLength); this.currentEventCoordinates.start -= deltaDays * CalendarSettings.EventDragDayLength; this.currentEventCoordinates.dayNumber += deltaDays; } $log.debug('event coordinates ' + JSON.stringify(this.currentEventCoordinates)); $rootScope.$emit('calendar:drag'); }, // SOGoEventDragPointerHandler.getContainerBasedCoordinates getContainerBasedCoordinates: function SEDPH_getCBC(view, pointerCoordinates) { var currentCoordinates = pointerCoordinates || this.currentCoordinates; var coordinates = currentCoordinates.getDelta(view.coordinates); var container = view.element; if (coordinates.x < view.daysOffset || coordinates.x > container.clientWidth || coordinates.y < 0 || coordinates.y > container.clientHeight) coordinates = null; return coordinates; }, prepareWithEventType: function SEDPH_prepareWithEventType(eventType) { var methods = { "multiday": this.getEventMultiDayViewCoordinates, "multiday-allday": this.getEventMultiDayAllDayViewCoordinates, "monthly": this.getEventMonthlyViewCoordinates, "unknown": null }; var method = methods[eventType]; this.eventType = eventType; this.getEventViewCoordinates = method; }, getEventMultiDayViewCoordinates: function SEDPH_gEMultiDayViewC(view, pointerCoordinates) { /* x = day; y = quarter */ var coordinates = this.getEventMultiDayAllDayViewCoordinates(view, pointerCoordinates); // get the x coordinate if (coordinates) { var quarterHeight = view.quarterHeight; var pxCoordinates = this.getContainerBasedCoordinates(view, pointerCoordinates); pxCoordinates.y += view.element.scrollTop; coordinates.y = Math.floor((pxCoordinates.y - CalendarSettings.EventDragHorizontalOffset) / quarterHeight); var maxY = CalendarSettings.EventDragDayLength - 1; if (coordinates.y < 0) coordinates.y = 0; else if (coordinates.y > maxY) coordinates.y = maxY; } return coordinates; }, getEventMultiDayAllDayViewCoordinates: function SEDPH_gEMultiDayADVC(view, pointerCoordinates) { /* x = day; y = quarter */ var coordinates; var pxCoordinates = this.getContainerBasedCoordinates(view, pointerCoordinates); if (pxCoordinates) { coordinates = new SOGoCoordinates(); var dayWidth = view.dayWidth; var daysOffset = view.daysOffset; coordinates.x = Math.floor((pxCoordinates.x - daysOffset) / dayWidth); var minX = 0; var maxX = Calendar.$view.maxX; if (this.dragMode != 'move-event') { var calendarData = calendarDayCtrl.calendarData(); if (calendarData) // Resizing an event can't span a different day when in multicolumn view minX = maxX = calendarData.index; } if (coordinates.x < minX) coordinates.x = minX; else if (coordinates.x > maxX) coordinates.x = maxX; coordinates.y = 0; } else { coordinates = null; } return coordinates; }, getEventMonthlyViewCoordinates: function SEDPH_gEMonthlyViewC(view, pointerCoordinates) { /* x = day; y = quarter */ var coordinates; var pxCoordinates = this.getContainerBasedCoordinates(view, pointerCoordinates); if (pxCoordinates) { coordinates = new SOGoCoordinates(); var maxX = view.maxX; var daysTopOffset = 0; var dayWidth = view.dayWidth; var daysOffset = view.daysOffset; var dayHeight = view.dayHeight; var daysY = Math.floor((pxCoordinates.y - daysTopOffset) / dayHeight); if (daysY < 0) daysY = 0; coordinates.x = Math.floor((pxCoordinates.x - daysOffset) / dayWidth); if (coordinates.x < 0) coordinates.x = 0; else if (coordinates.x > maxX) coordinates.x = maxX; coordinates.x += (maxX + 1) * daysY; coordinates.y = 0; } else { coordinates = null; } return coordinates; }, getDistance: function SEDPH_getDistance() { return this.currentCoordinates.getDistance(this.originalCoordinates); }, setTimeFromQuarters: function SEDPH_setTimeFromQuarters(date, quarters) { var hours, minutes; hours = Math.floor(quarters / 4); minutes = (quarters % 4) * 15; date.setHours(hours, minutes); } }; } } angular .module('SOGo.SchedulerUI') .directive('sgDraggableCalendarBlock', sgDraggableCalendarBlock); })();