diff --git a/UI/Scheduler/UIxAppointmentActions.m b/UI/Scheduler/UIxAppointmentActions.m index 7c7dc045f..9044c4f01 100644 --- a/UI/Scheduler/UIxAppointmentActions.m +++ b/UI/Scheduler/UIxAppointmentActions.m @@ -38,6 +38,7 @@ #import #import +#import #import #import #import @@ -59,23 +60,25 @@ SoSecurityManager *sm; iCalEvent *event; NSCalendarDate *start, *newStart, *end, *newEnd; + NSDictionary *params; NSTimeInterval newDuration; SOGoUserDefaults *ud; - NSString *daysDelta, *startDelta, *durationDelta, *destionationCalendar; + NSNumber *daysDelta, *startDelta, *durationDelta; + NSString *destionationCalendar; NSTimeZone *tz; NSException *ex; SOGoAppointmentFolder *targetCalendar, *sourceCalendar; SOGoAppointmentFolders *folders; rq = [context request]; + params = [[rq contentAsString] objectFromJSONString]; - daysDelta = [rq formValueForKey: @"days"]; - startDelta = [rq formValueForKey: @"start"]; - durationDelta = [rq formValueForKey: @"duration"]; - destionationCalendar = [rq formValueForKey: @"destination"]; + daysDelta = [params objectForKey: @"days"]; + startDelta = [params objectForKey: @"start"]; + durationDelta = [params objectForKey: @"duration"]; + destionationCalendar = [params objectForKey: @"destination"]; - if ([daysDelta length] > 0 - || [startDelta length] > 0 || [durationDelta length] > 0) + if (daysDelta || startDelta || durationDelta) { co = [self clientObject]; event = (iCalEvent *) [[self clientObject] occurence]; @@ -119,7 +122,7 @@ [event setLastModified: [NSCalendarDate calendarDate]]; ex = [co saveComponent: event]; // This condition will be executed only if the event is moved from a calendar to another. If destionationCalendar == 0; there is no calendar change - if (![destionationCalendar isEqualToString:@"0"]) + if ([destionationCalendar length] > 0) { folders = [[self->context activeUser] calendarsFolderInContext: self->context]; sourceCalendar = [co container]; @@ -151,9 +154,8 @@ response = [self responseWith204]; } else - response - = (WOResponse *) [NSException exceptionWithHTTPStatus: 400 - reason: @"missing 'days', 'start' and/or 'duration' parameters"]; + response = (WOResponse *) [NSException exceptionWithHTTPStatus: 400 + reason: @"missing 'days', 'start' and/or 'duration' parameters"]; return response; } diff --git a/UI/Scheduler/UIxCalDayTable.m b/UI/Scheduler/UIxCalDayTable.m index c20ed1838..b601134d2 100644 --- a/UI/Scheduler/UIxCalDayTable.m +++ b/UI/Scheduler/UIxCalDayTable.m @@ -284,9 +284,9 @@ return [daysToDisplay indexOfObject: currentTableDay]; } -- (NSString *) currentAppointmentHour +- (NSNumber *) currentAppointmentHour { - return [NSString stringWithFormat: @"%.2d00", [currentTableHour intValue]]; + return [NSNumber numberWithInt: [currentTableHour intValue]]; } - (NSString *) labelForDay diff --git a/UI/Templates/SchedulerUI/UIxCalDayTable.wox b/UI/Templates/SchedulerUI/UIxCalDayTable.wox index 838ceaa47..fbfe149b6 100644 --- a/UI/Templates/SchedulerUI/UIxCalDayTable.wox +++ b/UI/Templates/SchedulerUI/UIxCalDayTable.wox @@ -51,24 +51,28 @@
-
+
-
-
+ var:sg-day="currentTableDay.shortDateString"> + +
- +
@@ -80,18 +84,21 @@
- +
-
-
+
@@ -100,10 +107,12 @@ + var:sg-day-number="currentDayNumber" + var:sg-day="currentTableDay.shortDateString"> +
-
+
diff --git a/UI/Templates/SchedulerUI/UIxCalMainView.wox b/UI/Templates/SchedulerUI/UIxCalMainView.wox index bd359db97..7cf681302 100644 --- a/UI/Templates/SchedulerUI/UIxCalMainView.wox +++ b/UI/Templates/SchedulerUI/UIxCalMainView.wox @@ -551,7 +551,7 @@ - + refresh
diff --git a/UI/WebServerResources/js/Scheduler/Calendar.service.js b/UI/WebServerResources/js/Scheduler/Calendar.service.js index b558dd253..1d8293baa 100644 --- a/UI/WebServerResources/js/Scheduler/Calendar.service.js +++ b/UI/WebServerResources/js/Scheduler/Calendar.service.js @@ -34,7 +34,8 @@ $$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()), $Component: Component, $$Acl: Acl, - activeUser: Settings.activeUser() + activeUser: Settings.activeUser(), + $view: null }); return Calendar; // return constructor @@ -51,6 +52,10 @@ angular.module('SOGo.SchedulerUI', ['SOGo.Common']); } angular.module('SOGo.SchedulerUI') + .value('CalendarSettings', { + EventDragDayLength: 24 * 4, + EventDragHorizontalOffset: 3 + }) .factory('Calendar', Calendar.$factory); /** diff --git a/UI/WebServerResources/js/Scheduler/CalendarController.js b/UI/WebServerResources/js/Scheduler/CalendarController.js index b0273e6e3..38a3affaa 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarController.js @@ -6,25 +6,31 @@ /** * @ngInject */ - CalendarController.$inject = ['$scope', '$state', '$stateParams', '$timeout', '$interval', '$log', 'sgFocus', 'Calendar', 'Component', 'stateEventsBlocks']; - function CalendarController($scope, $state, $stateParams, $timeout, $interval, $log, focus, Calendar, Component, stateEventsBlocks) { - var vm = this; + CalendarController.$inject = ['$scope', '$rootScope', '$state', '$stateParams', 'Calendar', 'Component', 'stateEventsBlocks']; + function CalendarController($scope, $rootScope, $state, $stateParams, Calendar, Component, stateEventsBlocks) { + var vm = this, deregisterCalendarsList; vm.views = stateEventsBlocks; vm.changeView = changeView; // Refresh current view when the list of calendars is modified - $scope.$on('calendars:list', function() { + deregisterCalendarsList = $rootScope.$on('calendars:list', updateView); + + $scope.$on('$destroy', deregisterCalendarsList); + + function updateView() { // See stateEventsBlocks in Scheduler.app.js Component.$eventsBlocksForView($stateParams.view, $stateParams.day.asDate()).then(function(data) { vm.views = data; _.forEach(vm.views, function(view) { if (view.id) { + // Note: this can't be done in Component service since it would make Component dependent on + // the Calendar service and create a circular dependency view.calendar = new Calendar({ id: view.id, name: view.calendarName }); } }); }); - }); + } // Change calendar's view function changeView($event) { diff --git a/UI/WebServerResources/js/Scheduler/CalendarListController.js b/UI/WebServerResources/js/Scheduler/CalendarListController.js index 25031f680..b3550e8c6 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarListController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarListController.js @@ -6,8 +6,8 @@ /** * @ngInject */ - CalendarListController.$inject = ['$scope', '$timeout', '$state', '$mdDialog', 'Dialog', 'Preferences', 'Calendar', 'Component']; - function CalendarListController($scope, $timeout, $state, $mdDialog, Dialog, Preferences, Calendar, Component) { + CalendarListController.$inject = ['$rootScope', '$timeout', '$state', '$mdDialog', 'Dialog', 'Preferences', 'Calendar', 'Component']; + function CalendarListController($rootScope, $timeout, $state, $mdDialog, Dialog, Preferences, Calendar, Component) { var vm = this; vm.component = Component; @@ -25,6 +25,7 @@ vm.filteredBy = filteredBy; vm.sort = sort; vm.sortedBy = sortedBy; + vm.reload = reload; vm.cancelSearch = cancelSearch; vm.mode = { search: false }; @@ -39,10 +40,12 @@ }); // Refresh current list when the list of calendars is modified - $scope.$on('calendars:list', function() { + $rootScope.$on('calendars:list', function() { Component.$filter(vm.componentType, { reload: true }); }); + $rootScope.$on('calendar:dragend', updateComponentFromGhost); + // Switch between components tabs function selectComponentType(type, options) { if (options && options.reload || vm.componentType != type) { @@ -111,17 +114,21 @@ }); } - function newComponent($event) { + function newComponent($event, baseComponent) { var type = 'appointment', component; if (vm.componentType == 'tasks') type = 'task'; - component = new Component({ pid: 'personal', type: type }); + if (baseComponent) + component = baseComponent; + else + // TODO respect SOGoDefaultCalendar + component = new Component({ pid: 'personal', type: type }); // UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox or // UI/Templates/SchedulerUI/UIxTaskEditorTemplate.wox var templateUrl = 'UIx' + type.capitalize() + 'EditorTemplate'; - $mdDialog.show({ + return $mdDialog.show({ parent: angular.element(document.body), targetEvent: $event, clickOutsideToClose: true, @@ -135,6 +142,84 @@ }); } + // Adjust component or create new component through drag'n'drop + function updateComponentFromGhost($event) { + var component, pointerHandler, coordinates, delta, params; + + component = Component.$ghost.component; + pointerHandler = Component.$ghost.pointerHandler; + + if (component.isNew) { + coordinates = pointerHandler.currentEventCoordinates; + component.setDelta(coordinates.duration * 15); + newComponent(null, component).finally(function() { + $timeout(function() { + Component.$ghost.pointerHandler = null; + Component.$ghost.component = null; + }); + }); + } + else { + delta = pointerHandler.currentEventCoordinates.getDelta(pointerHandler.originalEventCoordinates); + params = { + days: delta.dayNumber, + start: delta.start * 15, + duration: delta.duration * 15 + }; + if (component.isException || !component.occurrenceId) + // Component is an exception to a recurrence or is not recurrent; + // Immediately perform the adjustments + component.$adjust(params).then(function() { + $rootScope.$emit('calendars:list'); + $timeout(function() { + Component.$ghost = {}; + }); + }); + else if (component.occurrenceId) { + $mdDialog.show({ + clickOutsideToClose: true, + escapeToClose: true, + locals: { + component: component, + params: params + }, + template: [ + '', + ' ', + '

