TODO: - drag'n'drop in month's view; - drag to create an all-day event; - drag'n'drop to a calendar in the sidenav; - touchscreen gestures support.
1118 lines
37 KiB
JavaScript
1118 lines
37 KiB
JavaScript
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
/**
|
|
* @name Component
|
|
* @constructor
|
|
* @param {object} futureComponentData - either an object literal or a promise
|
|
*/
|
|
function Component(futureComponentData) {
|
|
// Data is immediately available
|
|
if (typeof futureComponentData.then !== 'function') {
|
|
this.init(futureComponentData);
|
|
if (this.pid && !this.id) {
|
|
// Prepare for the creation of a new component;
|
|
// Get UID from the server.
|
|
var newComponentData = Component.$$resource.newguid(this.pid);
|
|
this.$unwrap(newComponentData);
|
|
this.isNew = true;
|
|
}
|
|
}
|
|
else {
|
|
// The promise will be unwrapped first
|
|
this.$unwrap(futureComponentData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @memberof Component
|
|
* @desc The factory we'll use to register with Angular
|
|
* @returns the Component constructor
|
|
*/
|
|
Component.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Preferences', 'Gravatar', 'Resource', function($q, $timeout, $log, Settings, Preferences, Gravatar, Resource) {
|
|
angular.extend(Component, {
|
|
$q: $q,
|
|
$timeout: $timeout,
|
|
$log: $log,
|
|
$Preferences: Preferences,
|
|
$gravatar: Gravatar,
|
|
$$resource: new Resource(Settings.baseURL(), Settings.activeUser()),
|
|
timeFormat: "%H:%M",
|
|
// Filter parameters common to events and tasks
|
|
$query: { value: '', search: 'title_Category_Location' },
|
|
// Filter paramaters specific to events
|
|
$queryEvents: { sort: 'start', asc: 1, filterpopup: 'view_next7' },
|
|
// Filter parameters specific to tasks
|
|
$queryTasks: { sort: 'status', asc: 1, filterpopup: 'view_incomplete' },
|
|
$refreshTimeout: null,
|
|
$ghost: {}
|
|
});
|
|
Preferences.ready().then(function() {
|
|
// Initialize filter parameters from user's settings
|
|
if (Preferences.settings.Calendar.EventsFilterState)
|
|
Component.$queryEvents.filterpopup = Preferences.settings.Calendar.EventsFilterState;
|
|
if (Preferences.settings.Calendar.TasksFilterState)
|
|
Component.$queryTasks.filterpopup = Preferences.settings.Calendar.TasksFilterState;
|
|
if (Preferences.settings.Calendar.EventsSortingState) {
|
|
Component.$queryEvents.sort = Preferences.settings.Calendar.EventsSortingState[0];
|
|
Component.$queryEvents.asc = parseInt(Preferences.settings.Calendar.EventsSortingState[1]);
|
|
}
|
|
if (Preferences.settings.Calendar.TasksSortingState) {
|
|
Component.$queryTasks.sort = Preferences.settings.Calendar.TasksSortingState[0];
|
|
Component.$queryTasks.asc = parseInt(Preferences.settings.Calendar.TasksSortingState[1]);
|
|
}
|
|
Component.$queryTasks.show_completed = parseInt(Preferences.settings.ShowCompletedTasks);
|
|
// Initialize categories from user's defaults
|
|
Component.$categories = Preferences.defaults.SOGoCalendarCategoriesColors;
|
|
// Initialize time format from user's defaults
|
|
if (Preferences.defaults.SOGoTimeFormat) {
|
|
Component.timeFormat = Preferences.defaults.SOGoTimeFormat;
|
|
}
|
|
});
|
|
|
|
return Component; // return constructor
|
|
}];
|
|
|
|
/**
|
|
* @module SOGo.SchedulerUI
|
|
* @desc Factory registration of Component in Angular module.
|
|
*/
|
|
try {
|
|
angular.module('SOGo.SchedulerUI');
|
|
}
|
|
catch(e) {
|
|
angular.module('SOGo.SchedulerUI', ['SOGo.Common']);
|
|
}
|
|
angular.module('SOGo.SchedulerUI')
|
|
.factory('Component', Component.$factory);
|
|
|
|
/**
|
|
* @function $selectedCount
|
|
* @memberof Component
|
|
* @desc Return the number of events or tasks selected by the user.
|
|
* @returns the number of selected events or tasks
|
|
*/
|
|
Component.$selectedCount = function() {
|
|
var count;
|
|
|
|
count = 0;
|
|
if (Component.$events) {
|
|
count = (_.filter(Component.$events, function(event) { return event.selected; })).length;
|
|
}
|
|
if (Component.$tasks) {
|
|
count = (_.filter(Component.$tasks, function(task) { return task.selected; })).length;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
/**
|
|
* @function $startRefreshTimeout
|
|
* @memberof Component
|
|
* @desc Starts the refresh timeout for the current selected component type, for all calendars
|
|
*/
|
|
Component.$startRefreshTimeout = function(type) {
|
|
var _this = this;
|
|
|
|
if (Component.$refreshTimeout)
|
|
Component.$timeout.cancel(Component.$refreshTimeout);
|
|
|
|
Component.$Preferences.ready().then(function() {
|
|
// Restart the refresh timer, if needed
|
|
var refreshViewCheck = Component.$Preferences.defaults.SOGoRefreshViewCheck;
|
|
if (refreshViewCheck && refreshViewCheck != 'manually') {
|
|
var f = angular.bind(_this, Component.$filter, type);
|
|
Component.$refreshTimeout = Component.$timeout(f, refreshViewCheck.timeInterval()*1000);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $filter
|
|
* @memberof Component
|
|
* @desc Search for components matching some criterias
|
|
* @param {string} type - either 'events' or 'tasks'
|
|
* @param {object} [options] - additional options to the query
|
|
* @returns a collection of Components instances
|
|
*/
|
|
Component.$filter = function(type, options) {
|
|
var _this = this,
|
|
now = new Date(),
|
|
day = now.getDate(),
|
|
month = now.getMonth() + 1,
|
|
year = now.getFullYear(),
|
|
queryKey = '$query' + type.capitalize(),
|
|
params = {
|
|
day: '' + year + (month < 10?'0':'') + month + (day < 10?'0':'') + day,
|
|
};
|
|
|
|
Component.$startRefreshTimeout(type);
|
|
|
|
return this.$Preferences.ready().then(function() {
|
|
var futureComponentData,
|
|
dirty = false,
|
|
otherType;
|
|
|
|
angular.extend(_this.$query, params);
|
|
|
|
if (options) {
|
|
_.each(_.keys(options), function(key) {
|
|
// Query parameters common to events and tasks are compared
|
|
dirty |= (_this.$query[key] && options[key] != Component.$query[key]);
|
|
if (key == 'reload' && options[key])
|
|
dirty = true;
|
|
// Update either the common parameters or the type-specific parameters
|
|
else if (angular.isDefined(_this.$query[key]))
|
|
_this.$query[key] = options[key];
|
|
else
|
|
_this[queryKey][key] = options[key];
|
|
});
|
|
}
|
|
|
|
// Perform query with both common and type-specific parameters
|
|
futureComponentData = _this.$$resource.fetch(null, type + 'list',
|
|
angular.extend(_this[queryKey], _this.$query));
|
|
|
|
// Invalidate cached results of other type if $query has changed
|
|
otherType = (type == 'tasks')? '$events' : '$tasks';
|
|
if (dirty) {
|
|
delete Component[otherType];
|
|
Component.$log.debug('force reload of ' + otherType);
|
|
}
|
|
|
|
return _this.$unwrapCollection(type, futureComponentData);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $find
|
|
* @desc Fetch a component from a specific calendar.
|
|
* @param {string} calendarId - the calendar ID
|
|
* @param {string} componentId - the component ID
|
|
* @param {string} [occurrenceId] - the component ID
|
|
* @see {@link Calendar.$getComponent}
|
|
*/
|
|
Component.$find = function(calendarId, componentId, occurrenceId) {
|
|
var futureComponentData, path = [calendarId, componentId];
|
|
|
|
if (occurrenceId)
|
|
path.push(occurrenceId);
|
|
|
|
futureComponentData = this.$$resource.fetch(path.join('/'), 'view');
|
|
|
|
return new Component(futureComponentData);
|
|
};
|
|
|
|
/**
|
|
* @function filterCategories
|
|
* @desc Search for categories matching some criterias
|
|
* @param {string} search - the search string to match
|
|
* @returns a collection of strings
|
|
*/
|
|
Component.filterCategories = function(query) {
|
|
var re = new RegExp(query, 'i');
|
|
return _.filter(_.keys(Component.$categories), function(category) {
|
|
return category.search(re) != -1;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function saveSelectedList
|
|
* @desc Save to the user's settings the currently selected list.
|
|
* @param {string} componentType - either "events" or "tasks"
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Component.saveSelectedList = function(componentType) {
|
|
return this.$$resource.post(null, 'saveSelectedList', { list: componentType + 'ListView' });
|
|
};
|
|
|
|
/**
|
|
* @function $eventsBlocksForView
|
|
* @desc Events blocks for a specific week
|
|
* @param {string} view - Either 'day' or 'week'
|
|
* @param {Date} type - Date of any day of the desired period
|
|
* @returns a promise of a collection of objects describing the events blocks
|
|
*/
|
|
Component.$eventsBlocksForView = function(view, date) {
|
|
var viewAction, startDate, endDate, params;
|
|
|
|
if (view == 'day') {
|
|
viewAction = 'dayView';
|
|
startDate = endDate = date;
|
|
}
|
|
else if (view == 'multicolumnday') {
|
|
viewAction = 'multicolumndayView';
|
|
startDate = endDate = date;
|
|
}
|
|
else if (view == 'week') {
|
|
viewAction = 'weekView';
|
|
startDate = date.beginOfWeek();
|
|
endDate = new Date();
|
|
endDate.setTime(startDate.getTime());
|
|
endDate.addDays(6);
|
|
}
|
|
else if (view == 'month') {
|
|
viewAction = 'monthView';
|
|
startDate = date;
|
|
startDate.setDate(1);
|
|
startDate = startDate.beginOfWeek();
|
|
endDate = new Date();
|
|
endDate.setTime(startDate.getTime());
|
|
endDate.setMonth(endDate.getMonth() + 1);
|
|
endDate.addDays(-1);
|
|
endDate = endDate.endOfWeek();
|
|
}
|
|
return this.$eventsBlocks(viewAction, startDate, endDate);
|
|
};
|
|
|
|
/**
|
|
* @function $eventsBlocks
|
|
* @desc Events blocks for a specific view and period
|
|
* @param {string} view - Either 'day', 'multicolumnday', 'week' or 'month'
|
|
* @param {Date} startDate - period's start date
|
|
* @param {Date} endDate - period's end date
|
|
* @returns a promise of a collection of objects describing the events blocks
|
|
*/
|
|
Component.$eventsBlocks = function(view, startDate, endDate) {
|
|
var params, futureComponentData, i, j, dates = [],
|
|
deferred = Component.$q.defer();
|
|
|
|
params = { view: view.toLowerCase(), sd: startDate.getDayString(), ed: endDate.getDayString() };
|
|
Component.$log.debug('eventsblocks ' + JSON.stringify(params, undefined, 2));
|
|
futureComponentData = this.$$resource.fetch(null, 'eventsblocks', params);
|
|
futureComponentData.then(function(views) {
|
|
var reduceComponent, associateComponent;
|
|
|
|
reduceComponent = function(objects, eventData, i) {
|
|
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) {
|
|
this[block.nbr].blocks.push(block); // Associate block to component
|
|
block.component = this[block.nbr]; // Associate component to block
|
|
};
|
|
|
|
Component.$views = [];
|
|
Component.$timeout(function() {
|
|
_.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);
|
|
|
|
// Associate Component objects to blocks positions
|
|
_.forEach(_.flatten(data.blocks), associateComponent, components);
|
|
|
|
// Associate Component objects to all-day blocks positions
|
|
_.each(_.flatten(data.allDayBlocks), associateComponent, components);
|
|
|
|
// Build array of dates
|
|
if (dates.length === 0)
|
|
for (i = 0; i < data.blocks.length; i++) {
|
|
dates.push(startDate.getDayString());
|
|
startDate.addDays(1);
|
|
}
|
|
|
|
// 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 + ')');
|
|
|
|
// Save the blocks to the object model
|
|
viewData = { blocks: blocks, allDayBlocks: allDayBlocks };
|
|
if (data.id && data.calendarName) {
|
|
// The multicolumnday view also includes calendar information
|
|
viewData.id = data.id;
|
|
viewData.calendarName = data.calendarName;
|
|
}
|
|
Component.$views.push(viewData);
|
|
});
|
|
|
|
deferred.resolve(Component.$views);
|
|
});
|
|
}, deferred.reject);
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
/**
|
|
* @function $unwrapCollection
|
|
* @desc Unwrap a promise and instanciate new Component objects using received data.
|
|
* @param {string} type - either 'events' or 'tasks'
|
|
* @param {promise} futureComponentData - a promise of the components' metadata
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Component.$unwrapCollection = function(type, futureComponentData) {
|
|
var _this = this,
|
|
components = [];
|
|
|
|
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) {
|
|
var data = _.object(fields, componentData);
|
|
components.push(new Component(data));
|
|
return components;
|
|
}, components);
|
|
|
|
Component.$log.debug('list of ' + type + ' ready (' + components.length + ')');
|
|
|
|
// Save the list of components to the object model
|
|
Component['$' + type] = components;
|
|
|
|
return components;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function init
|
|
* @memberof Component.prototype
|
|
* @desc Extend instance with required attributes and new data.
|
|
* @param {object} data - attributes of component
|
|
*/
|
|
Component.prototype.init = function(data) {
|
|
var _this = this;
|
|
|
|
this.categories = [];
|
|
this.repeat = {};
|
|
this.alarm = { action: 'display', quantity: 5, unit: 'MINUTES', reference: 'BEFORE', relation: 'START' };
|
|
this.status = 'not-specified';
|
|
this.delta = 60;
|
|
angular.extend(this, data);
|
|
|
|
Component.$Preferences.ready().then(function() {
|
|
var type = (_this.type == 'appointment')? 'Events' : 'Tasks';
|
|
|
|
// Set default values from user's defaults
|
|
_this.classification = _this.classification ||
|
|
Component.$Preferences.defaults['SOGoCalendar' + type + 'DefaultClassification'].toLowerCase();
|
|
});
|
|
|
|
if (this.component == 'vevent')
|
|
this.type = 'appointment';
|
|
else if (this.component == 'vtoto')
|
|
this.type = 'task';
|
|
|
|
if (this.startDate) {
|
|
if (angular.isString(this.startDate))
|
|
// Ex: 2015-10-25T22:34:51+00:00
|
|
this.start = new Date(this.startDate.substring(0,10) + ' ' + this.startDate.substring(11,16));
|
|
else
|
|
// Date object
|
|
this.start = this.startDate;
|
|
}
|
|
else if (this.type == 'appointment') {
|
|
this.start = new Date();
|
|
this.start.setMinutes(Math.round(this.start.getMinutes()/15)*15);
|
|
}
|
|
|
|
if (this.endDate) {
|
|
this.end = new Date(this.endDate.substring(0,10) + ' ' + this.endDate.substring(11,16));
|
|
this.delta = this.start.minutesTo(this.end);
|
|
}
|
|
else if (this.type == 'appointment') {
|
|
this.setDelta(this.delta);
|
|
}
|
|
|
|
if (this.dueDate)
|
|
this.due = new Date(this.dueDate.substring(0,10) + ' ' + this.dueDate.substring(11,16));
|
|
|
|
// Parse recurrence rule definition and initialize default values
|
|
this.$isRecurrent = angular.isDefined(data.repeat);
|
|
if (this.repeat.days) {
|
|
var byDayMask = _.find(this.repeat.days, function(o) {
|
|
return angular.isDefined(o.occurrence);
|
|
});
|
|
if (byDayMask)
|
|
if (this.repeat.frequency == 'yearly')
|
|
this.repeat.year = { byday: true };
|
|
this.repeat.month = {
|
|
type: 'byday',
|
|
occurrence: byDayMask.occurrence.toString(),
|
|
day: byDayMask.day
|
|
};
|
|
}
|
|
else {
|
|
this.repeat.days = [];
|
|
}
|
|
if (angular.isUndefined(this.repeat.frequency))
|
|
this.repeat.frequency = 'never';
|
|
if (angular.isUndefined(this.repeat.interval))
|
|
this.repeat.interval = 1;
|
|
if (angular.isUndefined(this.repeat.month))
|
|
this.repeat.month = { occurrence: '1', day: 'SU', type: 'bymonthday' };
|
|
if (angular.isUndefined(this.repeat.monthdays))
|
|
// TODO: initialize this.repeat.monthdays with month day of start date
|
|
this.repeat.monthdays = [];
|
|
if (angular.isUndefined(this.repeat.months))
|
|
// TODO: initialize this.repeat.months with month of start date
|
|
this.repeat.months = [];
|
|
if (angular.isUndefined(this.repeat.year))
|
|
this.repeat.year = {};
|
|
if (this.repeat.count)
|
|
this.repeat.end = 'count';
|
|
else if (this.repeat.until) {
|
|
this.repeat.end = 'until';
|
|
this.repeat.until = this.repeat.until.substring(0,10).asDate();
|
|
}
|
|
else
|
|
this.repeat.end = 'never';
|
|
this.$hasCustomRepeat = this.hasCustomRepeat();
|
|
|
|
if (this.isNew) {
|
|
// Set default alarm
|
|
Component.$Preferences.ready().then(function() {
|
|
var units = { M: 'MINUTES', H: 'HOURS', D: 'DAYS', W: 'WEEKS' };
|
|
var match = /-PT?([0-9]+)([MHDW])/.exec(Component.$Preferences.defaults.SOGoCalendarDefaultReminder);
|
|
if (match) {
|
|
_this.$hasAlarm = true;
|
|
_this.alarm.quantity = parseInt(match[1]);
|
|
_this.alarm.unit = units[match[2]];
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
this.$hasAlarm = angular.isDefined(data.alarm);
|
|
}
|
|
|
|
// Allow the component to be moved to a different calendar
|
|
this.destinationCalendar = this.pid;
|
|
|
|
if (this.organizer && this.organizer.email) {
|
|
this.organizer.$image = Component.$gravatar(this.organizer.email, 32);
|
|
}
|
|
|
|
// Load freebusy of attendees
|
|
this.freebusy = this.updateFreeBusyCoverage();
|
|
|
|
if (this.attendees) {
|
|
_.each(this.attendees, function(attendee) {
|
|
attendee.image = Component.$gravatar(attendee.email, 32);
|
|
_this.updateFreeBusy(attendee);
|
|
});
|
|
}
|
|
|
|
this.selected = false;
|
|
};
|
|
|
|
/**
|
|
* @function hasCustomRepeat
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component has a custom recurrence rule.
|
|
* @returns true if the recurrence rule requires the full recurrence editor
|
|
*/
|
|
Component.prototype.hasCustomRepeat = function() {
|
|
var b = angular.isDefined(this.repeat) &&
|
|
(this.repeat.interval > 1 ||
|
|
this.repeat.days && this.repeat.days.length > 0 ||
|
|
this.repeat.monthdays && this.repeat.monthdays.length > 0 ||
|
|
this.repeat.months && this.repeat.months.length > 0);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* @function isEditable
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component is editable and not an occurrence of a recurrent component
|
|
* @returns true or false
|
|
*/
|
|
Component.prototype.isEditable = function() {
|
|
return (!this.occurrenceId && !this.isReadOnly);
|
|
};
|
|
|
|
/**
|
|
* @function isEditableOccurrence
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component is editable and an occurrence of a recurrent component
|
|
* @returns true or false
|
|
*/
|
|
Component.prototype.isEditableOccurrence = function() {
|
|
return (this.occurrenceId && !this.isReadOnly);
|
|
};
|
|
|
|
/**
|
|
* @function isInvitation
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component an invitation and not an occurrence of a recurrent component
|
|
* @returns true or false
|
|
*/
|
|
Component.prototype.isInvitation = function() {
|
|
return (!this.occurrenceId && this.userHasRSVP);
|
|
};
|
|
|
|
/**
|
|
* @function isInvitationOccurrence
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component an invitation and an occurrence of a recurrent component
|
|
* @returns true or false
|
|
*/
|
|
Component.prototype.isInvitationOccurrence = function() {
|
|
return (this.occurrenceId && this.userHasRSVP);
|
|
};
|
|
|
|
/**
|
|
* @function isReadOnly
|
|
* @memberof Component.prototype
|
|
* @desc Check if the component is not editable and not an invitation
|
|
* @returns true or false
|
|
*/
|
|
Component.prototype.isReadOnly = function() {
|
|
return (this.isReadOnly && !this.userHasRSVP);
|
|
};
|
|
|
|
/**
|
|
* @function enablePercentComplete
|
|
* @memberof Component.prototype
|
|
* @desc Check if the percent completion should be enabled with respect to the
|
|
* component's type and status.
|
|
* @returns true if the percent completion should be displayed
|
|
*/
|
|
Component.prototype.enablePercentComplete = function() {
|
|
return (this.type == 'task' &&
|
|
this.status != 'not-specified' &&
|
|
this.status != 'cancelled');
|
|
};
|
|
|
|
/**
|
|
* @function coversFreeBusy
|
|
* @memberof Component.prototype
|
|
* @desc Check if a specific quarter matches the component's period
|
|
* @returns true if the quarter covers the component's period
|
|
*/
|
|
Component.prototype.coversFreeBusy = function(day, hour, quarter) {
|
|
var b = (angular.isDefined(this.freebusy[day]) &&
|
|
angular.isDefined(this.freebusy[day][hour]) &&
|
|
this.freebusy[day][hour][quarter] == 1);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* @function updateFreeBusyCoverage
|
|
* @memberof Component.prototype
|
|
* @desc Build a 15-minute-based representation of the component's period.
|
|
* @returns an object literal hashed by days and hours and arrays of four 1's and 0's
|
|
*/
|
|
Component.prototype.updateFreeBusyCoverage = function() {
|
|
var _this = this, freebusy = {};
|
|
|
|
if (this.start && this.end) {
|
|
var roundedStart = new Date(this.start.getTime()),
|
|
roundedEnd = new Date(this.end.getTime()),
|
|
startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5),
|
|
endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5);
|
|
roundedStart.setMinutes(15*startQuarter);
|
|
roundedEnd.setMinutes(15*endQuarter);
|
|
|
|
_.each(roundedStart.daysUpTo(roundedEnd), function(date, index) {
|
|
var currentDay = date.getDate(),
|
|
dayKey = date.getDayString(),
|
|
hourKey;
|
|
if (dayKey == _this.start.getDayString()) {
|
|
hourKey = date.getHours().toString();
|
|
freebusy[dayKey] = {};
|
|
freebusy[dayKey][hourKey] = [];
|
|
while (startQuarter > 0) {
|
|
freebusy[dayKey][hourKey].push(0);
|
|
startQuarter--;
|
|
}
|
|
}
|
|
else {
|
|
date = date.beginOfDay();
|
|
freebusy[dayKey] = {};
|
|
}
|
|
while (date.getTime() < _this.end.getTime() &&
|
|
date.getDate() == currentDay) {
|
|
hourKey = date.getHours().toString();
|
|
if (angular.isUndefined(freebusy[dayKey][hourKey]))
|
|
freebusy[dayKey][hourKey] = [];
|
|
freebusy[dayKey][hourKey].push(1);
|
|
date.addMinutes(15);
|
|
}
|
|
});
|
|
return freebusy;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function setDelta
|
|
* @memberof Component.prototype
|
|
* @desc Set the end time to the specified number of minutes after the start time.
|
|
* @param {number} delta - the number of minutes
|
|
*/
|
|
Component.prototype.setDelta = function(delta) {
|
|
this.delta = delta;
|
|
this.end = new Date(this.start.getTime());
|
|
this.end.setMinutes(Math.round(this.end.getMinutes()/15)*15);
|
|
this.end.addMinutes(this.delta);
|
|
};
|
|
|
|
/**
|
|
* @function updateFreeBusy
|
|
* @memberof Component.prototype
|
|
* @desc Update the freebusy information for the component's period for a specific attendee.
|
|
* @param {Object} card - an Card object instance of the attendee
|
|
*/
|
|
Component.prototype.updateFreeBusy = function(attendee) {
|
|
var params, url, days;
|
|
if (attendee.uid) {
|
|
params =
|
|
{
|
|
sday: this.start.getDayString(),
|
|
eday: this.end.getDayString()
|
|
};
|
|
url = ['..', '..', attendee.uid, 'freebusy.ifb'];
|
|
days = _.map(this.start.daysUpTo(this.end), function(day) { return day.getDayString(); });
|
|
|
|
if (angular.isUndefined(attendee.freebusy))
|
|
attendee.freebusy = {};
|
|
|
|
// Fetch FreeBusy information
|
|
Component.$$resource.fetch(url.join('/'), 'ajaxRead', params).then(function(data) {
|
|
_.each(days, function(day) {
|
|
var hour;
|
|
|
|
if (angular.isUndefined(attendee.freebusy[day]))
|
|
attendee.freebusy[day] = {};
|
|
|
|
if (angular.isUndefined(data[day]))
|
|
data[day] = {};
|
|
|
|
for (var i = 0; i <= 23; i++) {
|
|
hour = i.toString();
|
|
if (data[day][hour])
|
|
attendee.freebusy[day][hour] = [
|
|
data[day][hour]["0"],
|
|
data[day][hour]["15"],
|
|
data[day][hour]["30"],
|
|
data[day][hour]["45"]
|
|
];
|
|
else
|
|
attendee.freebusy[day][hour] = [0, 0, 0, 0];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function getClassName
|
|
* @memberof Component.prototype
|
|
* @desc Return the component CSS class name based on its container (calendar) ID.
|
|
* @param {string} [base] - the prefix to add to the class name (defaults to "fg")
|
|
* @returns a string representing the foreground CSS class name
|
|
*/
|
|
Component.prototype.getClassName = function(base) {
|
|
if (angular.isUndefined(base))
|
|
base = 'fg';
|
|
return base + '-folder' + (this.destinationCalendar || this.c_folder || this.pid);
|
|
};
|
|
|
|
/**
|
|
* @function addAttendee
|
|
* @memberof Component.prototype
|
|
* @desc Add an attendee and fetch his freebusy info.
|
|
* @param {Object} card - an Card object instance to be added to the attendees list
|
|
*/
|
|
Component.prototype.addAttendee = function(card) {
|
|
var attendee, url, params;
|
|
if (card) {
|
|
attendee = {
|
|
name: card.c_cn,
|
|
email: card.$preferredEmail(),
|
|
role: 'req-participant',
|
|
status: 'needs-action',
|
|
uid: card.c_uid
|
|
};
|
|
if (!_.find(this.attendees, function(o) {
|
|
return o.email == attendee.email;
|
|
})) {
|
|
attendee.image = Component.$gravatar(attendee.email, 32);
|
|
if (this.attendees)
|
|
this.attendees.push(attendee);
|
|
else
|
|
this.attendees = [attendee];
|
|
this.updateFreeBusy(attendee);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function hasAttendee
|
|
* @memberof Component.prototype
|
|
* @desc Verify if one of the email addresses of a Card instance matches an attendee.
|
|
* @param {Object} card - an Card object instance
|
|
* @returns true if the Card matches an attendee
|
|
*/
|
|
Component.prototype.hasAttendee = function(card) {
|
|
var attendee = _.find(this.attendees, function(attendee) {
|
|
return _.find(card.emails, function(email) {
|
|
return email.value == attendee.email;
|
|
});
|
|
});
|
|
return angular.isDefined(attendee);
|
|
};
|
|
|
|
/**
|
|
* @function deleteAttendee
|
|
* @memberof Component.prototype
|
|
* @desc Remove an attendee from the component
|
|
* @param {Object} attendee - an object literal defining an attendee
|
|
*/
|
|
Component.prototype.deleteAttendee = function(attendee) {
|
|
var index = _.findIndex(this.attendees, function(currentAttendee) {
|
|
return currentAttendee.email == attendee.email;
|
|
});
|
|
this.attendees.splice(index, 1);
|
|
};
|
|
|
|
/**
|
|
* @function canRemindAttendeesByEmail
|
|
* @memberof Component.prototype
|
|
* @desc Verify if the component's reminder must be send by email and if it has at least one attendee.
|
|
* @returns true if attendees can receive a reminder by email
|
|
*/
|
|
Component.prototype.canRemindAttendeesByEmail = function() {
|
|
return this.alarm.action == 'email' &&
|
|
!this.isReadOnly &&
|
|
this.attendees && this.attendees.length > 0;
|
|
};
|
|
|
|
/**
|
|
* @function addAttachUrl
|
|
* @memberof Component.prototype
|
|
* @desc Add a new attach URL if not already defined
|
|
* @param {string} attachUrl - the URL
|
|
* @returns the number of values in the list of attach URLs
|
|
*/
|
|
Component.prototype.addAttachUrl = function(attachUrl) {
|
|
if (angular.isUndefined(this.attachUrls)) {
|
|
this.attachUrls = [{value: attachUrl}];
|
|
}
|
|
else {
|
|
for (var i = 0; i < this.attachUrls.length; i++) {
|
|
if (this.attachUrls[i].value == attachUrl) {
|
|
break;
|
|
}
|
|
}
|
|
if (i == this.attachUrls.length)
|
|
this.attachUrls.push({value: attachUrl});
|
|
}
|
|
return this.attachUrls.length - 1;
|
|
};
|
|
|
|
/**
|
|
* @function deleteAttachUrl
|
|
* @memberof Component.prototype
|
|
* @desc Remove an attach URL
|
|
* @param {number} index - the URL index in the list of attach URLs
|
|
*/
|
|
Component.prototype.deleteAttachUrl = function(index) {
|
|
if (index > -1 && this.attachUrls.length > index) {
|
|
this.attachUrls.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function $addDueDate
|
|
* @memberof Component.prototype
|
|
* @desc Add a due date
|
|
*/
|
|
Component.prototype.$addDueDate = function() {
|
|
this.due = new Date();
|
|
this.due.setMinutes(Math.round(this.due.getMinutes()/15)*15);
|
|
this.dueDate = this.due.toISOString();
|
|
};
|
|
|
|
/**
|
|
* @function $deleteDueDate
|
|
* @memberof Component.prototype
|
|
* @desc Delete a due date
|
|
*/
|
|
Component.prototype.$deleteDueDate = function() {
|
|
delete this.due;
|
|
delete this.dueDate;
|
|
};
|
|
|
|
/**
|
|
* @function $addStartDate
|
|
* @memberof Component.prototype
|
|
* @desc Add a start date
|
|
*/
|
|
Component.prototype.$addStartDate = function() {
|
|
this.start = new Date();
|
|
this.start.setMinutes(Math.round(this.start.getMinutes()/15)*15);
|
|
};
|
|
|
|
/**
|
|
* @function $deleteStartDate
|
|
* @memberof Component.prototype
|
|
* @desc Delete a start date
|
|
*/
|
|
Component.prototype.$deleteStartDate = function() {
|
|
delete this.start;
|
|
delete this.startDate;
|
|
};
|
|
|
|
/**
|
|
* @function $reset
|
|
* @memberof Component.prototype
|
|
* @desc Reset the original state the component's data.
|
|
*/
|
|
Component.prototype.$reset = function() {
|
|
var _this = this;
|
|
angular.forEach(this, function(value, key) {
|
|
if (key != 'constructor' && key[0] != '$') {
|
|
delete _this[key];
|
|
}
|
|
});
|
|
this.init(this.$shadowData);
|
|
this.$shadowData = this.$omit(true);
|
|
};
|
|
|
|
/**
|
|
* @function $reply
|
|
* @memberof Component.prototype
|
|
* @desc Reply to an invitation.
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Component.prototype.$reply = function() {
|
|
var _this = this, data, path = [this.pid, this.id];
|
|
|
|
if (this.occurrenceId)
|
|
path.push(this.occurrenceId);
|
|
|
|
data = {
|
|
reply: this.reply,
|
|
delegatedTo: this.delegatedTo,
|
|
alarm: this.$hasAlarm? this.alarm : {}
|
|
};
|
|
|
|
return Component.$$resource.save(path.join('/'), data, { action: 'rsvpAppointment' })
|
|
.then(function(data) {
|
|
// Make a copy of the data for an eventual reset
|
|
_this.$shadowData = _this.$omit(true);
|
|
return data;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @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
|
|
* @desc Save the component to the server.
|
|
*/
|
|
Component.prototype.$save = function() {
|
|
var _this = this, options, path = [this.pid, this.id];
|
|
|
|
if (this.isNew)
|
|
options = { action: 'saveAs' + this.type.capitalize() };
|
|
|
|
if (this.occurrenceId)
|
|
path.push(this.occurrenceId);
|
|
|
|
return Component.$$resource.save(path.join('/'), this.$omit(), options)
|
|
.then(function(data) {
|
|
// Make a copy of the data for an eventual reset
|
|
_this.$shadowData = _this.$omit(true);
|
|
return data;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $delete
|
|
* @memberof Component.prototype
|
|
* @desc Delete the component from the server.
|
|
* @param {boolean} occurrenceOnly - delete this occurrence only
|
|
*/
|
|
Component.prototype.remove = function(occurrenceOnly) {
|
|
var _this = this, path = [this.pid, this.id];
|
|
|
|
if (occurrenceOnly && this.occurrenceId)
|
|
path.push(this.occurrenceId);
|
|
|
|
return Component.$$resource.remove(path.join('/'));
|
|
};
|
|
|
|
/**
|
|
* @function $unwrap
|
|
* @memberof Component.prototype
|
|
* @desc Unwrap a promise.
|
|
* @param {promise} futureComponentData - a promise of some of the Component's data
|
|
*/
|
|
Component.prototype.$unwrap = function(futureComponentData) {
|
|
var _this = this;
|
|
|
|
// Expose the promise
|
|
this.$futureComponentData = futureComponentData;
|
|
|
|
// Resolve the promise
|
|
this.$futureComponentData.then(function(data) {
|
|
_this.init(data);
|
|
// Make a copy of the data for an eventual reset
|
|
_this.$shadowData = _this.$omit();
|
|
}, function(data) {
|
|
angular.extend(_this, data);
|
|
_this.isError = true;
|
|
Component.$log.error(_this.error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $omit
|
|
* @memberof Component.prototype
|
|
* @desc Return a sanitized object used to send to the server.
|
|
* @return an object literal copy of the Component instance
|
|
*/
|
|
Component.prototype.$omit = function() {
|
|
var component = {}, date;
|
|
angular.forEach(this, function(value, key) {
|
|
if (key != 'constructor' &&
|
|
key[0] != '$' &&
|
|
key != 'blocks') {
|
|
component[key] = angular.copy(value);
|
|
}
|
|
});
|
|
|
|
// Format dates and times
|
|
component.startDate = component.start ? formatDate(component.start) : '';
|
|
component.startTime = component.start ? formatTime(component.start) : '';
|
|
component.endDate = component.end ? formatDate(component.end) : '';
|
|
component.endTime = component.end ? formatTime(component.end) : '';
|
|
component.dueDate = component.due ? formatDate(component.due) : '';
|
|
component.dueTime = component.due ? formatTime(component.due) : '';
|
|
|
|
// Update recurrence definition depending on selections
|
|
if (this.$hasCustomRepeat) {
|
|
if (this.repeat.frequency == 'monthly' && this.repeat.month.type && this.repeat.month.type == 'byday' ||
|
|
this.repeat.frequency == 'yearly' && this.repeat.year.byday) {
|
|
// BYDAY mask for a monthly or yearly recurrence
|
|
delete component.repeat.monthdays;
|
|
component.repeat.days = [{ day: this.repeat.month.day, occurrence: this.repeat.month.occurrence.toString() }];
|
|
}
|
|
else if (this.repeat.month.type) {
|
|
// montly recurrence by month days or yearly by month
|
|
delete component.repeat.days;
|
|
}
|
|
}
|
|
else if (this.repeat.frequency) {
|
|
component.repeat = { frequency: this.repeat.frequency };
|
|
}
|
|
if (this.repeat.frequency) {
|
|
if (this.repeat.end == 'until' && this.repeat.until)
|
|
component.repeat.until = this.repeat.until.stringWithSeparator('-');
|
|
else if (this.repeat.end == 'count' && this.repeat.count)
|
|
component.repeat.count = this.repeat.count;
|
|
else {
|
|
delete component.repeat.until;
|
|
delete component.repeat.count;
|
|
}
|
|
}
|
|
else {
|
|
delete component.repeat;
|
|
}
|
|
|
|
if (this.$hasAlarm) {
|
|
if (this.alarm.action && this.alarm.action == 'email' &&
|
|
!(this.attendees && this.attendees.length > 0)) {
|
|
// No attendees; email reminder must be sent to organizer only
|
|
this.alarm.attendees = 0;
|
|
this.alarm.organizer = 1;
|
|
}
|
|
}
|
|
else {
|
|
component.alarm = {};
|
|
}
|
|
|
|
function formatTime(date) {
|
|
var hours = date.getHours();
|
|
if (hours < 10) hours = '0' + hours;
|
|
|
|
var minutes = date.getMinutes();
|
|
if (minutes < 10) minutes = '0' + minutes;
|
|
return hours + ':' + minutes;
|
|
}
|
|
|
|
function formatDate(date) {
|
|
var year = date.getYear();
|
|
if (year < 1000) year += 1900;
|
|
|
|
var month = '' + (date.getMonth() + 1);
|
|
if (month.length == 1)
|
|
month = '0' + month;
|
|
|
|
var day = '' + date.getDate();
|
|
if (day.length == 1)
|
|
day = '0' + day;
|
|
|
|
return year + '-' + month + '-' + day;
|
|
}
|
|
|
|
return component;
|
|
};
|
|
|
|
})();
|