sogo/UI/WebServerResources/js/Scheduler/sgDraggableCalendarBlock.di...

625 lines
23 KiB
JavaScript

/* -*- 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:
<div class="sg-draggable-calendar-block"/>
*/
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('<div class="dragGrip"></div>');
dragGrip.addClass('bdr-folder' + component.pid);
if (component.c_isallday ||
element[0].parentNode.tagName === 'SG-CALENDAR-MONTH-DAY') {
if (isFirstBlock) {
leftGrip = angular.element('<div class="dragGrip-left"></div>').append(dragGrip);
element.append(leftGrip);
}
if (isLastBlock) {
rightGrip = angular.element('<div class="dragGrip-right"></div>').append(dragGrip.clone());
element.append(rightGrip);
}
}
else {
if (isFirstBlock) {
topGrip = angular.element('<div class="dragGrip-top"></div>').append(dragGrip);
element.append(topGrip);
}
if (isLastBlock) {
bottomGrip = angular.element('<div class="dragGrip-bottom"></div>').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);
})();