' + l('editRepeatingItem') + '

', + '
', + '
', + ' ' + l('button_thisOccurrenceOnly') + '', + ' ' + l('button_allOccurrences') + '', + '
', + '
' + ].join(''), + controller: RecurrentComponentDialogController + }).then(function() { + $rootScope.$emit('calendars:list'); + }).finally(function() { + $timeout(function() { + Component.$ghost = {}; + }); + }); + } + } + + /** + * @ngInject + */ + RecurrentComponentDialogController.$inject = ['$scope', '$mdDialog', 'component', 'params']; + function RecurrentComponentDialogController($scope, $mdDialog, component, params) { + $scope.updateThisOccurrence = function() { + component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + }; + $scope.updateAllOccurrences = function() { + delete component.occurrenceId; + component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + }; + } + } + function filter(filterpopup) { Component.$filter(vm.componentType, { filterpopup: filterpopup }); } @@ -151,6 +236,10 @@ return Component['$query' + vm.componentType.capitalize()].sort == field; } + function reload() { + $rootScope.$emit('calendars:list'); + } + function cancelSearch() { vm.mode.search = false; Component.$filter(vm.componentType, { value: '' }); diff --git a/UI/WebServerResources/js/Scheduler/CalendarsController.js b/UI/WebServerResources/js/Scheduler/CalendarsController.js index 6f83b282c..30350f160 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarsController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarsController.js @@ -6,8 +6,8 @@ /** * @ngInject */ - CalendarsController.$inject = ['$scope', '$window', '$mdDialog', '$log', 'sgFocus', 'Dialog', 'sgSettings', 'Calendar', 'User', 'stateCalendars']; - function CalendarsController($scope, $window, $mdDialog, $log, focus, Dialog, Settings, Calendar, User, stateCalendars) { + CalendarsController.$inject = ['$rootScope', '$scope', '$window', '$mdDialog', '$log', 'sgFocus', 'Dialog', 'sgSettings', 'Calendar', 'User', 'stateCalendars']; + function CalendarsController($rootScope, $scope, $window, $mdDialog, $log, focus, Dialog, Settings, Calendar, User, stateCalendars) { var vm = this; vm.activeUser = Settings.activeUser; @@ -42,7 +42,7 @@ _.each(ids, function(id) { var calendar = Calendar.$get(id); calendar.$setActivation().then(function() { - $scope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); }); }); } @@ -77,7 +77,7 @@ // Unsubscribe without confirmation folder.$delete() .then(function() { - $scope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); }, function(data, status) { Dialog.alert(l('An error occured while deleting the calendar "%{0}".', folder.name), l(data.error)); @@ -88,7 +88,7 @@ .then(function() { folder.$delete() .then(function() { - $scope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); }, function(data, status) { Dialog.alert(l('An error occured while deleting the calendar "%{0}".', folder.name), l(data.error)); diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index a71bcefdb..61b54a86d 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -46,7 +46,8 @@ $queryEvents: { sort: 'start', asc: 1, filterpopup: 'view_next7' }, // Filter parameters specific to tasks $queryTasks: { sort: 'status', asc: 1, filterpopup: 'view_incomplete' }, - $refreshTimeout: null + $refreshTimeout: null, + $ghost: {} }); Preferences.ready().then(function() { // Initialize filter parameters from user's settings @@ -274,7 +275,7 @@ * @returns a promise of a collection of objects describing the events blocks */ Component.$eventsBlocks = function(view, startDate, endDate) { - var params, futureComponentData, i, dates = [], + var params, futureComponentData, i, j, dates = [], deferred = Component.$q.defer(); params = { view: view.toLowerCase(), sd: startDate.getDayString(), ed: endDate.getDayString() }; @@ -287,12 +288,14 @@ var componentData = _.object(this.eventsFields, eventData), start = new Date(componentData.c_startdate * 1000); componentData.hour = start.getHourString(); + componentData.blocks = []; objects.push(new Component(componentData)); return objects; }; associateComponent = function(block) { - block.component = this[block.nbr]; + this[block.nbr].blocks.push(block); // Associate block to component + block.component = this[block.nbr]; // Associate component to block }; Component.$views = []; @@ -300,6 +303,12 @@ _.forEach(views, function(data) { var components = [], blocks = {}, allDayBlocks = {}, viewData; + // Change some attributes names + data.eventsFields.splice(_.indexOf(data.eventsFields, 'c_folder'), 1, 'pid'); + data.eventsFields.splice(_.indexOf(data.eventsFields, 'c_name'), 1, 'id'); + data.eventsFields.splice(_.indexOf(data.eventsFields, 'c_recurrence_id'), 1, 'occurrenceId'); + data.eventsFields.splice(_.indexOf(data.eventsFields, 'c_title'), 1, 'summary'); + // Instantiate Component objects _.reduce(data.events, reduceComponent, components, data); @@ -318,14 +327,32 @@ // Convert array of blocks to object with days as keys for (i = 0; i < data.blocks.length; i++) { + for (j = 0; j < data.blocks[i].length; j++) + data.blocks[i][j].dayNumber = i; blocks[dates[i]] = data.blocks[i]; } // Convert array of all-day blocks to object with days as keys for (i = 0; i < data.allDayBlocks.length; i++) { + for (j = 0; j < data.allDayBlocks[i].length; j++) + data.allDayBlocks[i][j].dayNumber = i; allDayBlocks[dates[i]] = data.allDayBlocks[i]; } + // "blocks" is now an object literal with the following structure: + // { day: [ + // { start: number, + // length: number, + // siblings: number, + // realSiblings: number, + // position: number, + // nbr: number, + // component: Component }, + // .. ], + // .. } + // + // Where day is a string with format YYYYMMDD + Component.$log.debug('blocks ready (' + _.flatten(data.blocks).length + ')'); Component.$log.debug('all day blocks ready (' + _.flatten(data.allDayBlocks).length + ')'); @@ -360,6 +387,9 @@ return futureComponentData.then(function(data) { return Component.$timeout(function() { var fields = _.invoke(data.fields, 'toLowerCase'); + fields.splice(_.indexOf(fields, 'c_folder'), 1, 'pid'); + fields.splice(_.indexOf(fields, 'c_name'), 1, 'id'); + fields.splice(_.indexOf(fields, 'c_recurrence_id'), 1, 'occurrenceId'); // Instanciate Component objects _.reduce(data[type], function(components, componentData, i) { @@ -582,7 +612,7 @@ * @returns true if the percent completion should be displayed */ Component.prototype.enablePercentComplete = function() { - return (this.component = 'vtodo' && + return (this.type == 'task' && this.status != 'not-specified' && this.status != 'cancelled'); }; @@ -718,7 +748,7 @@ Component.prototype.getClassName = function(base) { if (angular.isUndefined(base)) base = 'fg'; - return base + '-folder' + (this.destinationCalendar || this.c_folder); + return base + '-folder' + (this.destinationCalendar || this.c_folder || this.pid); }; /** @@ -884,7 +914,7 @@ }; /** - * @function reply + * @function $reply * @memberof Component.prototype * @desc Reply to an invitation. * @returns a promise of the HTTP operation @@ -909,6 +939,27 @@ }); }; + /** + * @function $adjust + * @memberof Component.prototype + * @desc Adjust the start, day, and/or duration of the component + * @returns a promise of the HTTP operation + */ + Component.prototype.$adjust = function(params) { + var path = [this.pid, this.id]; + + if (_.every(_.values(params), function(v) { return v === 0; })) + // No changes + return Component.$q.when(); + + if (this.occurrenceId) + path.push(this.occurrenceId); + + Component.$log.debug('adjust ' + path.join('/') + ' ' + JSON.stringify(params)); + + return Component.$$resource.save(path.join('/'), params, { action: 'adjust' }); + }; + /** * @function $save * @memberof Component.prototype @@ -979,7 +1030,9 @@ Component.prototype.$omit = function() { var component = {}, date; angular.forEach(this, function(value, key) { - if (key != 'constructor' && key[0] != '$') { + if (key != 'constructor' && + key[0] != '$' && + key != 'blocks') { component[key] = angular.copy(value); } }); diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index aa65643cf..d41abb37e 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -23,7 +23,7 @@ // Load all attributes of component if (angular.isUndefined(vm.component.$futureComponentData)) { - component = Calendar.$get(vm.component.c_folder).$getComponent(vm.component.c_name, vm.component.c_recurrence_id); + component = Calendar.$get(vm.component.pid).$getComponent(vm.component.id, vm.component.occurrenceId); component.$futureComponentData.then(function() { vm.component = component; vm.organizer = [vm.component.organizer]; @@ -72,7 +72,7 @@ var c = component || vm.component; c.$reply().then(function() { - $rootScope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); $mdDialog.hide(); Alarm.getAlarms(); }); @@ -94,14 +94,14 @@ function deleteOccurrence() { vm.component.remove(true).then(function() { - $rootScope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); $mdDialog.hide(); }); } function deleteAllOccurrences() { vm.component.remove().then(function() { - $rootScope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); $mdDialog.hide(); }); } @@ -222,7 +222,7 @@ if (form.$valid) { vm.component.$save() .then(function(data) { - $rootScope.$broadcast('calendars:list'); + $rootScope.$emit('calendars:list'); $mdDialog.hide(); Alarm.getAlarms(); }, function(data, status) { @@ -237,7 +237,7 @@ // Cancelling the creation of a component vm.component = null; } - $mdDialog.hide(); + $mdDialog.cancel(); } function getDays() { diff --git a/UI/WebServerResources/js/Scheduler/Scheduler.app.js b/UI/WebServerResources/js/Scheduler/Scheduler.app.js index d78133b0c..6813f1dc8 100644 --- a/UI/WebServerResources/js/Scheduler/Scheduler.app.js +++ b/UI/WebServerResources/js/Scheduler/Scheduler.app.js @@ -29,8 +29,8 @@ }) .state('calendars.view', { url: '/{view:(?:day|week|month|multicolumnday)}/:day', - sticky: true, - deepStateRedirect: true, + //sticky: true, + //deepStateRedirect: true, views: { calendarView: { templateUrl: function($stateParams) { @@ -93,6 +93,8 @@ .then(function(views) { _.forEach(views, function(view) { if (view.id) { + // Note: this can't be done in Component service since it would make Component dependent on + // the Calendar service and create a circular dependency view.calendar = new Calendar({ id: view.id, name: view.calendarName }); } }); diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarDay.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarDay.directive.js new file mode 100644 index 000000000..841039ee6 --- /dev/null +++ b/UI/WebServerResources/js/Scheduler/sgCalendarDay.directive.js @@ -0,0 +1,51 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + /* jshint validthis: true */ + 'use strict'; + + /* + * sgCalendarDay - An element that represents a day in the calendar's view + * @memberof SOGo.SchedulerUI + * @restrict element + * @param {string} sgDay - the day of the events to display (YYYYMMDD) + * @param {string} sgDayString - the day in ISO8601 format (YYYY-MM-DDTHH:MM+-HH:MM) + * @param {number} sgDayNumber - the day index within the calendar's view + * + * @example: + + + .. + + */ + function sgCalendarDay() { + return { + restrict: 'E', + scope: { + day: '@sgDay', + dayNumber: '@sgDayNumber', + dayString: '@sgDayString' + }, + controller: sgCalendarDayController + }; + } + + /** + * @ngInject + */ + sgCalendarDayController.$inject = ['$scope']; + function sgCalendarDayController($scope) { + // Expose some scope variables to the controller + // See the sgCalendarDayTable directive + this.day = $scope.day; + this.dayNumber = $scope.dayNumber; + this.dayString = $scope.dayString; + } + + angular + .module('SOGo.SchedulerUI') + .directive('sgCalendarDay', sgCalendarDay); +})(); diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarDayBlock.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarDayBlock.directive.js index 8578b192f..c4975c5ea 100644 --- a/UI/WebServerResources/js/Scheduler/sgCalendarDayBlock.directive.js +++ b/UI/WebServerResources/js/Scheduler/sgCalendarDayBlock.directive.js @@ -5,7 +5,7 @@ /* * sgCalendarDayBlock - An event block to be displayed in a week - * @memberof SOGo.Common + * @memberof SOGo.SchedulerUI * @restrict element * @param {object} sgBlock - the event block definition * @param {function} sgClick - the function to call when clicking on a block. @@ -19,7 +19,8 @@ sg-block="block" sg-click="open(clickEvent, clickComponent)" /> */ - function sgCalendarDayBlock() { + sgCalendarDayBlock.$inject = ['CalendarSettings']; + function sgCalendarDayBlock(CalendarSettings) { return { restrict: 'E', scope: { @@ -28,33 +29,33 @@ }, replace: true, template: [ - '
', + '
', '
', - '
', - '
', - '
{{ block.component.c_title }}', - ' ', - // Component has an alarm - ' ', - // Component is confidential - ' ', - // Component is private - ' ', - '
', + '
{{ block.component.summary }}', + ' ', + // Component has an alarm + ' ', + // Component is confidential + ' ', + // Component is private + ' ', + ' ', '
', - '
', - '
', + '
', '
' ].join(''), link: link }; function link(scope, iElement, attrs) { - // Compute overlapping (2%) - var pc = 100 / scope.block.siblings, - left = scope.block.position * pc, - right = 100 - (scope.block.position + 1) * pc; + var pc, left, right; + // Compute overlapping (2%) + pc = 100 / scope.block.siblings; + left = scope.block.position * pc; + right = 100 - (scope.block.position + 1) * pc; if (pc < 100) { if (left > 0) left -= 2; @@ -73,7 +74,9 @@ iElement.css('right', right + '%'); iElement.addClass('starts' + scope.block.start); iElement.addClass('lasts' + scope.block.length); - iElement.addClass('bg-folder' + scope.block.component.c_folder); + + // Set background color + iElement.addClass('bg-folder' + scope.block.component.pid); } } diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarDayTable.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarDayTable.directive.js index e2506a3e0..a41d3930c 100644 --- a/UI/WebServerResources/js/Scheduler/sgCalendarDayTable.directive.js +++ b/UI/WebServerResources/js/Scheduler/sgCalendarDayTable.directive.js @@ -5,7 +5,7 @@ /* * sgCalendarDayTable - Build list of blocks for a specific day - * @memberof SOGo.Common + * @memberof SOGo.SchedulerUI * @restrict element * @param {object} sgBlocks - the events blocks definitions for the current view * @param {string} sgDay - the day of the events to display @@ -29,7 +29,7 @@ clickBlock: '&sgClick' }, template: [ - '' diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarMonthEvent.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarMonthEvent.directive.js index 54f25045c..f842453d2 100644 --- a/UI/WebServerResources/js/Scheduler/sgCalendarMonthEvent.directive.js +++ b/UI/WebServerResources/js/Scheduler/sgCalendarMonthEvent.directive.js @@ -26,7 +26,7 @@ template: [ '
', ' {{ block.starthour }} - ', - ' {{ block.component.c_title }}', + ' {{ block.component.summary }}', ' ', ' ', ' ', @@ -40,7 +40,7 @@ }; function link(scope, iElement, attrs) { - iElement.addClass('bg-folder' + scope.block.component.c_folder); + iElement.addClass('bg-folder' + scope.block.component.pid); } } diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarScrollView.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarScrollView.directive.js new file mode 100644 index 000000000..059f167cb --- /dev/null +++ b/UI/WebServerResources/js/Scheduler/sgCalendarScrollView.directive.js @@ -0,0 +1,181 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + /* jshint validthis: true */ + 'use strict'; + + /* + * sgCalendarScrollView - scrollable view that contains draggable elements + * @memberof SOGo.SchedulerUI + * @restrict attribute + * @param {string} sgCalendarScrollView - the view type (multiday, multiday-allday, or monthly) + * + * @example: + + + .. + + */ + sgCalendarScrollView.$inject = ['$rootScope', '$window', '$document', '$q', '$timeout', '$mdGesture', 'Calendar', 'Component']; + function sgCalendarScrollView($rootScope, $window, $document, $q, $timeout, $mdGesture, Calendar, Component) { + return { + restrict: 'A', + scope: { + type: '@sgCalendarScrollView' + }, + controller: sgCalendarScrollViewController, + link: function(scope, element, attrs, controller) { + var view, scrollView, type, lastScroll, deregisterDragStart, deregisterDragStop; + + scrollView = element[0]; + type = scope.type; // multiday, multiday-allday, monthly, unknown? + lastScroll = 0; + + // Listen to dragstart and dragend events + deregisterDragStart = $rootScope.$on('calendar:dragstart', onDragStart); + deregisterDragStop = $rootScope.$on('calendar:dragend', onDragEnd); + + // Update the "view" object literal once the Angular template has been transformed + $timeout(initView); + + // Deregister listeners when destroying the view + scope.$on('$destroy', function() { + deregisterDragStart(); + deregisterDragStop(); + element.off('mouseover', updateFromPointerHandler); + angular.element($window).off('resize', updateCoordinates); + }); + + function initView() { + var quarterHeight; + + // Quarter height doesn't change if window is resize; compute it only once + quarterHeight = getQuarterHeight(); + + view = { + type: type, + quarterHeight: quarterHeight, + scrollStep: 6 * quarterHeight, + maxX: getMaxColumns(), + + // Expose a reference of the view element + element: scrollView + }; + + // Compute coordinates of view element; recompute it on window resize + angular.element($window).on('resize', updateCoordinates); + updateCoordinates(); + } + + function getQuarterHeight() { + var hour0, hour23, height; + + hour0 = document.getElementById('hour0'); + hour23 = document.getElementById('hour23'); + height = ((hour23.offsetTop - hour0.offsetTop) / (23 * 4)); + + return height; + } + + function getDayWidth(viewLeft) { + var width, offset, nodes, domRect; + + width = 0; + offset = 0; + nodes = scrollView.getElementsByClassName('day0'); + + if (nodes.length > 0) { + domRect = nodes[0].getBoundingClientRect(); + width = domRect.width; + offset = domRect.left - viewLeft; + } + + return [width, offset]; + } + + function getMaxColumns() { + var max = 0; + + //if (type == 'multiday') { + max = scrollView.getElementsByClassName('day').length - 1; + //} + + return max; + } + + // View has been resized; + // Compute the view's origins (x, y), a day's width (dayWidth) and the left margin (daysOffset). + function updateCoordinates() { + var domRect, dayWidth; + + domRect = scrollView.getBoundingClientRect(); + dayWidth = getDayWidth(domRect.left); + + angular.extend(view, { + coordinates: { + x: domRect.left, + y: domRect.top + }, + dayWidth: dayWidth[0], + daysOffset: dayWidth[1] + }); + } + + function onDragStart() { + element.on('mouseover', updateFromPointerHandler); + updateFromPointerHandler(); + } + + function onDragEnd() { + element.off('mouseover', updateFromPointerHandler); + Calendar.$view = null; + } + + // From SOGoScrollController.updateFromPointerHandler + function updateFromPointerHandler() { + var scrollStep, pointerHandler, pointerCoordinates, now, scrollY, minY, delta; + + scrollStep = view.scrollStep; + pointerHandler = Component.$ghost.pointerHandler; + if (pointerHandler) { + pointerCoordinates = pointerHandler.getContainerBasedCoordinates(view); + + if (pointerCoordinates) { + // Pointer is inside view; Adjust scrollbar if necessary + Calendar.$view = view; + now = new Date().getTime(); + if (!lastScroll || now > lastScroll + 100) { + lastScroll = now; + scrollY = pointerCoordinates.y - scrollStep; + if (scrollY < 0) { + minY = -scrollView.scrollTop; + if (scrollY < minY) + scrollY = minY; + scrollView.scrollTop += scrollY; + } + else { + scrollY = pointerCoordinates.y + scrollStep; + delta = scrollY - scrollView.clientHeight; + if (delta > 0) { + scrollView.scrollTop += delta; + } + } + } + } + } + } + } + }; + } + + sgCalendarScrollViewController.$inject = ['$scope']; + function sgCalendarScrollViewController($scope) { + // Expose the view type to the controller + // See sgCalendarDayBlockGhost + this.type = $scope.type; + } + + angular + .module('SOGo.SchedulerUI') + .directive('sgCalendarScrollView', sgCalendarScrollView); +})(); diff --git a/UI/WebServerResources/js/Scheduler/sgDraggableCalendarBlock.directive.js b/UI/WebServerResources/js/Scheduler/sgDraggableCalendarBlock.directive.js new file mode 100644 index 000000000..c51a71d18 --- /dev/null +++ b/UI/WebServerResources/js/Scheduler/sgDraggableCalendarBlock.directive.js @@ -0,0 +1,516 @@ +/* -*- 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', 'Calendar', 'CalendarSettings', 'Component']; + function sgDraggableCalendarBlock($rootScope, $timeout, $log, Calendar, CalendarSettings, Component) { + return { + restrict: 'CA', + require: '^sgCalendarDay', + link: link + }; + + function link(scope, element, attrs, calendarDayCtrl) { + if (scope.block) + // Add dragging grips to existing event block + initGrips(); + + // Start dragging on mousedown + element.on('mousedown', onDragStart); + + // Deregister mousedown when removing the element from the DOM + scope.$on('$destroy', function() { + element.off('mousedown', onDragStart); + }); + + function initGrips() { + var component, dayNumber, blockIndex, isFirstBlock, isLastBlock, + dragGrip, leftGrip, rightGrip, topGrip, bottomGrip; + + component = scope.block.component; + dayNumber = scope.block.dayNumber; + blockIndex = _.findIndex(component.blocks, _.matchesProperty('dayNumber', dayNumber)); + isFirstBlock = (blockIndex === 0); + isLastBlock = (blockIndex === component.blocks.length - 1); + + dragGrip = angular.element('
'); + dragGrip.addClass('bdr-folder' + component.pid); + + if (component.c_isallday) { + 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 onDragStart(ev) { + var block, dragMode, eventType, startDate, newData, newComponent, pointerHandler; + + dragMode = 'move-event'; + eventType = 'multiday'; + + // Stop dragging on the next "mouseup" + angular.element(document).one('mouseup', onDragEnd); + + if (scope.block && scope.block.component) { + // Move or resize existing component + block = scope.block; + 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'; + startDate = new Date(calendarDayCtrl.dayString.substring(0,10) + + ' ' + + calendarDayCtrl.dayString.substring(11,16)); + startDate.setHours(parseInt(element.attr('sg-hour'))); + newData = { + type: 'appointment', + pid: 'personal', // TODO respect SOGoDefaultCalendar + summary: l('New Event'), + startDate: startDate + }; + newComponent = new Component(newData); + block = { + component: newComponent, + start: parseInt(element.attr('sg-hour')) * 4, + dayNumber: calendarDayCtrl.dayNumber, + length: 0 + }; + block.component.blocks = [block]; + } + + // Mark all blocks as being dragged + _.forEach(block.component.blocks, function(b) { + b.dragging = true; + }); + + if (block.component.c_isallday) + eventType = 'multiday-allday'; + + // Initialize pointer handler + pointerHandler = new SOGoEventDragPointerHandler(dragMode); + pointerHandler.prepareWithEventType(eventType); + pointerHandler.initFromEvent(ev); + pointerHandler.initFromBlock(block); + + // Update Component.$ghost + Component.$ghost.component = block.component; + Component.$ghost.pointerHandler = pointerHandler; + + angular.element(document).on('mousemove', onDrag); + } + + 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); + angular.element(document).off('mouseup', onDragEnd); + + if (pointer.dragHasStarted) { + $rootScope.$emit('calendar:dragend'); + pointer.dragHasStarted = false; + } + + // Unmark all blocks as being dragged + if (block) + _.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; + + 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() { + } + + SOGoEventDragEventCoordinates.prototype = { + dayNumber: -1, + start: -1, + duration: -1, + + eventType: null, + + setEventType: function(eventType) { + this.eventType = eventType; + }, + + initFromBlock: function(block) { + // Get the start (first quarter) from the event's first block + this.start = block.component.blocks[0].start; + + // Compute overall length + this.duration = _.sum(block.component.blocks, function(b) { + return b.length; + }); + + // Get the dayNumber from the event's first block + this.dayNumber = block.component.blocks[0].dayNumber; + }, + + 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 + originalViewCoordinates: null, + currentViewCoordinates: null, + + // Event start-duration coordinates + originalEventCoordinates: null, + currentEventCoordinates: null, + + dragHasStarted: false, + + // Function to return the day and quarter coordinates of the pointer cursor + // within the day view + getEventViewCoordinates: null, + + initFromEvent: function SEDPH_initFromEvent(event) { + this.currentCoordinates = new SOGoCoordinates(); + this.updateFromEvent(event); + this.originalCoordinates = this.currentCoordinates.clone(); + }, + + initFromBlock: function SEDPH_initFromBlock(block) { + this.currentEventCoordinates = new SOGoEventDragEventCoordinates(); + this.originalEventCoordinates = new SOGoEventDragEventCoordinates(); + this.originalEventCoordinates.initFromBlock(block); + }, + + // 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 (!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) { + // Emit 'dragstart' event only if pointer has moved from at least 3 pixels + this.dragHasStarted = true; + $rootScope.$emit('calendar:dragstart'); + } + } + }, + + // 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.x * CalendarSettings.EventDragDayLength + delta.y; + $log.debug('quarters delta ' + deltaQuarters); + + // 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 dayNumber needs to be updated with the calendar number. + // if (currentView == "multicolumndayview") + // this._updateMulticolumnViewDayNumber_SEDGC(); + 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 maxX = Calendar.$view.maxX; + if (coordinates.x < 0) + coordinates.x = 0; + else if (coordinates.x > maxX) + coordinates.x = maxX; + coordinates.y = 0; + } + else { + coordinates = null; + } + + return coordinates; + }, + // getEventMonthlyViewCoordinates: function SEDPH_gEMonthlyViewC() { + // /* x = day; y = quarter */ + // var coordinates; + + // var pxCoordinates = this.getContainerBasedCoordinates(); + // if (pxCoordinates) { + // coordinates = new SOGoCoordinates(); + // var utilities = SOGoEventDragUtilities(); + // var daysOffset = utilities.getDaysOffset(); + // var daysTopOffset = daysOffset; /* change later */ + // var dayHeight = utilities.getDayHeight(); + // var daysY = Math.floor((pxCoordinates.y - daysTopOffset) / dayHeight); + // if (daysY < 0) + // daysY = 0; + // var dayWidth = utilities.getDayWidth(); + + // coordinates.x = Math.floor((pxCoordinates.x - daysOffset) / dayWidth); + // if (coordinates.x < 0) + // coordinates.x = 0; + // else if (coordinates.x > 6) + // coordinates.x = 6; + // coordinates.x += 7 * daysY; + // coordinates.y = 0; + // } else { + // coordinates = null; + // } + + // return coordinates; + // }, + + getDistance: function SEDPH_getDistance() { + return this.currentCoordinates.getDistance(this.originalCoordinates); + } + }; + } + } + + angular + .module('SOGo.SchedulerUI') + .directive('sgDraggableCalendarBlock', sgDraggableCalendarBlock); +})(); + diff --git a/UI/WebServerResources/scss/views/SchedulerUI.scss b/UI/WebServerResources/scss/views/SchedulerUI.scss index 7382534eb..df7ebe7d6 100644 --- a/UI/WebServerResources/scss/views/SchedulerUI.scss +++ b/UI/WebServerResources/scss/views/SchedulerUI.scss @@ -1,6 +1,6 @@ /// SchedulerUI.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*- $hours_margin: 50px; -$scrollbar_width: 16px; +$block_margin: 2%; // See sgCalendarDayBlock.directive.js /** * Affected templates: @@ -64,10 +64,13 @@ $scrollbar_width: 16px; z-index: $z-index-toolbar - 1; .days { margin-left: $hours_margin; - margin-right: $scrollbar_width; // scrollbar + overflow-y: scroll; &.dayLabels { .day { padding-left: 1%; + div { + overflow: hidden; + } } } } @@ -80,11 +83,11 @@ $scrollbar_width: 16px; } } } -// &.monthView { -// > div { -// margin-right: $scrollbar_width; -// } -// } + &.monthView { + > div { + overflow-y: scroll; + } + } } // The all-day events appear in the shrinkable toolbar, bellow the days labels @@ -92,17 +95,50 @@ $scrollbar_width: 16px; border-bottom: 1px solid sg-color($sogoPaper, 300); max-height: $sg-font-size-4 * 6; - .sg-event { - margin: 2%; // See sgCalendarDayBlock.directive.js - line-height: initial; + .day { position: relative; } - .gradient, .text { + .sg-event { + margin: $block_margin; + line-height: initial; + position: relative; + + &--ghost { + position: absolute; + top: 0; + left: 0; + right: 0; + margin-left: 0; + margin-right: 0; + border-radius: 0; + padding-left: $block_margin; + padding-right: $block_margin; + &--first { + margin-left: $block_margin; + padding-left: 0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + &--last { + margin-right: $block_margin; + padding-right: 0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + } + } + + .text { position: relative; } } + md-content { + overflow-y: scroll; + overflow-x: hidden; + } + // Days row .days { display: flex; @@ -115,21 +151,24 @@ $scrollbar_width: 16px; .clickableHourCell { height: 40px; border-bottom: 1px solid sg-color($sogoPaper, 300); + user-select: none; + &.outOfDay { + background-color: $colorGrey50; + } } } } // Header of month tiles .sg-calendar-tile-header { + font-size: $sg-font-size-2; + min-height: $sg-font-size-2 + 8px; overflow: hidden; + padding: 4px; + text-align: right; text-overflow: ellipsis; white-space: nowrap; - font-size: $sg-font-size-2; - padding: 4px; - min-height: $sg-font-size-2 + 8px; - text-align: right; //font-weight: $sg-font-light; - overflow: hidden; } // The left column of hours @@ -173,7 +212,8 @@ $scrollbar_width: 16px; } // Events from editable calendars are draggable - .sg-draggable { + .sg-draggable-calendar-block, + .sg-event--ghost { cursor: move; } @@ -183,16 +223,135 @@ $scrollbar_width: 16px; left: 0; right: 0; opacity: 0.9; // When events from a same calendar overlap, it creates a border to help distinguish the events - overflow: hidden; + user-select: none; + transition: $swift-linear; $i: 0; @while $i < 96 { // number of 15-minutes blocks in a day &.starts#{$i} { top: 10px * $i; } &.lasts#{$i} { height: 10px * $i; } $i: $i + 1; } + + &--notransition { + transition: none; + } + + &--ghost { + opacity: 1; + left: $block_margin; + right: $block_margin; + } + + &--dragging { + background-image: repeating-linear-gradient(-45deg, + rgba(255,255,255,0.2), + rgba(255,255,255,0.2) 2px, + transparent 2px, + transparent 4px ); + opacity: 0.5; + } + .eventInside { overflow: hidden; } + + // Event DnD drag grips + + &:hover { + .dragGrip { + &-top, &-bottom { + display: block; + cursor: ns-resize; + } + &-left, &-right { + display: block; + cursor: ew-resize; + } + } + } + + .dragGrip { + &-top, &-bottom, &-left, &-right { + display: none; + position: absolute; + } + + &-top, &-bottom { + left: 1px; + right: 1px; + height: 10px; + line-height: 8px; + } + + &-top { + top: 0; + .dragGrip { + right: 0; + top: -3px; + } + } + + &-bottom { + bottom: 0; + .dragGrip { + bottom: -3px; + } + } + + &-left, &-right { + top: 1px; + bottom: 1px; + width: 10px; + line-height: 8px; + } + + &-left { + left: -$block_margin; + .dragGrip { + bottom: 0; + left: -1px; + } + } + + &-right { + right: -$block_margin; + .dragGrip { + right: -1px; + } + } + } + + .dragGrip { + background-color: white; + border-radius: 50%; + border-style: solid; + border-width: 1px; + display: inline-block; + height: 8px; + position: absolute; + width: 8px; + } + + // Event DnD ghost start/end hours + + .ghostStartHour, + .ghostEndHour { + position: absolute; + width: 100%; + vertical-align: baseline; + height: 14px; + left: 0px; + color: #222; + text-align: center; + } + + .ghostStartHour { + top: -14px; + } + + .ghostEndHour { + bottom: -14px; + } } // Multicolumn day cell that contains the calendar name @@ -250,8 +409,8 @@ $scrollbar_width: 16px; position: relative; border-radius: 1px; overflow: hidden; - padding: 2%; - margin: 2%; + padding: $block_margin; + margin: $block_margin; text-overflow: ellipsis; white-space: nowrap; span { // hours @@ -269,13 +428,15 @@ $scrollbar_width: 16px; border-bottom: 1px dotted sg-color($sogoPaper, 300); } - .gradient, .text { + .text { position: absolute; - top: 1px; - left: 4px; - right: 2px; - bottom: 1px; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0 2px; overflow: hidden; + line-height: $sg-font-size-2; } .gradient > IMG { @@ -311,6 +472,7 @@ $scrollbar_width: 16px; } md-list-item { padding-left: 0; + padding-right: 0; &:hover { background-color: initial; }