From e50f7c756f7683edc164389d04b9cdd8743be012 Mon Sep 17 00:00:00 2001 From: Mathieu Schopfer Date: Thu, 9 Jun 2016 20:23:27 +0200 Subject: [PATCH] Implementing a time parser for the event summary (feature request #485) --- js/app/controllers/editorcontroller.js | 38 + js/public/app.js | 5085 ++++++++++++++++++++++++ templates/editor.popover.php | 1 + templates/editor.sidebar.php | 1 + 4 files changed, 5125 insertions(+) create mode 100644 js/public/app.js diff --git a/js/app/controllers/editorcontroller.js b/js/app/controllers/editorcontroller.js index 90dcb020ed..afbb4ad69a 100644 --- a/js/app/controllers/editorcontroller.js +++ b/js/app/controllers/editorcontroller.js @@ -85,6 +85,44 @@ app.controller('EditorController', ['$scope', 'TimezoneService', 'AutoCompletion $scope.registerPostHook = function(callback) { $scope.postEditingHooks.push(callback); }; + + $scope.parse_summary = function() { + var summary = $scope.properties.summary.value; + + // Parse text to extract schedule from string beginning + var timePattern = "(\\d{1,2}:\\d{2}(?:\\s?[ap]\\.?m\\.?)?)"; + var linkPattern = "\\s?[-/]\\s?"; + var scheduleExp = new RegExp("^"+timePattern+"(?:"+linkPattern+timePattern+")?\\s", 'i'); + var matches = scheduleExp.exec(summary); + + if(matches !== null) { + + // Strip schedule string from summary + summary = summary.substring(matches.shift().length); + + var start = matches[0]; + var end = matches[1]; + + // Convert string to time + var schemes = ["HH:mm", "hh:mm a"]; + start = moment(start, schemes); + if(typeof end !== 'undefined') { + end = moment(end, schemes); + } + else { + end = start.clone(); + } + + // Update event properties + $scope.properties.dtstart.value = moment($scope.properties.dtstart.value.set( + {'hour': start.get('hour'), 'minute': start.get('minute')})); + $scope.properties.dtend.value = moment($scope.properties.dtend.value.set( + {'hour': end.get('hour'), 'minute': end.get('minute')})); + $scope.properties.allDay = false; + $scope.toggledAllDay(); + $scope.properties.summary.value = summary; + } + }; $scope.proceed = function() { $scope.prepareClose(); diff --git a/js/public/app.js b/js/public/app.js new file mode 100644 index 0000000000..00798cb4a2 --- /dev/null +++ b/js/public/app.js @@ -0,0 +1,5085 @@ + +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; + +/** +* Configuration / App Initialization File +*/ + +var app = angular.module('Calendar', ['ui.bootstrap']); + +app.config(['$provide', '$httpProvider', function ($provide, $httpProvider) { + 'use strict'; + + $httpProvider.defaults.headers.common.requesttoken = oc_requesttoken; + + ICAL.design.defaultSet.param['x-oc-group-id'] = { + allowXName: true + }; + + angular.forEach($.fullCalendar.langs, function (obj, lang) { + $.fullCalendar.lang(lang, { + timeFormat: obj.mediumTimeFormat + }); + + var propsToCheck = ['extraSmallTimeFormat', 'hourFormat', 'mediumTimeFormat', 'noMeridiemTimeFormat', 'smallTimeFormat']; + + angular.forEach(propsToCheck, function (propToCheck) { + if (obj[propToCheck]) { + var overwrite = {}; + overwrite[propToCheck] = obj[propToCheck].replace('HH', 'H'); + + $.fullCalendar.lang(lang, overwrite); + } + }); + }); +}]); + +app.run(['$document', '$rootScope', '$window', function ($document, $rootScope, $window) { + 'use strict'; + + $rootScope.baseUrl = $window.location.origin + $window.location.pathname + ($window.location.pathname.substr(-1) === '/' ? '' : '/') + 'v1/'; + + $document.click(function (event) { + $rootScope.$broadcast('documentClicked', event); + }); +}]); + +app.controller('AttendeeController', ["$scope", "AutoCompletionService", function ($scope, AutoCompletionService) { + 'use strict'; + + $scope.newAttendeeGroup = -1; + + $scope.cutstats = [{ displayname: t('calendar', 'Individual'), val: 'INDIVIDUAL' }, { displayname: t('calendar', 'Group'), val: 'GROUP' }, { displayname: t('calendar', 'Resource'), val: 'RESOURCE' }, { displayname: t('calendar', 'Room'), val: 'ROOM' }, { displayname: t('calendar', 'Unknown'), val: 'UNKNOWN' }]; + + $scope.partstats = [{ displayname: t('calendar', 'Required'), val: 'REQ-PARTICIPANT' }, { displayname: t('calendar', 'Optional'), val: 'OPT-PARTICIPANT' }, { displayname: t('calendar', 'Does not attend'), val: 'NON-PARTICIPANT' }]; + + $scope.$parent.registerPostHook(function () { + $scope.properties.attendee = $scope.properties.attendee || []; + if ($scope.properties.attendee.length > 0 && $scope.properties.organizer === null) { + $scope.properties.organizer = { + value: 'MAILTO:' + $scope.$parent.emailAddress, + parameters: { + cn: OC.getCurrentUser().displayName + } + }; + } + }); + + $scope.add = function (email) { + if (email !== '') { + $scope.properties.attendee = $scope.properties.attendee || []; + $scope.properties.attendee.push({ + value: 'MAILTO:' + email, + group: $scope.newAttendeeGroup--, + parameters: { + 'role': 'REQ-PARTICIPANT', + 'rsvp': true, + 'partstat': 'NEEDS-ACTION', + 'cutype': 'INDIVIDUAL' + } + }); + } + $scope.attendeeoptions = false; + $scope.nameofattendee = ''; + }; + + $scope.remove = function (attendee) { + $scope.properties.attendee = $scope.properties.attendee.filter(function (elem) { + return elem.group !== attendee.group; + }); + }; + + $scope.search = function (value) { + return AutoCompletionService.searchAttendee(value); + }; + + $scope.selectFromTypeahead = function (item) { + $scope.properties.attendee = $scope.properties.attendee || []; + $scope.properties.attendee.push({ + value: 'MAILTO:' + item.email, + parameters: { + cn: item.name, + role: 'REQ-PARTICIPANT', + rsvp: true, + partstat: 'NEEDS-ACTION', + cutype: 'INDIVIDUAL' + } + }); + $scope.nameofattendee = ''; + }; +}]); + +/** +* Controller: CalController +* Description: The fullcalendar controller. +*/ + +app.controller('CalController', ['$scope', 'Calendar', 'CalendarService', 'VEventService', 'SettingsService', 'TimezoneService', 'VEvent', 'is', 'fc', 'EventsEditorDialogService', 'PopoverPositioningUtility', function ($scope, Calendar, CalendarService, VEventService, SettingsService, TimezoneService, VEvent, is, fc, EventsEditorDialogService, PopoverPositioningUtility) { + 'use strict'; + + is.loading = true; + + $scope.calendars = []; + $scope.eventSource = {}; + $scope.defaulttimezone = TimezoneService.current(); + $scope.eventModal = null; + var switcher = []; + + function showCalendar(url) { + if (switcher.indexOf(url) === -1 && $scope.eventSource[url].isRendering === false) { + switcher.push(url); + fc.elm.fullCalendar('removeEventSource', $scope.eventSource[url]); + fc.elm.fullCalendar('addEventSource', $scope.eventSource[url]); + } + } + + function hideCalendar(url) { + fc.elm.fullCalendar('removeEventSource', $scope.eventSource[url]); + if (switcher.indexOf(url) !== -1) { + switcher.splice(switcher.indexOf(url), 1); + } + } + + function createAndRenderEvent(calendar, data, start, end, tz) { + VEventService.create(calendar, data).then(function (vevent) { + if (calendar.enabled) { + fc.elm.fullCalendar('refetchEventSources', calendar.fcEventSource); + } + }); + } + + function deleteAndRemoveEvent(vevent, fcEvent) { + VEventService.delete(vevent).then(function () { + fc.elm.fullCalendar('removeEvents', fcEvent.id); + }); + } + + $scope.$watchCollection('calendars', function (newCalendars, oldCalendars) { + newCalendars.filter(function (calendar) { + return oldCalendars.indexOf(calendar) === -1; + }).forEach(function (calendar) { + $scope.eventSource[calendar.url] = calendar.fcEventSource; + if (calendar.enabled) { + showCalendar(calendar.url); + } + + calendar.register(Calendar.hookEnabledChanged, function (enabled) { + if (enabled) { + showCalendar(calendar.url); + } else { + hideCalendar(calendar.url); + //calendar.list.loading = false; + } + }); + + calendar.register(Calendar.hookColorChanged, function () { + if (calendar.enabled) { + hideCalendar(calendar.url); + showCalendar(calendar.url); + } + }); + }); + + oldCalendars.filter(function (calendar) { + return newCalendars.indexOf(calendar) === -1; + }).forEach(function (calendar) { + var url = calendar.url; + hideCalendar(calendar.url); + + delete $scope.eventSource[url]; + }); + }); + + TimezoneService.get($scope.defaulttimezone).then(function (timezone) { + if (timezone) { + ICAL.TimezoneService.register($scope.defaulttimezone, timezone.jCal); + } + }).catch(function () { + OC.Notification.showTemporary(t('calendar', 'You are in an unknown timezone ({tz}), falling back to UTC', { + tz: $scope.defaulttimezone + })); + + $scope.defaulttimezone = 'UTC'; + $scope.uiConfig.calendar.timezone = 'UTC'; + }); + + CalendarService.getAll().then(function (calendars) { + $scope.calendars = calendars; + is.loading = false; + // TODO - scope.apply should not be necessary here + $scope.$apply(); + }); + + /** + * Calendar UI Configuration. + */ + $scope.fcConfig = { + timezone: $scope.defaulttimezone, + select: function select(start, end, jsEvent, view) { + var writableCalendars = $scope.calendars.filter(function (elem) { + return elem.isWritable(); + }); + + if (writableCalendars.length === 0) { + OC.Notification.showTemporary(t('calendar', 'Please create a calendar first.')); + return; + } + + start.add(start.toDate().getTimezoneOffset(), 'minutes'); + end.add(end.toDate().getTimezoneOffset(), 'minutes'); + + var vevent = VEvent.fromStartEnd(start, end, $scope.defaulttimezone); + vevent.calendar = writableCalendars[0]; + + var timestamp = Date.now(); + var fcEventClass = 'new-event-dummy-' + timestamp; + + var fcEvent = vevent.getFcEvent(view.start, view.end, $scope.defaulttimezone)[0]; + fcEvent.title = t('calendar', 'New event'); + fcEvent.className.push(fcEventClass); + fcEvent.writable = false; + fc.elm.fullCalendar('renderEvent', fcEvent); + + EventsEditorDialogService.open($scope, fcEvent, function () { + var elements = angular.element('.' + fcEventClass); + var isHidden = angular.element(elements[0]).parents('.fc-limited').length !== 0; + if (isHidden) { + return PopoverPositioningUtility.calculate(jsEvent.clientX, jsEvent.clientY, jsEvent.clientX, jsEvent.clientY, view); + } else { + return PopoverPositioningUtility.calculateByTarget(elements[0], view); + } + }, function () { + return null; + }, function () { + fc.elm.fullCalendar('removeEvents', function (fcEventToCheck) { + if (Array.isArray(fcEventToCheck.className)) { + return fcEventToCheck.className.indexOf(fcEventClass) !== -1; + } else { + return false; + } + }); + }).then(function (result) { + createAndRenderEvent(result.calendar, result.vevent.data, view.start, view.end, $scope.defaulttimezone); + }).catch(function () { + //fcEvent is removed by unlock callback + //no need to anything + return null; + }); + }, + eventClick: function eventClick(fcEvent, jsEvent, view) { + var vevent = fcEvent.vevent; + var oldCalendar = vevent.calendar; + var fcEvt = fcEvent; + + EventsEditorDialogService.open($scope, fcEvent, function () { + return PopoverPositioningUtility.calculateByTarget(jsEvent.currentTarget, view); + }, function () { + fcEvt.editable = false; + fc.elm.fullCalendar('updateEvent', fcEvt); + }, function () { + fcEvt.editable = fcEvent.calendar.writable; + fc.elm.fullCalendar('updateEvent', fcEvt); + }).then(function (result) { + // was the event moved to another calendar? + if (result.calendar === oldCalendar) { + VEventService.update(vevent).then(function () { + fc.elm.fullCalendar('removeEvents', fcEvent.id); + + if (result.calendar.enabled) { + fc.elm.fullCalendar('refetchEventSources', result.calendar.fcEventSource); + } + }); + } else { + deleteAndRemoveEvent(vevent, fcEvent); + createAndRenderEvent(result.calendar, result.vevent.data, view.start, view.end, $scope.defaulttimezone); + } + console.log(result); + }).catch(function (reason) { + if (reason === 'delete') { + deleteAndRemoveEvent(vevent, fcEvent); + } + }); + }, + eventResize: function eventResize(fcEvent, delta, revertFunc) { + fcEvent.resize(delta); + VEventService.update(fcEvent.vevent).catch(function () { + revertFunc(); + }); + }, + eventDrop: function eventDrop(fcEvent, delta, revertFunc) { + fcEvent.drop(delta); + VEventService.update(fcEvent.vevent).catch(function () { + revertFunc(); + }); + }, + viewRender: function viewRender(view, element) { + angular.element('#firstrow').find('.datepicker_current').html(view.title).text(); + angular.element('#datecontrol_date').datepicker('setDate', element.fullCalendar('getDate')); + var newView = view.name; + if (newView !== $scope.defaultView) { + SettingsService.setView(newView); + $scope.defaultView = newView; + } + if (newView === 'agendaDay') { + angular.element('td.fc-state-highlight').css('background-color', '#ffffff'); + } else { + angular.element('.fc-bg td.fc-state-highlight').css('background-color', '#ffc'); + } + if (newView === 'agendaWeek') { + element.fullCalendar('option', 'aspectRatio', 0.1); + } else { + element.fullCalendar('option', 'aspectRatio', 1.35); + } + }, + eventRender: function eventRender(event, element) { + var status = event.getSimpleEvent().status; + if (status !== null) { + if (status.value === 'TENTATIVE') { + element.css({ 'opacity': 0.5 }); + } else if (status.value === 'CANCELLED') { + element.css({ + 'text-decoration': 'line-through', + 'opacity': 0.5 + }); + } + } + } + }; +}]); + +/** +* Controller: CalendarListController +* Description: Takes care of CalendarList in App Navigation. +*/ + +app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'CalendarService', 'is', 'CalendarListItem', 'Calendar', function ($scope, $rootScope, $window, CalendarService, is, CalendarListItem, Calendar) { + 'use strict'; + + $scope.calendarListItems = []; + $scope.is = is; + $scope.newCalendarInputVal = ''; + $scope.newCalendarColorVal = ''; + + window.scope = $scope; + + $scope.$watchCollection('calendars', function (newCalendars, oldCalendars) { + newCalendars = newCalendars || []; + oldCalendars = oldCalendars || []; + + newCalendars.filter(function (calendar) { + return oldCalendars.indexOf(calendar) === -1; + }).forEach(function (calendar) { + var item = CalendarListItem(calendar); + if (item) { + $scope.calendarListItems.push(item); + calendar.register(Calendar.hookFinishedRendering, function () { + if (!$scope.$$phase) { + $scope.$apply(); + } + }); + } + }); + + oldCalendars.filter(function (calendar) { + return newCalendars.indexOf(calendar) === -1; + }).forEach(function (calendar) { + $scope.calendarListItems = $scope.calendarListItems.filter(function (itemToCheck) { + return itemToCheck.calendar !== calendar; + }); + }); + + if (!$scope.$$phase) { + $scope.$apply(); + } + }); + + $scope.create = function (name, color) { + CalendarService.create(name, color).then(function (calendar) { + $scope.calendars.push(calendar); + $rootScope.$broadcast('createdCalendar', calendar); + $rootScope.$broadcast('reloadCalendarList'); + }); + + $scope.newCalendarInputVal = ''; + $scope.newCalendarColorVal = ''; + angular.element('#new-calendar-button').click(); + }; + + $scope.download = function (item) { + var url = item.calendar.url; + // cut off last slash to have a fancy name for the ics + if (url.slice(url.length - 1) === '/') { + url = url.slice(0, url.length - 1); + } + url += '?export'; + + $window.open(url); + }; + + $scope.toggleSharesEditor = function (calendar) { + calendar.toggleSharesEditor(); + }; + + $scope.prepareUpdate = function (calendar) { + calendar.prepareUpdate(); + }; + + $scope.onSelectSharee = function (item, model, label, calendar) { + // Remove content from text box + calendar.selectedSharee = ''; + // Create a default share with the user/group, read only + CalendarService.share(calendar, item.type, item.identifier, false, false).then(function () { + $scope.$apply(); + }); + }; + + $scope.updateExistingUserShare = function (calendar, userId, writable) { + CalendarService.share(calendar, OC.Share.SHARE_TYPE_USER, userId, writable, true).then(function () { + $scope.$apply(); + }); + }; + + $scope.updateExistingGroupShare = function (calendar, groupId, writable) { + CalendarService.share(calendar, OC.Share.SHARE_TYPE_GROUP, groupId, writable, true).then(function () { + $scope.$apply(); + }); + }; + + $scope.unshareFromUser = function (calendar, userId) { + CalendarService.unshare(calendar, OC.Share.SHARE_TYPE_USER, userId).then(function () { + $scope.$apply(); + }); + }; + + $scope.unshareFromGroup = function (calendar, groupId) { + CalendarService.unshare(calendar, OC.Share.SHARE_TYPE_GROUP, groupId).then(function () { + $scope.$apply(); + }); + }; + + $scope.findSharee = function (val, calendar) { + return $.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { + format: 'json', + search: val.trim(), + perPage: 200, + itemType: 'principals' + }).then(function (result) { + var users = result.ocs.data.exact.users.concat(result.ocs.data.users); + var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups); + + var userShares = calendar.shares.users; + var groupShares = calendar.shares.groups; + var userSharesLength = userShares.length; + var groupSharesLength = groupShares.length; + var i, j; + + // Filter out current user + var usersLength = users.length; + for (i = 0; i < usersLength; i++) { + if (users[i].value.shareWith === OC.currentUser) { + users.splice(i, 1); + break; + } + } + + // Now filter out all sharees that are already shared with + for (i = 0; i < userSharesLength; i++) { + var share = userShares[i]; + usersLength = users.length; + for (j = 0; j < usersLength; j++) { + if (users[j].value.shareWith === share.id) { + users.splice(j, 1); + break; + } + } + } + + // Combine users and groups + users = users.map(function (item) { + return { + display: item.value.shareWith, + type: OC.Share.SHARE_TYPE_USER, + identifier: item.value.shareWith + }; + }); + + groups = groups.map(function (item) { + return { + display: item.value.shareWith + ' (group)', + type: OC.Share.SHARE_TYPE_GROUP, + identifier: item.value.shareWith + }; + }); + + return groups.concat(users); + }); + }; + + $scope.performUpdate = function (item) { + item.saveEditor(); + CalendarService.update(item.calendar).then(function () { + $rootScope.$broadcast('updatedCalendar', item.calendar); + $rootScope.$broadcast('reloadCalendarList'); + }); + }; + + /** + * Updates the shares of the calendar + */ + $scope.performUpdateShares = function (calendar) { + CalendarService.update(calendar).then(function () { + calendar.dropPreviousState(); + calendar.list.edit = false; + $rootScope.$broadcast('updatedCalendar', calendar); + $rootScope.$broadcast('reloadCalendarList'); + }); + }; + + $scope.triggerEnable = function (item) { + item.calendar.toggleEnabled(); + + CalendarService.update(item.calendar).then(function () { + $rootScope.$broadcast('updatedCalendarsVisibility', item.calendar); + $rootScope.$broadcast('reloadCalendarList'); + }); + }; + + $scope.remove = function (item) { + CalendarService.delete(item.calendar).then(function () { + $scope.$parent.calendars = $scope.$parent.calendars.filter(function (elem) { + return elem !== item.calendar; + }); + if (!$scope.$$phase) { + $scope.$apply(); + } + }); + }; + + $rootScope.$on('reloadCalendarList', function () { + if (!$scope.$$phase) { + $scope.$apply(); + } + }); +}]); + +/** +* Controller: Date Picker Controller +* Description: Takes care for pushing dates from app navigation date picker and fullcalendar. +*/ +app.controller('DatePickerController', ['$scope', 'fc', 'uibDatepickerConfig', function ($scope, fc, uibDatepickerConfig) { + 'use strict'; + + $scope.datepickerOptions = { + formatDay: 'd' + }; + + function getStepSizeFromView() { + switch ($scope.selectedView) { + case 'agendaDay': + return 'day'; + + case 'agendaWeek': + return 'week'; + + case 'month': + return 'month'; + } + } + + $scope.dt = new Date(); + $scope.visibility = false; + + $scope.selectedView = angular.element('#fullcalendar').attr('data-defaultView'); + + angular.extend(uibDatepickerConfig, { + showWeeks: false, + startingDay: parseInt(moment().startOf('week').format('d')) + }); + + $scope.today = function () { + $scope.dt = new Date(); + }; + + $scope.prev = function () { + $scope.dt = moment($scope.dt).subtract(1, getStepSizeFromView()).toDate(); + }; + + $scope.next = function () { + $scope.dt = moment($scope.dt).add(1, getStepSizeFromView()).toDate(); + }; + + $scope.toggle = function () { + $scope.visibility = !$scope.visibility; + }; + + $scope.$watch('dt', function (newValue) { + if (fc) { + fc.elm.fullCalendar('gotoDate', newValue); + } + }); + + $scope.$watch('selectedView', function (newValue) { + if (fc) { + fc.elm.fullCalendar('changeView', newValue); + } + }); +}]); + +/** + * Controller: Events Dialog Controller + * Description: Takes care of anything inside the Events Modal. + */ + +app.controller('EditorController', ['$scope', 'TimezoneService', 'AutoCompletionService', '$window', '$uibModalInstance', 'vevent', 'simpleEvent', 'calendar', 'isNew', 'emailAddress', function ($scope, TimezoneService, AutoCompletionService, $window, $uibModalInstance, vevent, simpleEvent, calendar, isNew, emailAddress) { + 'use strict'; + + $scope.properties = simpleEvent; + $scope.is_new = isNew; + $scope.calendar = calendar; + $scope.oldCalendar = isNew ? calendar : vevent.calendar; + $scope.readOnly = isNew ? false : !vevent.calendar.isWritable(); + $scope.selected = 1; + $scope.timezones = []; + $scope.emailAddress = emailAddress; + $scope.edittimezone = $scope.properties.dtstart.parameters.zone !== 'floating' && $scope.properties.dtstart.parameters.zone !== $scope.defaulttimezone || $scope.properties.dtend.parameters.zone !== 'floating' && $scope.properties.dtend.parameters.zone !== $scope.defaulttimezone; + + $scope.preEditingHooks = []; + $scope.postEditingHooks = []; + + $scope.tabs = [{ title: t('calendar', 'Attendees'), value: 1 }, { title: t('calendar', 'Reminders'), value: 2 }, { title: t('calendar', 'Repeating'), value: 3 }]; + + $scope.classSelect = [{ displayname: t('calendar', 'When shared show full event'), type: 'PUBLIC' }, { displayname: t('calendar', 'When shared show only busy'), type: 'CONFIDENTIAL' }, { displayname: t('calendar', 'When shared hide this event'), type: 'PRIVATE' }]; + + $scope.statusSelect = [{ displayname: t('calendar', 'Confirmed'), type: 'CONFIRMED' }, { displayname: t('calendar', 'Tentative'), type: 'TENTATIVE' }, { displayname: t('calendar', 'Cancelled'), type: 'CANCELLED' }]; + + $scope.registerPreHook = function (callback) { + $scope.preEditingHooks.push(callback); + }; + + $uibModalInstance.rendered.then(function () { + if ($scope.properties.dtend.type === 'date') { + $scope.properties.dtend.value = moment($scope.properties.dtend.value.subtract(1, 'days')); + } + + angular.forEach($scope.preEditingHooks, function (callback) { + callback(); + }); + + $scope.tabopener(1); + }); + + $scope.registerPostHook = function (callback) { + $scope.postEditingHooks.push(callback); + }; + + $scope.parse_summary = function () { + var summary = $scope.properties.summary.value; + + // Parse text to extract schedule from string beginning + var timePattern = "(\\d{1,2}:\\d{2}(?:\\s?[ap]\\.?m\\.?)?)"; + var linkPattern = "\\s?[-/]\\s?"; + var scheduleExp = new RegExp("^" + timePattern + "(?:" + linkPattern + timePattern + ")?\\s", 'i'); + var matches = scheduleExp.exec(summary); + + if (matches !== null) { + + // Strip schedule string from summary + summary = summary.substring(matches.shift().length); + + var start = matches[0]; + var end = matches[1]; + + // Convert string to time + var schemes = ["HH:mm", "hh:mm a"]; + start = moment(start, schemes); + if (typeof end !== 'undefined') { + end = moment(end, schemes); + } else { + end = start.clone(); + } + + // Update event properties + $scope.properties.dtstart.value = moment($scope.properties.dtstart.value.set({ 'hour': start.get('hour'), 'minute': start.get('minute') })); + $scope.properties.dtend.value = moment($scope.properties.dtend.value.set({ 'hour': end.get('hour'), 'minute': end.get('minute') })); + $scope.properties.allDay = false; + $scope.toggledAllDay(); + $scope.properties.summary.value = summary; + } + }; + + $scope.proceed = function () { + $scope.prepareClose(); + $uibModalInstance.close({ + action: 'proceed', + calendar: $scope.calendar, + simple: $scope.properties, + vevent: vevent + }); + }; + + $scope.save = function () { + if (!$scope.validate()) { + return; + } + + $scope.prepareClose(); + $scope.properties.patch(); + $uibModalInstance.close({ + action: 'save', + calendar: $scope.calendar, + simple: $scope.properties, + vevent: vevent + }); + }; + + $scope.validate = function () { + var error = false; + if ($scope.properties.summary === null || $scope.properties.summary.value.trim() === '') { + OC.Notification.showTemporary(t('calendar', 'Please add a title!')); + error = true; + } + if ($scope.calendar === null || typeof $scope.calendar === 'undefined') { + OC.Notification.showTemporary(t('calendar', 'Please select a calendar!')); + error = true; + } + + return !error; + }; + + $scope.prepareClose = function () { + if ($scope.properties.allDay) { + $scope.properties.dtstart.type = 'date'; + $scope.properties.dtend.type = 'date'; + $scope.properties.dtend.value.add(1, 'days'); + } else { + $scope.properties.dtstart.type = 'date-time'; + $scope.properties.dtend.type = 'date-time'; + } + + angular.forEach($scope.postEditingHooks, function (callback) { + callback(); + }); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + $scope.delete = function () { + $uibModalInstance.dismiss('delete'); + }; + + $scope.export = function () { + $window.open($scope.oldCalendar.url + vevent.uri); + }; + + /** + * Everything tabs + */ + $scope.tabopener = function (val) { + $scope.selected = val; + if (val === 1) { + $scope.eventsattendeeview = true; + $scope.eventsalarmview = false; + $scope.eventsrepeatview = false; + } else if (val === 2) { + $scope.eventsattendeeview = false; + $scope.eventsalarmview = true; + $scope.eventsrepeatview = false; + } else if (val === 3) { + $scope.eventsattendeeview = false; + $scope.eventsalarmview = false; + $scope.eventsrepeatview = true; + } + }; + /** + * Everything date and time + */ + $scope.$watch('properties.dtstart.value', function (nv, ov) { + var diff = nv.diff(ov, 'seconds'); + if (diff !== 0) { + $scope.properties.dtend.value = moment($scope.properties.dtend.value.add(diff, 'seconds')); + } + }); + + $scope.toggledAllDay = function () { + if ($scope.properties.allDay) { + return; + } + + if ($scope.properties.dtstart.value.isSame($scope.properties.dtend.value)) { + $scope.properties.dtend.value = moment($scope.properties.dtend.value.add(1, 'hours')); + } + + if ($scope.properties.dtstart.parameters.zone === 'floating' && $scope.properties.dtend.parameters.zone === 'floating') { + $scope.properties.dtstart.parameters.zone = $scope.defaulttimezone; + $scope.properties.dtend.parameters.zone = $scope.defaulttimezone; + } + }; + + /** + * Everything timezones + */ + TimezoneService.listAll().then(function (list) { + if ($scope.properties.dtstart.parameters.zone !== 'floating' && list.indexOf($scope.properties.dtstart.parameters.zone) === -1) { + list.push($scope.properties.dtstart.parameters.zone); + } + if ($scope.properties.dtend.parameters.zone !== 'floating' && list.indexOf($scope.properties.dtend.parameters.zone) === -1) { + list.push($scope.properties.dtend.parameters.zone); + } + + angular.forEach(list, function (timezone) { + if (timezone === 'GMT' || timezone === 'Z') { + return; + } + + if (timezone.split('/').length === 1) { + $scope.timezones.push({ + displayname: timezone, + group: t('calendar', 'Global'), + value: timezone + }); + } else { + $scope.timezones.push({ + displayname: timezone.split('/').slice(1).join('/'), + group: timezone.split('/', 1), + value: timezone + }); + } + }); + + $scope.timezones.push({ + displayname: t('calendar', 'None'), + group: t('calendar', 'Global'), + value: 'floating' + }); + }); + + $scope.loadTimezone = function (tzId) { + TimezoneService.get(tzId).then(function (timezone) { + ICAL.TimezoneService.register(tzId, timezone.jCal); + }); + }; + + /** + * Everything location + */ + $scope.searchLocation = function (value) { + return AutoCompletionService.searchLocation(value); + }; + + $scope.selectLocationFromTypeahead = function (item) { + $scope.properties.location.value = item.label; + }; + + /** + * Everything access class + */ + $scope.setClassToDefault = function () { + if ($scope.properties.class === null) { + $scope.properties.class = { + type: 'string', + value: 'PUBLIC' + }; + } + }; + + $scope.setStatusToDefault = function () { + if ($scope.properties.status === null) { + $scope.properties.status = { + type: 'string', + value: 'CONFIRMED' + }; + } + }; +}]); + +/** + * Controller: ImportController + * Description: Takes care of importing calendars + */ + +app.controller('ImportController', ['$scope', '$filter', 'CalendarService', 'VEventService', '$uibModalInstance', 'files', 'ImportFileWrapper', function ($scope, $filter, CalendarService, VEventService, $uibModalInstance, files, ImportFileWrapper) { + 'use strict'; + + $scope.nameSize = 25; + + $scope.rawFiles = files; + $scope.files = []; + + $scope.showCloseButton = false; + $scope.writableCalendars = $scope.calendars.filter(function (elem) { + return elem.isWritable(); + }); + + $scope.import = function (fileWrapper) { + fileWrapper.state = ImportFileWrapper.stateScheduled; + + var importCalendar = function importCalendar(calendar) { + var objects = fileWrapper.splittedICal.objects; + + angular.forEach(objects, function (object) { + VEventService.create(calendar, object, false).then(function (response) { + fileWrapper.state = ImportFileWrapper.stateImporting; + fileWrapper.progress++; + + if (!response) { + fileWrapper.errors++; + } + }); + }); + }; + + if (fileWrapper.selectedCalendar === 'new') { + var name = fileWrapper.splittedICal.name || fileWrapper.file.name; + var color = fileWrapper.splittedICal.color || randColour(); // jshint ignore:line + + var components = []; + if (fileWrapper.splittedICal.vevents.length > 0) { + components.push('vevent'); + components.push('vtodo'); + } + if (fileWrapper.splittedICal.vjournals.length > 0) { + components.push('vjournal'); + } + if (fileWrapper.splittedICal.vtodos.length > 0 && components.indexOf('vtodo') === -1) { + components.push('vtodo'); + } + + CalendarService.create(name, color, components).then(function (calendar) { + if (calendar.components.vevent) { + $scope.calendars.push(calendar); + $scope.writableCalendars.push(calendar); + } + importCalendar(calendar); + fileWrapper.selectedCalendar = calendar.url; + }); + } else { + var calendar = $scope.calendars.filter(function (element) { + return element.url === fileWrapper.selectedCalendar; + })[0]; + importCalendar(calendar); + } + }; + + $scope.preselectCalendar = function (fileWrapper) { + var possibleCalendars = $filter('importCalendarFilter')($scope.writableCalendars, fileWrapper); + if (possibleCalendars.length === 0) { + fileWrapper.selectedCalendar = 'new'; + } else { + fileWrapper.selectedCalendar = possibleCalendars[0].url; + } + }; + + $scope.changeCalendar = function (fileWrapper) { + if (fileWrapper.selectedCalendar === 'new') { + fileWrapper.incompatibleObjectsWarning = false; + } else { + var possibleCalendars = $filter('importCalendarFilter')($scope.writableCalendars, fileWrapper); + fileWrapper.incompatibleObjectsWarning = possibleCalendars.indexOf(fileWrapper.selectedCalendar) === -1; + } + }; + + angular.forEach($scope.rawFiles, function (rawFile) { + var fileWrapper = ImportFileWrapper(rawFile); + fileWrapper.read(function () { + $scope.preselectCalendar(fileWrapper); + $scope.$apply(); + }); + + fileWrapper.register(ImportFileWrapper.hookProgressChanged, function () { + $scope.$apply(); + }); + + fileWrapper.register(ImportFileWrapper.hookDone, function () { + $scope.$apply(); + $scope.closeIfNecessary(); + + var calendar = $scope.calendars.filter(function (element) { + return element.url === fileWrapper.selectedCalendar; + })[0]; + if (calendar.enabled) { + calendar.enabled = false; + calendar.enabled = true; + } + }); + + fileWrapper.register(ImportFileWrapper.hookErrorsChanged, function () { + $scope.$apply(); + }); + + $scope.files.push(fileWrapper); + }); + + $scope.closeIfNecessary = function () { + var unfinishedFiles = $scope.files.filter(function (fileWrapper) { + return !fileWrapper.wasCanceled() && !fileWrapper.isDone(); + }); + var filesEncounteredErrors = $scope.files.filter(function (fileWrapper) { + return fileWrapper.isDone() && fileWrapper.hasErrors(); + }); + + if (unfinishedFiles.length === 0 && filesEncounteredErrors.length === 0) { + $uibModalInstance.close(); + } else if (unfinishedFiles.length === 0 && filesEncounteredErrors.length !== 0) { + $scope.showCloseButton = true; + $scope.$apply(); + } + }; + + $scope.close = function () { + $uibModalInstance.close(); + }; + + $scope.cancelFile = function (fileWrapper) { + fileWrapper.state = ImportFileWrapper.stateCanceled; + $scope.closeIfNecessary(); + }; +}]); + +app.controller('RecurrenceController', ["$scope", function ($scope) { + 'use strict'; + + $scope.rruleNotSupported = false; + + $scope.repeat_options_simple = [{ val: 'NONE', displayname: t('calendar', 'None') }, { val: 'DAILY', displayname: t('calendar', 'Every day') }, { val: 'WEEKLY', displayname: t('calendar', 'Every week') }, { val: 'MONTHLY', displayname: t('calendar', 'Every month') }, { val: 'YEARLY', displayname: t('calendar', 'Every year') } //, + //{val: 'CUSTOM', displayname: t('calendar', 'Custom')} + ]; + + $scope.selected_repeat_end = 'NEVER'; + $scope.repeat_end = [{ val: 'NEVER', displayname: t('calendar', 'never') }, { val: 'COUNT', displayname: t('calendar', 'after') } //, + //{val: 'UNTIL', displayname: t('calendar', 'on date')} + ]; + + $scope.$parent.registerPreHook(function () { + if ($scope.properties.rrule.freq !== 'NONE') { + var unsupportedFREQs = ['SECONDLY', 'MINUTELY', 'HOURLY']; + if (unsupportedFREQs.indexOf($scope.properties.rrule.freq) !== -1) { + $scope.rruleNotSupported = true; + } + + if (typeof $scope.properties.rrule.parameters !== 'undefined') { + var partIds = Object.getOwnPropertyNames($scope.properties.rrule.parameters); + if (partIds.length > 0) { + $scope.rruleNotSupported = true; + } + } + + if ($scope.properties.rrule.count !== null) { + $scope.selected_repeat_end = 'COUNT'; + } else if ($scope.properties.rrule.until !== null) { + $scope.rruleNotSupported = true; + //$scope.selected_repeat_end = 'UNTIL'; + } + + /*if (!moment.isMoment($scope.properties.rrule.until)) { + $scope.properties.rrule.until = moment(); + }*/ + + if ($scope.properties.rrule.interval === null) { + $scope.properties.rrule.interval = 1; + } + } + }); + + $scope.$parent.registerPostHook(function () { + $scope.properties.rrule.dontTouch = $scope.rruleNotSupported; + + if ($scope.selected_repeat_end === 'NEVER') { + $scope.properties.rrule.count = null; + $scope.properties.rrule.until = null; + } + }); + + $scope.resetRRule = function () { + $scope.selected_repeat_end = 'NEVER'; + $scope.properties.rrule.freq = 'NONE'; + $scope.properties.rrule.count = null; + //$scope.properties.rrule.until = null; + $scope.properties.rrule.interval = 1; + $scope.rruleNotSupported = false; + $scope.properties.rrule.parameters = {}; + }; +}]); + +/** + * Controller: SettingController + * Description: Takes care of the Calendar Settings. + */ + +app.controller('SettingsController', ['$scope', '$uibModal', 'SettingsService', function ($scope, $uibModal, SettingsService) { + 'use strict'; + + $scope.settingsCalDavLink = OC.linkToRemote('dav') + '/'; + $scope.settingsCalDavPrincipalLink = OC.linkToRemote('dav') + '/principals/users/' + escapeHTML(encodeURIComponent(oc_current_user)) + '/'; + $scope.skipPopover = angular.element('#fullcalendar').attr('data-skipPopover'); + + angular.element('#import').on('change', function () { + var filesArray = []; + for (var i = 0; i < this.files.length; i++) { + filesArray.push(this.files[i]); + } + + if (filesArray.length > 0) { + $uibModal.open({ + templateUrl: 'import.html', + controller: 'ImportController', + windowClass: 'import', + backdropClass: 'import-backdrop', + keyboard: false, + appendTo: angular.element('#importpopover-container'), + resolve: { + files: function files() { + return filesArray; + } + }, + scope: $scope + }); + } + + angular.element('#import').val(null); + }); + + $scope.updateSkipPopover = function () { + var newValue = $scope.skipPopover; + angular.element('#fullcalendar').attr('data-skipPopover', newValue); + SettingsService.setSkipPopover(newValue); + }; +}]); + +/** +* Controller: SubscriptionController +* Description: Takes care of Subscription List in the App Navigation. +*/ +app.controller('SubscriptionController', ['$scope', function ($scope) {}]); +/* +app.controller('SubscriptionController', ['$scope', '$rootScope', '$window', 'SubscriptionModel', 'CalendarModel', 'Restangular', + function ($scope, $rootScope, $window, SubscriptionModel, CalendarModel, Restangular) { + 'use strict'; + + $scope.subscriptions = SubscriptionModel.getAll(); + var subscriptionResource = Restangular.all('subscriptions'); + + var backendResource = Restangular.all('backends'); + backendResource.getList().then(function (backendObject) { + $scope.subscriptiontypeSelect = SubscriptionModel.getSubscriptionNames(backendObject); + $scope.selectedsubscriptionbackendmodel = $scope.subscriptiontypeSelect[0]; // to remove the empty model. + }, function (response) { + OC.Notification.show(t('calendar', response.data.message)); + }); + + $scope.newSubscriptionUrl = ''; + + $scope.create = function () { + subscriptionResource.post({ + type: $scope.selectedsubscriptionbackendmodel.type, + url: $scope.newSubscriptionUrl + }).then(function (newSubscription) { + SubscriptionModel.create(newSubscription); + $rootScope.$broadcast('createdSubscription', { + subscription: newSubscription + }); + }, function (response) { + OC.Notification.show(t('calendar', response.data.message)); + }); + + $scope.newSubscriptionUrl = ''; + }; + } +]); +*/ + +app.controller('VAlarmController', ["$scope", function ($scope) { + 'use strict'; + + $scope.newReminderId = -1; + + $scope.alarmFactors = [60, //seconds + 60, //minutes + 24, //hours + 7 //days + ]; + + $scope.reminderSelect = [{ displayname: t('calendar', 'At time of event'), trigger: 0 }, { displayname: t('calendar', '5 minutes before'), trigger: -1 * 5 * 60 }, { displayname: t('calendar', '10 minutes before'), trigger: -1 * 10 * 60 }, { displayname: t('calendar', '15 minutes before'), trigger: -1 * 15 * 60 }, { displayname: t('calendar', '30 minutes before'), trigger: -1 * 30 * 60 }, { displayname: t('calendar', '1 hour before'), trigger: -1 * 60 * 60 }, { displayname: t('calendar', '2 hours before'), trigger: -1 * 2 * 60 * 60 }, { displayname: t('calendar', 'Custom'), trigger: 'custom' }]; + + $scope.reminderSelectTriggers = $scope.reminderSelect.map(function (elem) { + return elem.trigger; + }).filter(function (elem) { + return typeof elem === 'number'; + }); + + $scope.reminderTypeSelect = [{ displayname: t('calendar', 'Audio'), type: 'AUDIO' }, { displayname: t('calendar', 'E Mail'), type: 'EMAIL' }, { displayname: t('calendar', 'Pop up'), type: 'DISPLAY' }]; + + $scope.timeUnitReminderSelect = [{ displayname: t('calendar', 'sec'), factor: 1 }, { displayname: t('calendar', 'min'), factor: 60 }, { displayname: t('calendar', 'hours'), factor: 60 * 60 }, { displayname: t('calendar', 'days'), factor: 60 * 60 * 24 }, { displayname: t('calendar', 'week'), factor: 60 * 60 * 24 * 7 }]; + + $scope.timePositionReminderSelect = [{ displayname: t('calendar', 'before'), factor: -1 }, { displayname: t('calendar', 'after'), factor: 1 }]; + + $scope.startEndReminderSelect = [{ displayname: t('calendar', 'start'), type: 'start' }, { displayname: t('calendar', 'end'), type: 'end' }]; + + $scope.$parent.registerPreHook(function () { + angular.forEach($scope.properties.alarm, function (alarm) { + $scope._addEditorProps(alarm); + }); + }); + + $scope.$parent.registerPostHook(function () { + angular.forEach($scope.properties.alarm, function (alarm) { + if (alarm.editor.triggerType === 'absolute') { + alarm.trigger.value = alarm.editor.absMoment; + } + }); + }); + + $scope._addEditorProps = function (alarm) { + angular.extend(alarm, { + editor: { + triggerValue: 0, + triggerBeforeAfter: -1, + triggerTimeUnit: 1, + absMoment: moment(), + editing: false + } + }); + + alarm.editor.reminderSelectValue = $scope.reminderSelectTriggers.indexOf(alarm.trigger.value) !== -1 ? alarm.editor.reminderSelectValue = alarm.trigger.value : alarm.editor.reminderSelectValue = 'custom'; + + alarm.editor.triggerType = alarm.trigger.type === 'duration' ? 'relative' : 'absolute'; + + if (alarm.editor.triggerType === 'relative') { + $scope._prepareRelativeVAlarm(alarm); + } else { + $scope._prepareAbsoluteVAlarm(alarm); + } + + $scope._prepareRepeat(alarm); + }; + + $scope._prepareRelativeVAlarm = function (alarm) { + var unitAndValue = $scope._getUnitAndValue(Math.abs(alarm.trigger.value)); + + angular.extend(alarm.editor, { + triggerBeforeAfter: alarm.trigger.value < 0 ? -1 : 1, + triggerTimeUnit: unitAndValue[0], + triggerValue: unitAndValue[1] + }); + }; + + $scope._prepareAbsoluteVAlarm = function (alarm) { + alarm.editor.absMoment = alarm.trigger.value; + }; + + $scope._prepareRepeat = function (alarm) { + var unitAndValue = $scope._getUnitAndValue(alarm.duration && alarm.duration.value ? alarm.duration.value : 0); + + angular.extend(alarm.editor, { + repeat: !(!alarm.repeat.value || alarm.repeat.value === 0), + repeatNTimes: alarm.editor.repeat ? alarm.repeat.value : 0, + repeatTimeUnit: unitAndValue[0], + repeatNValue: unitAndValue[1] + }); + }; + + $scope._getUnitAndValue = function (value) { + var unit = 1; + + var alarmFactors = [60, 60, 24, 7]; + + for (var i = 0; i < alarmFactors.length && value !== 0; i++) { + var mod = value % alarmFactors[i]; + if (mod !== 0) { + break; + } + + unit *= alarmFactors[i]; + value /= alarmFactors[i]; + } + + return [unit, value]; + }; + + $scope.add = function () { + var setTriggers = []; + angular.forEach($scope.properties.alarm, function (alarm) { + if (alarm.trigger && alarm.trigger.type === 'duration') { + setTriggers.push(alarm.trigger.value); + } + }); + + var triggersToSuggest = []; + angular.forEach($scope.reminderSelect, function (option) { + if (typeof option.trigger !== 'number' || option.trigger > -1 * 15 * 60) { + return; + } + + triggersToSuggest.push(option.trigger); + }); + + var triggerToSet = null; + for (var i = 0; i < triggersToSuggest.length; i++) { + if (setTriggers.indexOf(triggersToSuggest[i]) === -1) { + triggerToSet = triggersToSuggest[i]; + break; + } + } + if (triggerToSet === null) { + triggerToSet = triggersToSuggest[triggersToSuggest.length - 1]; + } + + var alarm = { + id: $scope.newReminderId--, + action: { + type: 'text', + value: 'AUDIO' + }, + trigger: { + type: 'duration', + value: triggerToSet, + related: 'start' + }, + repeat: {}, + duration: {} + }; + + $scope._addEditorProps(alarm); + $scope.properties.alarm.push(alarm); + }; + + $scope.remove = function (alarm) { + $scope.properties.alarm = $scope.properties.alarm.filter(function (elem) { + return elem !== alarm; + }); + }; + + $scope.triggerEdit = function (alarm) { + if (alarm.editor.editing === true) { + alarm.editor.editing = false; + } else { + if ($scope.isEditingReminderSupported(alarm)) { + alarm.editor.editing = true; + } else { + OC.Notification.showTemporary(t('calendar', 'Editing reminders of unknown type not supported.')); + } + } + }; + + $scope.isEditingReminderSupported = function (alarm) { + //WE DON'T AIM TO SUPPORT PROCEDURE + return ['AUDIO', 'DISPLAY', 'EMAIL'].indexOf(alarm.action.value) !== -1; + }; + + $scope.updateReminderSelectValue = function (alarm) { + var factor = alarm.editor.reminderSelectValue; + if (factor !== 'custom') { + alarm.duration = {}; + alarm.repeat = {}; + alarm.trigger.related = 'start'; + alarm.trigger.type = 'duration'; + alarm.trigger.value = parseInt(factor); + + $scope._addEditorProps(alarm); + } + }; + + $scope.updateReminderRelative = function (alarm) { + alarm.trigger.value = parseInt(alarm.editor.triggerBeforeAfter) * parseInt(alarm.editor.triggerTimeUnit) * parseInt(alarm.editor.triggerValue); + + alarm.trigger.type = 'duration'; + }; + + $scope.updateReminderAbsolute = function (alarm) { + if (!moment.isMoment(alarm.trigger.value)) { + alarm.trigger.value = moment(); + } + + alarm.trigger.type = 'date-time'; + }; + + $scope.updateReminderRepeat = function (alarm) { + alarm.repeat.type = 'string'; + alarm.repeat.value = alarm.editor.repeatNTimes; + alarm.duration.type = 'duration'; + alarm.duration.value = parseInt(alarm.editor.repeatNValue) * parseInt(alarm.editor.repeatTimeUnit); + }; +}]); + +/** + * Directive: Colorpicker + * Description: Colorpicker for the Calendar app. + */ +app.directive('colorpicker', ["ColorUtility", function (ColorUtility) { + 'use strict'; + + return { + scope: { + selected: '=', + customizedColors: '=colors' + }, + restrict: 'AE', + templateUrl: OC.filePath('calendar', 'templates', 'colorpicker.html'), + link: function link(scope, element, attr) { + scope.colors = scope.customizedColors || ColorUtility.colors; + scope.selected = scope.selected || scope.colors[0]; + scope.random = "#000000"; + + scope.randomizeColour = function () { + scope.random = ColorUtility.randomColor(); + scope.pick(scope.random); + }; + + scope.pick = function (color) { + scope.selected = color; + }; + } + }; +}]); + +app.directive('ocdatetimepicker', ["$compile", "$timeout", function ($compile, $timeout) { + 'use strict'; + + return { + restrict: 'E', + require: 'ngModel', + scope: { + disabletime: '=disabletime' + }, + link: function link(scope, element, attrs, ngModelCtrl) { + var templateHTML = ''; + templateHTML += ''; + var template = angular.element(templateHTML); + + scope.date = null; + scope.time = null; + + $compile(template)(scope); + element.append(template); + + function updateFromUserInput() { + var date = element.find('.events--date').datepicker('getDate'), + hours = 0, + minutes = 0; + + if (!scope.disabletime) { + hours = element.find('.events--time').timepicker('getHour'); + minutes = element.find('.events--time').timepicker('getMinute'); + } + + var m = moment(date); + m.hours(hours); + m.minutes(minutes); + m.seconds(0); + + //Hiding the timepicker is an ugly hack to mitigate a bug in the timepicker + //We might want to consider using another timepicker in the future + element.find('.events--time').timepicker('hide'); + ngModelCtrl.$setViewValue(m); + } + + var localeData = moment.localeData(); + function initDatePicker() { + element.find('.events--date').datepicker({ + dateFormat: localeData.longDateFormat('L').toLowerCase().replace('yy', 'y').replace('yyy', 'yy'), + monthNames: moment.months(), + monthNamesShort: moment.monthsShort(), + dayNames: moment.weekdays(), + dayNamesMin: moment.weekdaysMin(), + dayNamesShort: moment.weekdaysShort(), + firstDay: +localeData.firstDayOfWeek(), + minDate: null, + showOtherMonths: true, + selectOtherMonths: true, + onClose: updateFromUserInput + }); + } + function initTimepicker() { + element.find('.events--time').timepicker({ + showPeriodLabels: false, + showLeadingZero: true, + showPeriod: localeData.longDateFormat('LT').toLowerCase().indexOf('a') !== -1, + duration: 0, + onClose: updateFromUserInput + }); + } + + initDatePicker(); + initTimepicker(); + + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (value) { + if (moment.isMoment(value)) { + element.find('.events--date').datepicker('setDate', value.toDate()); + element.find('.events--time').timepicker('setTime', value.toDate()); + } + }); + element.on('$destroy', function () { + element.find('.events--date').datepicker('destroy'); + element.find('.events--time').timepicker('destroy'); + }); + } + }; +}]); + +app.constant('fc', {}).directive('fc', ["fc", "$window", function (fc, $window) { + 'use strict'; + + return { + restrict: 'A', + scope: {}, + link: function link(scope, elm, attrs) { + var monthNames = []; + var monthNamesShort = []; + for (var i = 0; i < 12; i++) { + monthNames.push(moment.localeData().months(moment([0, i]), '')); + monthNamesShort.push(moment.localeData().monthsShort(moment([0, i]), '')); + } + + var dayNames = []; + var dayNamesShort = []; + var momentWeekHelper = moment().startOf('week'); + momentWeekHelper.subtract(momentWeekHelper.format('d')); + for (var _i = 0; _i < 7; _i++) { + dayNames.push(moment.localeData().weekdays(momentWeekHelper)); + dayNamesShort.push(moment.localeData().weekdaysShort(momentWeekHelper)); + momentWeekHelper.add(1, 'days'); + } + + var firstDay = +moment().startOf('week').format('d'); + + var headerSize = angular.element('#header').height(); + var windowElement = angular.element($window); + windowElement.bind('resize', function () { + var newHeight = windowElement.height() - headerSize; + fc.elm.fullCalendar('option', 'height', newHeight); + }); + + var baseConfig = { + dayNames: dayNames, + dayNamesShort: dayNamesShort, + defaultView: attrs.defaultview, + editable: true, + eventLimit: true, + firstDay: firstDay, + header: false, + height: windowElement.height() - headerSize, + lang: moment.locale(), + monthNames: monthNames, + monthNamesShort: monthNamesShort, + nowIndicator: true, + selectable: true + }; + var controllerConfig = scope.$parent.fcConfig; + var config = angular.extend({}, baseConfig, controllerConfig); + + fc.elm = $(elm).fullCalendar(config); + } + }; +}]); + +/** +* Directive: Loading +* Description: Can be used to incorperate loading behavior, anywhere. +*/ + +app.directive('loading', [function () { + 'use strict'; + + return { + restrict: 'E', + replace: true, + template: "
", + link: function link($scope, element, attr) { + $scope.$watch('loading', function (val) { + if (val) { + $(element).show(); + } else { + $(element).hide(); + } + }); + } + }; +}]); + +/** +* Controller: Modal +* Description: The jQuery Model ported to angularJS as a directive. +*/ + +app.directive('openDialog', function () { + 'use strict'; + + return { + restrict: 'A', + link: function link(scope, elem, attr, ctrl) { + var dialogId = '#' + attr.openDialog; + elem.bind('click', function (e) { + $(dialogId).dialog('open'); + }); + } + }; +}); + +app.directive('onToggleShow', function () { + 'use strict'; + + return { + restrict: 'A', + scope: { + 'onToggleShow': '@' + }, + link: function link(scope, elem) { + elem.click(function () { + var target = $(scope.onToggleShow); + target.toggle(); + }); + + scope.$on('documentClicked', function (s, event) { + var target = $(scope.onToggleShow); + + if (event.target !== elem[0]) { + target.hide(); + } + }); + } + }; +}); + +app.filter('attendeeFilter', function () { + 'use strict'; + + return function (attendee) { + if ((typeof attendee === 'undefined' ? 'undefined' : _typeof(attendee)) !== 'object' || !attendee) { + return ''; + } else if (_typeof(attendee.parameters) === 'object' && typeof attendee.parameters.cn === 'string') { + return attendee.parameters.cn; + } else if (typeof attendee.value === 'string' && attendee.value.startsWith('MAILTO:')) { + return attendee.value.substr(7); + } else { + return attendee.value || ''; + } + }; +}); + +app.filter('attendeeNotOrganizerFilter', function () { + 'use strict'; + + return function (attendees, organizer) { + if (typeof organizer !== 'string' || organizer === '') { + return Array.isArray(attendees) ? attendees : []; + } + + if (!Array.isArray(attendees)) { + return []; + } + + var organizerValue = 'MAILTO:' + organizer; + return attendees.filter(function (element) { + if ((typeof element === 'undefined' ? 'undefined' : _typeof(element)) !== 'object') { + return false; + } else { + return element.value !== organizerValue; + } + }); + }; +}); + +app.filter('calendarFilter', function () { + 'use strict'; + + return function (calendars) { + if (!Array.isArray(calendars)) { + return []; + } + + return calendars.filter(function (element) { + if ((typeof element === 'undefined' ? 'undefined' : _typeof(element)) !== 'object') { + return false; + } else { + return element.isWritable(); + } + }); + }; +}); + +app.filter('calendarListFilter', ["CalendarListItem", function (CalendarListItem) { + 'use strict'; + + return function (calendarListItems) { + if (!Array.isArray(calendarListItems)) { + return []; + } + + return calendarListItems.filter(function (item) { + if (!CalendarListItem.isCalendarListItem(item)) { + return false; + } + return item.calendar.isWritable(); + }); + }; +}]); + +app.filter('calendarSelectorFilter', function () { + 'use strict'; + + return function (calendars, calendar) { + if (!Array.isArray(calendars)) { + return []; + } + + var options = calendars.filter(function (c) { + return c.isWritable(); + }); + + if ((typeof calendar === 'undefined' ? 'undefined' : _typeof(calendar)) !== 'object' || !calendar) { + return options; + } + + if (!calendar.isWritable()) { + return [calendar]; + } else { + if (options.indexOf(calendar) === -1) { + options.push(calendar); + } + + return options; + } + }; +}); + +app.filter('datepickerFilter', function () { + 'use strict'; + + return function (datetime, view) { + if (!(datetime instanceof Date) || typeof view !== 'string') { + return ''; + } + + switch (view) { + case 'agendaDay': + return moment(datetime).format('ll'); + + case 'agendaWeek': + return t('calendar', 'Week {number} of {year}', { number: moment(datetime).week(), + year: moment(datetime).week() === 1 ? moment(datetime).add(1, 'week').year() : moment(datetime).year() }); + + case 'month': + return moment(datetime).week() === 1 ? moment(datetime).add(1, 'week').format('MMMM GGGG') : moment(datetime).format('MMMM GGGG'); + + default: + return ''; + } + }; +}); + +app.filter('importCalendarFilter', function () { + 'use strict'; + + return function (calendars, file) { + if (!Array.isArray(calendars) || (typeof file === 'undefined' ? 'undefined' : _typeof(file)) !== 'object' || !file || _typeof(file.splittedICal) !== 'object' || !file.splittedICal) { + return []; + } + + var events = file.splittedICal.vevents.length, + journals = file.splittedICal.vjournals.length, + todos = file.splittedICal.vtodos.length; + + return calendars.filter(function (calendar) { + if (events !== 0 && !calendar.components.vevent) { + return false; + } + if (journals !== 0 && !calendar.components.vjournal) { + return false; + } + if (todos !== 0 && !calendar.components.vtodo) { + return false; + } + + return true; + }); + }; +}); + +app.filter('importErrorFilter', function () { + 'use strict'; + + return function (file) { + if ((typeof file === 'undefined' ? 'undefined' : _typeof(file)) !== 'object' || !file || typeof file.errors !== 'number') { + return ''; + } + + //TODO - use n instead of t to use proper plurals in all translations + switch (file.errors) { + case 0: + return t('calendar', 'Successfully imported'); + + case 1: + return t('calendar', 'Partially imported, 1 failure'); + + default: + return t('calendar', 'Partially imported, {n} failures', { + n: file.errors + }); + } + }; +}); + +app.filter('simpleReminderDescription', function () { + 'use strict'; + + var actionMapper = { + AUDIO: t('calendar', 'Audio alarm'), + DISPLAY: t('calendar', 'Pop-up'), + EMAIL: t('calendar', 'E-Mail'), + NONE: t('calendar', 'None') + }; + + function getActionName(alarm) { + var name = alarm.action.value; + if (name && actionMapper.hasOwnProperty(name)) { + return actionMapper[name]; + } else { + return name; + } + } + + return function (alarm) { + if ((typeof alarm === 'undefined' ? 'undefined' : _typeof(alarm)) !== 'object' || !alarm || _typeof(alarm.trigger) !== 'object' || !alarm.trigger) { + return ''; + } + + var relative = alarm.trigger.type === 'duration'; + var relatedToStart = alarm.trigger.related === 'start'; + if (relative) { + var timeString = moment.duration(Math.abs(alarm.trigger.value), 'seconds').humanize(); + if (alarm.trigger.value < 0) { + if (relatedToStart) { + return t('calendar', '{type} {time} before the event starts', { type: getActionName(alarm), time: timeString }); + } else { + return t('calendar', '{type} {time} before the event ends', { type: getActionName(alarm), time: timeString }); + } + } else if (alarm.trigger.value > 0) { + if (relatedToStart) { + return t('calendar', '{type} {time} after the event starts', { type: getActionName(alarm), time: timeString }); + } else { + return t('calendar', '{type} {time} after the event ends', { type: getActionName(alarm), time: timeString }); + } + } else { + if (relatedToStart) { + return t('calendar', '{type} at the event\'s start', { type: getActionName(alarm) }); + } else { + return t('calendar', '{type} at the event\'s end', { type: getActionName(alarm) }); + } + } + } else { + if (alarm.editor && moment.isMoment(alarm.editor.absMoment)) { + return t('calendar', '{type} at {time}', { + type: getActionName(alarm), + time: alarm.editor.absMoment.format('LLLL') + }); + } else { + return ''; + } + } + }; +}); + +app.filter('subscriptionFilter', function () { + 'use strict'; + + return function (calendars) { + if (!Array.isArray(calendars)) { + return []; + } + + return calendars.filter(function (element) { + if ((typeof element === 'undefined' ? 'undefined' : _typeof(element)) !== 'object') { + return false; + } else { + return !element.isWritable(); + } + }); + }; +}); + +app.filter('subscriptionListFilter', ["CalendarListItem", function (CalendarListItem) { + 'use strict'; + + return function (calendarListItems) { + if (!Array.isArray(calendarListItems)) { + return []; + } + + return calendarListItems.filter(function (item) { + if (!CalendarListItem.isCalendarListItem(item)) { + return false; + } + return !item.calendar.isWritable(); + }); + }; +}]); + +app.filter('timezoneFilter', ['$filter', function ($filter) { + 'use strict'; + + return function (timezone) { + if (typeof timezone !== 'string') { + return ''; + } + + timezone = timezone.split('_').join(' '); + + var elements = timezone.split('/'); + if (elements.length === 1) { + return elements[0]; + } else { + var continent = elements[0]; + var city = $filter('timezoneWithoutContinentFilter')(elements.slice(1).join('/')); + + return city + ' (' + continent + ')'; + } + }; +}]); + +app.filter('timezoneWithoutContinentFilter', function () { + 'use strict'; + + return function (timezone) { + timezone = timezone.split('_').join(' '); + timezone = timezone.replace('St ', 'St. '); + + return timezone.split('/').join(' - '); + }; +}); + +app.factory('CalendarListItem', ["Calendar", function (Calendar) { + 'use strict'; + + function CalendarListItem(calendar) { + var context = { + calendar: calendar, + isEditingShares: false, + isEditingProperties: false, + isDisplayingCalDAVUrl: false + }; + var iface = { + _isACalendarListItemObject: true + }; + + if (!Calendar.isCalendar(calendar)) { + return null; + } + + Object.defineProperties(iface, { + calendar: { + get: function get() { + return context.calendar; + } + } + }); + + iface.displayCalDAVUrl = function () { + return context.isDisplayingCalDAVUrl; + }; + + iface.showCalDAVUrl = function () { + context.isDisplayingCalDAVUrl = true; + }; + + iface.hideCalDAVUrl = function () { + context.isDisplayingCalDAVUrl = false; + }; + + iface.isEditingShares = function () { + return context.isEditingShares; + }; + + iface.toggleEditingShares = function () { + context.isEditingShares = !context.isEditingShares; + }; + + iface.isEditing = function () { + return context.isEditingProperties; + }; + + iface.displayActions = function () { + return !iface.isEditing(); + }; + + iface.displayColorIndicator = function () { + return !iface.isEditing() && !context.calendar.isRendering(); + }; + + iface.displaySpinner = function () { + return !iface.isEditing() && context.calendar.isRendering(); + }; + + iface.openEditor = function () { + iface.color = context.calendar.color; + iface.displayname = context.calendar.displayname; + + context.isEditingProperties = true; + }; + + iface.cancelEditor = function () { + iface.color = ''; + iface.displayname = ''; + + context.isEditingProperties = false; + }; + + iface.saveEditor = function () { + context.calendar.color = iface.color; + context.calendar.displayname = iface.displayname; + + iface.color = ''; + iface.displayname = ''; + + context.isEditingProperties = false; + }; + + //Properties for ng-model of calendar editor + iface.color = ''; + iface.displayname = ''; + + iface.order = 0; + + iface.selectedSharee = ''; + + return iface; + } + + CalendarListItem.isCalendarListItem = function (obj) { + return (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null && obj._isACalendarListItemObject === true; + }; + + return CalendarListItem; +}]); + +app.factory('Calendar', ["$window", "Hook", "VEventService", "TimezoneService", "ColorUtility", "RandomStringService", function ($window, Hook, VEventService, TimezoneService, ColorUtility, RandomStringService) { + 'use strict'; + + function Calendar(url, props) { + url = url || ''; + props = props || {}; + + var context = { + fcEventSource: {}, + components: props.components, + mutableProperties: { + color: props.color, + displayname: props.displayname, + enabled: props.enabled, + order: props.order + }, + updatedProperties: [], + tmpId: RandomStringService.generate(), + url: url, + owner: props.owner, + shares: props.shares, + warnings: [], + shareable: props.shareable, + writable: props.writable, + writableProperties: props.writableProperties + }; + var iface = { + _isACalendarObject: true + }; + + context.fcEventSource.events = function (start, end, timezone, callback) { + var fcAPI = this; + + TimezoneService.get(timezone).then(function (tz) { + context.fcEventSource.isRendering = true; + iface.emit(Calendar.hookFinishedRendering); + + VEventService.getAll(iface, start, end).then(function (events) { + var vevents = []; + for (var i = 0; i < events.length; i++) { + var vevent; + try { + vevent = events[i].getFcEvent(start, end, tz); + } catch (err) { + iface.addWarning(err.toString()); + console.log(err); + console.log(events[i]); + continue; + } + vevents = vevents.concat(vevent); + } + + callback(vevents); + fcAPI.reportEvents(fcAPI.clientEvents()); + context.fcEventSource.isRendering = false; + + iface.emit(Calendar.hookFinishedRendering); + }); + }); + }; + context.fcEventSource.editable = context.writable; + context.fcEventSource.calendar = iface; + context.fcEventSource.isRendering = false; + + context.setUpdated = function (property) { + if (context.updatedProperties.indexOf(property) === -1) { + context.updatedProperties.push(property); + } + }; + + Object.defineProperties(iface, { + color: { + get: function get() { + return context.mutableProperties.color; + }, + set: function set(color) { + var oldColor = context.mutableProperties.color; + if (color === oldColor) { + return; + } + context.mutableProperties.color = color; + context.setUpdated('color'); + iface.emit(Calendar.hookColorChanged, color, oldColor); + } + }, + textColor: { + get: function get() { + var colors = ColorUtility.extractRGBFromHexString(context.mutableProperties.color); + return ColorUtility.generateTextColorFromRGB(colors.r, colors.g, colors.b); + } + }, + displayname: { + get: function get() { + return context.mutableProperties.displayname; + }, + set: function set(displayname) { + var oldDisplayname = context.mutableProperties.displayname; + if (displayname === oldDisplayname) { + return; + } + context.mutableProperties.displayname = displayname; + context.setUpdated('displayname'); + iface.emit(Calendar.hookDisplaynameChanged, displayname, oldDisplayname); + } + }, + enabled: { + get: function get() { + return context.mutableProperties.enabled; + }, + set: function set(enabled) { + var oldEnabled = context.mutableProperties.enabled; + if (enabled === oldEnabled) { + return; + } + context.mutableProperties.enabled = enabled; + context.setUpdated('enabled'); + iface.emit(Calendar.hookEnabledChanged, enabled, oldEnabled); + } + }, + order: { + get: function get() { + return context.mutableProperties.order; + }, + set: function set(order) { + var oldOrder = context.mutableProperties.order; + if (order === oldOrder) { + return; + } + context.mutableProperties.order = order; + context.setUpdated('order'); + iface.emit(Calendar.hookOrderChanged, order, oldOrder); + } + + }, + components: { + get: function get() { + return context.components; + } + }, + url: { + get: function get() { + return context.url; + } + }, + caldav: { + get: function get() { + return $window.location.origin + context.url; + } + }, + fcEventSource: { + get: function get() { + return context.fcEventSource; + } + }, + shares: { + get: function get() { + return context.shares; + } + }, + tmpId: { + get: function get() { + return context.tmpId; + } + }, + warnings: { + get: function get() { + return context.warnings; + } + }, + owner: { + get: function get() { + return context.owner; + } + } + }); + + iface.hasUpdated = function () { + return context.updatedProperties.length !== 0; + }; + + iface.getUpdated = function () { + return context.updatedProperties; + }; + + iface.resetUpdated = function () { + context.updatedProperties = []; + }; + + iface.addWarning = function (msg) { + context.warnings.push(msg); + }; + + iface.hasWarnings = function () { + return context.warnings.length > 0; + }; + + iface.resetWarnings = function () { + context.warnings = []; + }; + + iface.toggleEnabled = function () { + context.mutableProperties.enabled = !context.mutableProperties.enabled; + context.setUpdated('enabled'); + iface.emit(Calendar.hookEnabledChanged, context.mutableProperties.enabled, !context.mutableProperties.enabled); + }; + + iface.isShared = function () { + return context.shares.groups.length !== 0 || context.shares.users.length !== 0; + }; + + iface.isPublished = function () { + return false; + }; + + iface.isShareable = function () { + return context.shareable; + }; + + iface.isPublishable = function () { + return false; + }; + + iface.isRendering = function () { + return context.fcEventSource.isRendering; + }; + + iface.isWritable = function () { + return context.writable; + }; + + iface.arePropertiesWritable = function () { + return context.writableProperties; + }; + + Object.assign(iface, Hook(context)); + + return iface; + } + + Calendar.isCalendar = function (obj) { + return (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null && obj._isACalendarObject === true; + }; + + Calendar.hookFinishedRendering = 1; + Calendar.hookColorChanged = 2; + Calendar.hookDisplaynameChanged = 3; + Calendar.hookEnabledChanged = 4; + Calendar.hookOrderChanged = 5; + + return Calendar; +}]); + +app.factory('FcEvent', ["SimpleEvent", function (SimpleEvent) { + 'use strict'; + + /** + * check if dtstart and dtend are both of type date + * @param dtstart + * @param dtend + * @returns {boolean} + */ + + function isEventAllDay(dtstart, dtend) { + return dtstart.icaltype === 'date' && dtend.icaltype === 'date'; + } + + /** + * get recurrence id from event + * @param {Component} event + * @returns {string} + */ + function getRecurrenceIdFromEvent(event) { + return event.hasProperty('recurrence-id') ? event.getFirstPropertyValue('recurrence-id').toICALString() : null; + } + + /** + * get calendar related information about event + * @param vevent + * @returns {{calendar: *, editable: *, backgroundColor: *, borderColor: *, textColor: *, className: *[]}} + */ + function getCalendarRelatedProps(vevent) { + return { + calendar: vevent.calendar, + editable: vevent.calendar.isWritable(), + className: ['fcCalendar-id-' + vevent.calendar.tmpId] + }; + } + + /** + * get event related information about event + * @param {Component} event + * @returns {{title: string}} + */ + function getEventRelatedProps(event) { + return { + title: event.getFirstPropertyValue('summary') + }; + } + + /** + * get unique id for fullcalendar + * @param {VEvent} vevent + * @param {Component} event + * @returns {string} + */ + function getFcEventId(vevent, event) { + var id = vevent.uri; + var recurrenceId = getRecurrenceIdFromEvent(event); + if (recurrenceId) { + id += recurrenceId; + } + + return id; + } + + /** + * @constructor + * @param {VEvent} vevent + * @param {Component} event + * @param {icaltime} start + * @param {icaltime} end + */ + function FcEvent(vevent, event, start, end) { + var iCalEvent = new ICAL.Event(event); + + angular.extend(this, { + vevent: vevent, + event: event, + id: getFcEventId(vevent, event), + allDay: isEventAllDay(start, end), + start: start.toJSDate(), + end: end.toJSDate(), + repeating: iCalEvent.isRecurring() + }, getCalendarRelatedProps(vevent), getEventRelatedProps(event)); + } + + FcEvent.prototype = { + get backgroundColor() { + return this.vevent.calendar.color; + }, + get borderColor() { + return this.vevent.calendar.color; + }, + get textColor() { + return this.vevent.calendar.textColor; + }, + /** + * get SimpleEvent for current fcEvent + * @returns {SimpleEvent} + */ + getSimpleEvent: function getSimpleEvent() { + return new SimpleEvent(this.event); + }, + /** + * moves the event to a different position + * @param {Duration} delta + */ + drop: function drop(delta) { + delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); + + if (this.event.hasProperty('dtstart')) { + var dtstart = this.event.getFirstPropertyValue('dtstart'); + dtstart.addDuration(delta); + this.event.updatePropertyWithValue('dtstart', dtstart); + } + + if (this.event.hasProperty('dtend')) { + var dtend = this.event.getFirstPropertyValue('dtend'); + dtend.addDuration(delta); + this.event.updatePropertyWithValue('dtend', dtend); + } + }, + /** + * resizes the event + * @param {moment.duration} delta + */ + resize: function resize(delta) { + delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); + + if (this.event.hasProperty('duration')) { + var duration = this.event.getFirstPropertyValue('duration'); + duration.fromSeconds(delta.toSeconds() + duration.toSeconds()); + this.event.updatePropertyWithValue('duration', duration); + } else if (this.event.hasProperty('dtend')) { + var dtend = this.event.getFirstPropertyValue('dtend'); + dtend.addDuration(delta); + this.event.updatePropertyWithValue('dtend', dtend); + } else if (this.event.hasProperty('dtstart')) { + var dtstart = event.getFirstPropertyValue('dtstart').clone(); + dtstart.addDuration(delta); + this.event.addPropertyWithValue('dtend', dtstart); + } + } + }; + + return FcEvent; +}]); + +app.factory('Hook', function () { + 'use strict'; + + return function Hook(context) { + context.hooks = {}; + var iface = {}; + + iface.emit = function (identifier, newValue, oldValue) { + if (Array.isArray(context.hooks[identifier])) { + context.hooks[identifier].forEach(function (callback) { + callback(newValue, oldValue); + }); + } + }; + + iface.register = function (identifier, callback) { + context.hooks[identifier] = context.hooks[identifier] || []; + context.hooks[identifier].push(callback); + }; + + return iface; + }; +}); + +app.factory('ImportFileWrapper', ["Hook", "ICalSplitterUtility", function (Hook, ICalSplitterUtility) { + 'use strict'; + + function ImportFileWrapper(file) { + var context = { + file: file, + splittedICal: null, + selectedCalendar: null, + state: 0, + errors: 0, + progress: 0, + progressToReach: 0 + }; + var iface = { + _isAImportFileWrapperObject: true + }; + + context.checkIsDone = function () { + if (context.progress === context.progressToReach) { + context.state = ImportFileWrapper.stateDone; + iface.emit(ImportFileWrapper.hookDone); + } + }; + + Object.defineProperties(iface, { + file: { + get: function get() { + return context.file; + } + }, + splittedICal: { + get: function get() { + return context.splittedICal; + } + }, + selectedCalendar: { + get: function get() { + return context.selectedCalendar; + }, + set: function set(selectedCalendar) { + context.selectedCalendar = selectedCalendar; + } + }, + state: { + get: function get() { + return context.state; + }, + set: function set(state) { + if (typeof state === 'number') { + context.state = state; + } + } + }, + errors: { + get: function get() { + return context.errors; + }, + set: function set(errors) { + if (typeof errors === 'number') { + var oldErrors = context.errors; + context.errors = errors; + iface.emit(ImportFileWrapper.hookErrorsChanged, errors, oldErrors); + } + } + }, + progress: { + get: function get() { + return context.progress; + }, + set: function set(progress) { + if (typeof progress === 'number') { + var oldProgress = context.progress; + context.progress = progress; + iface.emit(ImportFileWrapper.hookProgressChanged, progress, oldProgress); + + context.checkIsDone(); + } + } + }, + progressToReach: { + get: function get() { + return context.progressToReach; + } + } + }); + + iface.wasCanceled = function () { + return context.state === ImportFileWrapper.stateCanceled; + }; + + iface.isAnalyzing = function () { + return context.state === ImportFileWrapper.stateAnalyzing; + }; + + iface.isAnalyzed = function () { + return context.state === ImportFileWrapper.stateAnalyzed; + }; + + iface.isScheduled = function () { + return context.state === ImportFileWrapper.stateScheduled; + }; + + iface.isImporting = function () { + return context.state === ImportFileWrapper.stateImporting; + }; + + iface.isDone = function () { + return context.state === ImportFileWrapper.stateDone; + }; + + iface.hasErrors = function () { + return context.errors > 0; + }; + + iface.read = function (afterReadCallback) { + var reader = new FileReader(); + + reader.onload = function (event) { + context.splittedICal = ICalSplitterUtility.split(event.target.result); + context.progressToReach = context.splittedICal.vevents.length + context.splittedICal.vjournals.length + context.splittedICal.vtodos.length; + iface.state = ImportFileWrapper.stateAnalyzed; + afterReadCallback(); + }; + + reader.readAsText(file); + }; + + Object.assign(iface, Hook(context)); + + return iface; + } + + ImportFileWrapper.isImportWrapper = function (obj) { + return obj instanceof ImportFileWrapper || (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null && obj._isAImportFileWrapperObject !== null; + }; + + ImportFileWrapper.stateCanceled = -1; + ImportFileWrapper.stateAnalyzing = 0; + ImportFileWrapper.stateAnalyzed = 1; + ImportFileWrapper.stateScheduled = 2; + ImportFileWrapper.stateImporting = 3; + ImportFileWrapper.stateDone = 4; + + ImportFileWrapper.hookProgressChanged = 1; + ImportFileWrapper.hookDone = 2; + ImportFileWrapper.hookErrorsChanged = 3; + + return ImportFileWrapper; +}]); + +app.factory('SimpleEvent', function () { + 'use strict'; + + /** + * structure of simple data + */ + + var defaults = { + 'summary': null, + 'location': null, + //'created': null, + //'last-modified': null, + 'organizer': null, + 'class': null, + 'description': null, + //'url': null, + 'status': null, + //'resources': null, + 'alarm': null, + 'attendee': null, + //'categories': null, + 'dtstart': null, + 'dtend': null, + 'repeating': null, + 'rdate': null, + 'rrule': null, + 'exdate': null + }; + + var attendeeParameters = ['role', 'rvsp', 'partstat', 'cutype', 'cn', 'delegated-from', 'delegated-to']; + + var organizerParameters = ['cn']; + + /** + * parsers of supported properties + */ + var simpleParser = { + date: function date(data, vevent, key, parameters) { + parameters = (parameters || []).concat(['tzid']); + simpleParser._parseSingle(data, vevent, key, parameters, function (p) { + return p.type === 'duration' ? p.getFirstValue().toSeconds() : moment(p.getFirstValue().toJSDate()); + }); + }, + dates: function dates(data, vevent, key, parameters) { + parameters = (parameters || []).concat(['tzid']); + simpleParser._parseMultiple(data, vevent, key, parameters, function (p) { + var values = p.getValues(), + usableValues = []; + for (var vKey in values) { + if (!values.hasOwnProperty(vKey)) { + continue; + } + + usableValues.push(p.type === 'duration' ? values[vKey].toSeconds() : moment(values[vKey].toJSDate())); + } + + return usableValues; + }); + }, + string: function string(data, vevent, key, parameters) { + simpleParser._parseSingle(data, vevent, key, parameters, function (p) { + return p.isMultiValue ? p.getValues() : p.getFirstValue(); + }); + }, + strings: function strings(data, vevent, key, parameters) { + simpleParser._parseMultiple(data, vevent, key, parameters, function (p) { + return p.isMultiValue ? p.getValues() : p.getFirstValue(); + }); + }, + _parseSingle: function _parseSingle(data, vevent, key, parameters, valueParser) { + var prop = vevent.getFirstProperty(key); + if (!prop) { + return; + } + + data[key] = { + parameters: simpleParser._parseParameters(prop, parameters), + type: prop.type + }; + + if (prop.isMultiValue) { + angular.extend(data[key], { + values: valueParser(prop) + }); + } else { + angular.extend(data[key], { + value: valueParser(prop) + }); + } + }, + _parseMultiple: function _parseMultiple(data, vevent, key, parameters, valueParser) { + data[key] = data[key] || []; + + var properties = vevent.getAllProperties(key), + group = 0; + + for (var pKey in properties) { + if (!properties.hasOwnProperty(pKey)) { + continue; + } + + var currentElement = { + group: group, + parameters: simpleParser._parseParameters(properties[pKey], parameters), + type: properties[pKey].type + }; + + if (properties[pKey].isMultiValue) { + angular.extend(currentElement, { + values: valueParser(properties[pKey]) + }); + } else { + angular.extend(currentElement, { + value: valueParser(properties[pKey]) + }); + } + + data[key].push(currentElement); + properties[pKey].setParameter('x-oc-group-id', group.toString()); + group++; + } + }, + _parseParameters: function _parseParameters(prop, para) { + var parameters = {}; + + if (!para) { + return parameters; + } + + for (var i = 0, l = para.length; i < l; i++) { + parameters[para[i]] = prop.getParameter(para[i]); + } + + return parameters; + } + }; + + var simpleReader = { + date: function date(vevent, oldSimpleData, newSimpleData, key, parameters) { + parameters = (parameters || []).concat(['tzid']); + simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + if (v.type === 'duration') { + return ICAL.Duration.fromSeconds(v.value); + } else { + return ICAL.Time.fromJSDate(v.value.toDate()); + } + }); + }, + dates: function dates(vevent, oldSimpleData, newSimpleData, key, parameters) { + parameters = (parameters || []).concat(['tzid']); + simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + var values = []; + + for (var i = 0, length = v.values.length; i < length; i++) { + if (v.type === 'duration') { + values.push(ICAL.Duration.fromSeconds(v.values[i])); + } else { + values.push(ICAL.Time.fromJSDate(v.values[i].toDate())); + } + } + + return values; + }); + }, + string: function string(vevent, oldSimpleData, newSimpleData, key, parameters) { + simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + return isMultiValue ? v.values : v.value; + }); + }, + strings: function strings(vevent, oldSimpleData, newSimpleData, key, parameters) { + simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + return isMultiValue ? v.values : v.value; + }); + }, + _readSingle: function _readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { + if (!newSimpleData[key]) { + return; + } + if (!newSimpleData[key].hasOwnProperty('value') && !newSimpleData[key].hasOwnProperty('values')) { + return; + } + var isMultiValue = newSimpleData[key].hasOwnProperty('values'); + + var prop = vevent.updatePropertyWithValue(key, valueReader(newSimpleData[key], isMultiValue)); + simpleReader._readParameters(prop, newSimpleData[key], parameters); + }, + _readMultiple: function _readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { + var oldGroups = [], + properties = null, + pKey = null, + groupId; + + oldSimpleData[key] = oldSimpleData[key] || []; + for (var i = 0, oldLength = oldSimpleData[key].length; i < oldLength; i++) { + oldGroups.push(oldSimpleData[key][i].group); + } + + newSimpleData[key] = newSimpleData[key] || []; + for (var j = 0, newLength = newSimpleData[key].length; j < newLength; j++) { + var isMultiValue = newSimpleData[key][j].hasOwnProperty('values'); + var value = valueReader(newSimpleData[key][j], isMultiValue); + + if (oldGroups.indexOf(newSimpleData[key][j].group) === -1) { + var property = new ICAL.Property(key); + simpleReader._setProperty(property, value, isMultiValue); + simpleReader._readParameters(property, newSimpleData[key][j], parameters); + vevent.addProperty(property); + } else { + oldGroups.splice(oldGroups.indexOf(newSimpleData[key][j].group), 1); + + properties = vevent.getAllProperties(key); + for (pKey in properties) { + if (!properties.hasOwnProperty(pKey)) { + continue; + } + + groupId = properties[pKey].getParameter('x-oc-group-id'); + if (groupId === null) { + continue; + } + if (parseInt(groupId) === newSimpleData[key][j].group) { + simpleReader._setProperty(properties[pKey], value, isMultiValue); + simpleReader._readParameters(properties[pKey], newSimpleData[key][j], parameters); + } + } + } + } + + properties = vevent.getAllProperties(key); + for (pKey in properties) { + if (!properties.hasOwnProperty(pKey)) { + continue; + } + + groupId = properties[pKey].getParameter('x-oc-group-id'); + if (oldGroups.indexOf(parseInt(groupId)) !== -1) { + vevent.removeProperty(properties[pKey]); + } + } + }, + _readParameters: function _readParameters(prop, simple, para) { + if (!para) { + return; + } + if (!simple.parameters) { + return; + } + + for (var i = 0, l = para.length; i < l; i++) { + if (simple.parameters[para[i]]) { + prop.setParameter(para[i], simple.parameters[para[i]]); + } else { + prop.removeParameter(simple.parameters[para[i]]); + } + } + }, + _setProperty: function _setProperty(prop, value, isMultiValue) { + if (isMultiValue) { + prop.setValues(value); + } else { + prop.setValue(value); + } + } + }; + + /** + * properties supported by event editor + */ + var simpleProperties = { + //General + 'summary': { parser: simpleParser.string, reader: simpleReader.string }, + 'location': { parser: simpleParser.string, reader: simpleReader.string }, + //'created': {parser: simpleParser.date, reader: simpleReader.date}, + //'last-modified': {parser: simpleParser.date, reader: simpleReader.date}, + //'categories': {parser: simpleParser.strings, reader: simpleReader.strings}, + //attendees + 'attendee': { parser: simpleParser.strings, reader: simpleReader.strings, parameters: attendeeParameters }, + 'organizer': { parser: simpleParser.string, reader: simpleReader.string, parameters: organizerParameters }, + //sharing + 'class': { parser: simpleParser.string, reader: simpleReader.string }, + //other + 'description': { parser: simpleParser.string, reader: simpleReader.string }, + //'url': {parser: simpleParser.string, reader: simpleReader.string}, + 'status': { parser: simpleParser.string, reader: simpleReader.string } + //'resources': {parser: simpleParser.strings, reader: simpleReader.strings} + }; + + /** + * specific parsers that check only one property + */ + var specificParser = { + alarm: function alarm(data, vevent) { + data.alarm = data.alarm || []; + + var alarms = vevent.getAllSubcomponents('valarm'), + group = 0; + for (var key in alarms) { + if (!alarms.hasOwnProperty(key)) { + continue; + } + + var alarm = alarms[key]; + var alarmData = { + group: group, + action: {}, + trigger: {}, + repeat: {}, + duration: {}, + attendee: [] + }; + + simpleParser.string(alarmData, alarm, 'action'); + simpleParser.date(alarmData, alarm, 'trigger'); + simpleParser.string(alarmData, alarm, 'repeat'); + simpleParser.string(alarmData, alarm, 'duration'); + //simpleParser.strings(alarmData, alarm, 'attendee', attendeeParameters); + + if (alarmData.trigger.type === 'duration' && alarm.hasProperty('trigger')) { + var trigger = alarm.getFirstProperty('trigger'); + var related = trigger.getParameter('related'); + if (related) { + alarmData.trigger.related = related; + } else { + alarmData.trigger.related = 'start'; + } + } + + data.alarm.push(alarmData); + + alarm.getFirstProperty('action').setParameter('x-oc-group-id', group.toString()); + group++; + } + }, + date: function date(data, vevent) { + var dtstart = vevent.getFirstPropertyValue('dtstart'), + dtend; + if (vevent.hasProperty('dtend')) { + dtend = vevent.getFirstPropertyValue('dtend'); + } else if (vevent.hasProperty('duration')) { + dtend = dtstart.clone(); + dtend.addDuration(vevent.getFirstPropertyValue('duration')); + } else { + dtend = dtstart.clone(); + } + + data.dtstart = { + parameters: { + zone: dtstart.zone.toString() + }, + type: dtstart.icaltype, + value: moment({ years: dtstart.year, months: dtstart.month - 1, date: dtstart.day, + hours: dtstart.hour, minutes: dtstart.minute, seconds: dtstart.seconds }) + }; + data.dtend = { + parameters: { + zone: dtend.zone.toString() + }, + type: dtend.icaltype, + value: moment({ years: dtend.year, months: dtend.month - 1, date: dtend.day, + hours: dtend.hour, minutes: dtend.minute, seconds: dtend.seconds }) + }; + data.allDay = dtstart.icaltype === 'date' && dtend.icaltype === 'date'; + }, + repeating: function repeating(data, vevent) { + var iCalEvent = new ICAL.Event(vevent); + + data.repeating = iCalEvent.isRecurring(); + + var rrule = vevent.getFirstPropertyValue('rrule'); + if (rrule) { + data.rrule = { + count: rrule.count, + freq: rrule.freq, + interval: rrule.interval, + parameters: rrule.parts, + until: null + }; + + /*if (rrule.until) { + simpleParser.date(data.rrule, rrule, 'until'); + }*/ + } else { + data.rrule = { + freq: 'NONE' + }; + } + } + }; + + var specificReader = { + alarm: function alarm(vevent, oldSimpleData, newSimpleData) { + var oldGroups = [], + components = null, + cKey = null, + groupId, + key = 'alarm'; + + oldSimpleData[key] = oldSimpleData[key] || []; + for (var i = 0, oldLength = oldSimpleData[key].length; i < oldLength; i++) { + oldGroups.push(oldSimpleData[key][i].group); + } + + newSimpleData[key] = newSimpleData[key] || []; + for (var j = 0, newLength = newSimpleData[key].length; j < newLength; j++) { + var valarm; + if (oldGroups.indexOf(newSimpleData[key][j].group) === -1) { + valarm = new ICAL.Component('VALARM'); + vevent.addSubcomponent(valarm); + } else { + oldGroups.splice(oldGroups.indexOf(newSimpleData[key][j].group), 1); + + components = vevent.getAllSubcomponents('valarm'); + for (cKey in components) { + if (!components.hasOwnProperty(cKey)) { + continue; + } + + groupId = components[cKey].getFirstProperty('action').getParameter('x-oc-group-id'); + if (groupId === null) { + continue; + } + if (groupId === newSimpleData[key][j].group.toString()) { + valarm = components[cKey]; + } + } + } + + simpleReader.string(valarm, {}, newSimpleData[key][j], 'action', []); + simpleReader.date(valarm, {}, newSimpleData[key][j], 'trigger', []); + simpleReader.string(valarm, {}, newSimpleData[key][j], 'repeat', []); + simpleReader.string(valarm, {}, newSimpleData[key][j], 'duration', []); + simpleReader.strings(valarm, {}, newSimpleData[key][j], 'attendee', attendeeParameters); + } + }, + date: function date(vevent, oldSimpleData, newSimpleData) { + vevent.removeAllProperties('dtstart'); + vevent.removeAllProperties('dtend'); + vevent.removeAllProperties('duration'); + + newSimpleData.dtstart.parameters.zone = newSimpleData.dtstart.parameters.zone || 'floating'; + newSimpleData.dtend.parameters.zone = newSimpleData.dtend.parameters.zone || 'floating'; + + if (newSimpleData.dtstart.parameters.zone !== 'floating' && !ICAL.TimezoneService.has(newSimpleData.dtstart.parameters.zone)) { + throw { + kind: 'timezone_missing', + missing_timezone: newSimpleData.dtstart.parameters.zone + }; + } + if (newSimpleData.dtend.parameters.zone !== 'floating' && !ICAL.TimezoneService.has(newSimpleData.dtend.parameters.zone)) { + throw { + kind: 'timezone_missing', + missing_timezone: newSimpleData.dtend.parameters.zone + }; + } + + var start = ICAL.Time.fromJSDate(newSimpleData.dtstart.value.toDate(), false); + start.isDate = newSimpleData.allDay; + var end = ICAL.Time.fromJSDate(newSimpleData.dtend.value.toDate(), false); + end.isDate = newSimpleData.allDay; + + var availableTimezones = []; + var vtimezones = vevent.parent.getAllSubcomponents('vtimezone'); + angular.forEach(vtimezones, function (vtimezone) { + availableTimezones.push(vtimezone.getFirstPropertyValue('tzid')); + }); + + var dtstart = new ICAL.Property('dtstart', vevent); + dtstart.setValue(start); + if (newSimpleData.dtstart.parameters.zone !== 'floating') { + dtstart.setParameter('tzid', newSimpleData.dtstart.parameters.zone); + var startTz = ICAL.TimezoneService.get(newSimpleData.dtstart.parameters.zone); + start.zone = startTz; + if (availableTimezones.indexOf(newSimpleData.dtstart.parameters.zone) === -1) { + vevent.parent.addSubcomponent(startTz.component); + availableTimezones.push(newSimpleData.dtstart.parameters.zone); + } + } + + var dtend = new ICAL.Property('dtend', vevent); + dtend.setValue(end); + if (newSimpleData.dtend.parameters.zone !== 'floating') { + dtend.setParameter('tzid', newSimpleData.dtend.parameters.zone); + var endTz = ICAL.TimezoneService.get(newSimpleData.dtend.parameters.zone); + end.zone = endTz; + if (availableTimezones.indexOf(newSimpleData.dtend.parameters.zone) === -1) { + vevent.parent.addSubcomponent(endTz.component); + } + } + + vevent.addProperty(dtstart); + vevent.addProperty(dtend); + }, + repeating: function repeating(vevent, oldSimpleData, newSimpleData) { + // We won't support exrule, because it's deprecated and barely used in the wild + if (newSimpleData.rrule === null || newSimpleData.rrule.freq === 'NONE') { + vevent.removeAllProperties('rdate'); + vevent.removeAllProperties('rrule'); + vevent.removeAllProperties('exdate'); + + return; + } + + if (newSimpleData.rrule.dontTouch) { + return; + } + + var params = { + interval: newSimpleData.rrule.interval, + freq: newSimpleData.rrule.freq + }; + + if (newSimpleData.rrule.count) { + params.count = newSimpleData.rrule.count; + } + + var rrule = new ICAL.Recur(params); + vevent.updatePropertyWithValue('rrule', rrule); + } + }; + + function SimpleEvent(event) { + this._event = event; + angular.extend(this, defaults); + + var parser, parameters; + for (var key in simpleProperties) { + if (!simpleProperties.hasOwnProperty(key)) { + continue; + } + + parser = simpleProperties[key].parser; + parameters = simpleProperties[key].parameters; + if (this._event.hasProperty(key)) { + parser(this, this._event, key, parameters); + } + } + + for (parser in specificParser) { + if (!specificParser.hasOwnProperty(parser)) { + continue; + } + + specificParser[parser](this, this._event); + } + + this._generateOldProperties(); + } + + SimpleEvent.prototype = { + _generateOldProperties: function _generateOldProperties() { + this._oldProperties = {}; + + for (var def in defaults) { + if (!defaults.hasOwnProperty(def)) { + continue; + } + + this._oldProperties[def] = angular.copy(this[def]); + } + }, + patch: function patch() { + var key, reader, parameters; + + for (key in simpleProperties) { + if (!simpleProperties.hasOwnProperty(key)) { + continue; + } + + reader = simpleProperties[key].reader; + parameters = simpleProperties[key].parameters; + if (this._oldProperties[key] !== this[key]) { + if (this[key] === null) { + this._event.removeAllProperties(key); + } else { + reader(this._event, this._oldProperties, this, key, parameters); + } + } + } + + for (key in specificReader) { + if (!specificReader.hasOwnProperty(key)) { + continue; + } + + reader = specificReader[key]; + reader(this._event, this._oldProperties, this); + } + + this._generateOldProperties(); + } + }; + + return SimpleEvent; +}); + +app.factory('SplittedICal', function () { + 'use strict'; + + function SplittedICal(name, color) { + var context = { + name: name, + color: color, + vevents: [], + vjournals: [], + vtodos: [] + }; + var iface = { + _isASplittedICalObject: true + }; + + Object.defineProperties(iface, { + name: { + get: function get() { + return context.name; + } + }, + color: { + get: function get() { + return context.color; + } + }, + vevents: { + get: function get() { + return context.vevents; + } + }, + vjournals: { + get: function get() { + return context.vjournals; + } + }, + vtodos: { + get: function get() { + return context.vtodos; + } + }, + objects: { + get: function get() { + return [].concat(context.vevents).concat(context.vjournals).concat(context.vtodos); + } + } + }); + + iface.addObject = function (componentName, object) { + switch (componentName) { + case 'vevent': + context.vevents.push(object); + break; + + case 'vjournal': + context.vjournals.push(object); + break; + + case 'vtodo': + context.vtodos.push(object); + break; + + default: + break; + } + }; + + return iface; + } + + SplittedICal.isSplittedICal = function (obj) { + return obj instanceof SplittedICal || (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj !== null && obj._isASplittedICalObject !== null; + }; + + return SplittedICal; +}); + +app.factory('Timezone', function () { + 'use strict'; + + var timezone = function Timezone(data) { + angular.extend(this, { + _props: {} + }); + + if (data instanceof ICAL.Timezone) { + this._props.jCal = data; + this._props.name = data.tzid; + } else if (typeof data === 'string') { + var jCal = ICAL.parse(data); + var components = new ICAL.Component(jCal); + var iCalTimezone = null; + if (components.name === 'vtimezone') { + iCalTimezone = new ICAL.Timezone(components); + } else { + iCalTimezone = new ICAL.Timezone(components.getFirstSubcomponent('vtimezone')); + } + this._props.jCal = iCalTimezone; + this._props.name = iCalTimezone.tzid; + } + }; + + //Timezones are immutable + timezone.prototype = { + get jCal() { + return this._props.jCal; + }, + get name() { + return this._props.name; + } + }; + + return timezone; +}); + +app.factory('VEvent', ["FcEvent", "SimpleEvent", "ICalFactory", "RandomStringService", function (FcEvent, SimpleEvent, ICalFactory, RandomStringService) { + 'use strict'; + + /** + * get DTEND from vevent + * @param {ICAL.Component} vevent + * @returns {ICAL.Time} + */ + + function calculateDTEnd(vevent) { + if (vevent.hasProperty('dtend')) { + return vevent.getFirstPropertyValue('dtend'); + } else if (vevent.hasProperty('duration')) { + var dtstart = vevent.getFirstPropertyValue('dtstart').clone(); + dtstart.addDuration(vevent.getFirstPropertyValue('duration')); + return dtstart; + } else { + return vevent.getFirstPropertyValue('dtstart').clone(); + } + } + + /** + * check if we need to convert the timezone of either dtstart or dtend + * @param {ICAL.Time} dt + * @returns {boolean} + */ + function isTimezoneConversionNecessary(dt) { + return dt.icaltype !== 'date' && dt.zone !== ICAL.Timezone.utcTimezone && dt.zone !== ICAL.Timezone.localTimezone; + } + + /** + * convert a dt's timezone if necessary + * @param {ICAL.Time} dt + * @param {ICAL.Component} timezone + * @returns {ICAL.Time} + */ + function convertTimezoneIfNecessary(dt, timezone) { + if (isTimezoneConversionNecessary(dt) && timezone) { + dt = dt.convertToZone(timezone); + } + + return dt; + } + + /** + * parse an recurring event + * @param vevent + * @param event + * @param start + * @param end + * @param timezone + * @return [] + */ + function getTimeForRecurring(vevent, event, start, end, timezone) { + var dtstart = event.getFirstPropertyValue('dtstart'); + var dtend = calculateDTEnd(event); + var duration = dtend.subtractDate(dtstart); + var fcEvents = []; + + var iterator = new ICAL.RecurExpansion({ + component: event, + dtstart: dtstart + }); + + var next; + while (next = iterator.next()) { + if (next.compare(start) < 0) { + continue; + } + if (next.compare(end) > 0) { + break; + } + + var singleDtStart = next.clone(); + var singleDtEnd = next.clone(); + singleDtEnd.addDuration(duration); + + fcEvents.push(new FcEvent(vevent, event, convertTimezoneIfNecessary(singleDtStart, timezone), convertTimezoneIfNecessary(singleDtEnd, timezone))); + } + + return fcEvents; + } + + /** + * parse a single event + * @param vevent + * @param event + * @param timezone + * @returns {FcEvent} + */ + function getTime(vevent, event, timezone) { + var dtstart = event.getFirstPropertyValue('dtstart'); + var dtend = calculateDTEnd(event); + + return new FcEvent(vevent, event, convertTimezoneIfNecessary(dtstart, timezone), convertTimezoneIfNecessary(dtend, timezone)); + } + + /** + * register timezones from ical response + * @param {ICAL.Component} components + */ + function registerTimezones(components) { + var vtimezones = components.getAllSubcomponents('vtimezone'); + angular.forEach(vtimezones, function (vtimezone) { + var timezone = new ICAL.Timezone(vtimezone); + ICAL.TimezoneService.register(timezone.tzid, timezone); + }); + } + + /** + * @constructor + * @param {Calendar} calendar + * @param {string|ICAL.Component} ical + * @param {string|null} etag + * @param {string|null} uri + * @constructor + */ + function VEvent(calendar, ical, etag, uri) { + if (typeof ical === 'string') { + try { + var jcal = ICAL.parse(ical); + this.comp = new ICAL.Component(jcal); + } catch (e) { + console.log(e); + throw VEvent.INVALID; + } + } else if (Object.getPrototypeOf(ical) === ICAL.Component.prototype) { + this.comp = ical; + } + + if (!this.comp || this.comp.jCal.length === 0) { + throw VEvent.INVALID; + } + + registerTimezones(this.comp); + + angular.extend(this, { + calendar: calendar, + etag: etag, + uri: uri + }); + } + + VEvent.prototype = { + /** + * serialize jsical object to actual ical data + * @returns {String} + */ + get data() { + return this.comp.toString(); + }, + /** + * + * @param start + * @param end + * @param timezone + * @returns {Array} + */ + getFcEvent: function getFcEvent(start, end, timezone) { + var iCalStart = ICAL.Time.fromJSDate(start.toDate()); + var iCalEnd = ICAL.Time.fromJSDate(end.toDate()); + var renderedEvents = [], + self = this; + + var vevents = this.comp.getAllSubcomponents('vevent'); + angular.forEach(vevents, function (event) { + var iCalEvent = new ICAL.Event(event); + + if (!event.hasProperty('dtstart')) { + return; + } + + if (iCalEvent.isRecurring()) { + angular.extend(renderedEvents, getTimeForRecurring(self, event, iCalStart, iCalEnd, timezone.jCal)); + } else { + renderedEvents.push(getTime(self, event, timezone.jCal)); + } + }); + + return renderedEvents; + }, + /** + * + * @param recurrenceId + * @returns {SimpleEvent} + */ + getSimpleEvent: function getSimpleEvent(recurrenceId) { + var vevents = this.comp.getAllSubcomponents('vevent'), + simpleEvent = null; + + angular.forEach(vevents, function (event) { + var hasRecurrenceId = event.hasProperty('recurrence-id'); + if (!hasRecurrenceId && recurrenceId === null || hasRecurrenceId && recurrenceId === event.getFirstPropertyValue('recurrence-id')) { + simpleEvent = new SimpleEvent(event); + } + }); + + return simpleEvent; + } + }; + + /** + * + * @param start + * @param end + * @param timezone + * @returns {VEvent} + */ + VEvent.fromStartEnd = function (start, end, timezone) { + var comp = ICalFactory.new(); + + var iCalEvent = new ICAL.Component('vevent'); + comp.addSubcomponent(iCalEvent); + iCalEvent.updatePropertyWithValue('created', ICAL.Time.now()); + iCalEvent.updatePropertyWithValue('dtstamp', ICAL.Time.now()); + iCalEvent.updatePropertyWithValue('last-modified', ICAL.Time.now()); + iCalEvent.updatePropertyWithValue('uid', RandomStringService.generate()); + // add a dummy dtstart to make the ical valid before we create SimpleEvent + iCalEvent.updatePropertyWithValue('dtstart', ICAL.Time.now()); + + var uri = RandomStringService.generate(); + uri += RandomStringService.generate(); + uri += '.ics'; + + var vevent = new VEvent(null, comp, null, uri); + var simple = new SimpleEvent(iCalEvent); + angular.extend(simple, { + allDay: !start.hasTime() && !end.hasTime(), + dtstart: { + type: start.hasTime() ? 'datetime' : 'date', + value: start, + parameters: { + zone: timezone + } + }, + dtend: { + type: end.hasTime() ? 'datetime' : 'date', + value: end, + parameters: { + zone: timezone + } + } + }); + simple.patch(); + + return vevent; + }; + + /** + * + * @type {string} + */ + VEvent.INVALID = 'INVALID_EVENT'; + + return VEvent; +}]); + +app.service('AutoCompletionService', ['$rootScope', '$http', function ($rootScope, $http) { + 'use strict'; + + this.searchAttendee = function (name) { + return $http.get($rootScope.baseUrl + 'autocompletion/attendee', { + params: { + search: name + } + }).then(function (response) { + return response.data; + }); + }; + + this.searchLocation = function (address) { + return $http.get($rootScope.baseUrl + 'autocompletion/location', { + params: { + location: address + } + }).then(function (response) { + return response.data; + }); + }; +}]); + +app.service('CalendarService', ['DavClient', 'Calendar', function (DavClient, Calendar) { + 'use strict'; + + var self = this; + + this._CALENDAR_HOME = null; + + this._currentUserPrincipal = null; + + this._takenUrls = []; + + this._PROPERTIES = ['{' + DavClient.NS_DAV + '}displayname', '{' + DavClient.NS_IETF + '}calendar-description', '{' + DavClient.NS_IETF + '}calendar-timezone', '{' + DavClient.NS_APPLE + '}calendar-order', '{' + DavClient.NS_APPLE + '}calendar-color', '{' + DavClient.NS_IETF + '}supported-calendar-component-set', '{' + DavClient.NS_OWNCLOUD + '}calendar-enabled', '{' + DavClient.NS_DAV + '}acl', '{' + DavClient.NS_DAV + '}owner', '{' + DavClient.NS_OWNCLOUD + '}invite']; + + this._xmls = new XMLSerializer(); + + function discoverHome(callback) { + return DavClient.propFind(DavClient.buildUrl(OC.linkToRemoteBase('dav')), ['{' + DavClient.NS_DAV + '}current-user-principal'], 0, { 'requesttoken': OC.requestToken }).then(function (response) { + if (!DavClient.wasRequestSuccessful(response.status)) { + throw "CalDAV client could not be initialized - Querying current-user-principal failed"; + } + + if (response.body.propStat.length < 1) { + return; + } + var props = response.body.propStat[0].properties; + self._currentUserPrincipal = props['{' + DavClient.NS_DAV + '}current-user-principal'][0].textContent; + + return DavClient.propFind(DavClient.buildUrl(self._currentUserPrincipal), ['{' + DavClient.NS_IETF + '}calendar-home-set'], 0, { 'requesttoken': OC.requestToken }).then(function (response) { + if (!DavClient.wasRequestSuccessful(response.status)) { + throw "CalDAV client could not be initialized - Querying calendar-home-set failed"; + } + + if (response.body.propStat.length < 1) { + return; + } + var props = response.body.propStat[0].properties; + self._CALENDAR_HOME = props['{' + DavClient.NS_IETF + '}calendar-home-set'][0].textContent; + + return callback(); + }); + }); + } + + function getResponseCodeFromHTTPResponse(t) { + return parseInt(t.split(' ')[1]); + } + + this.getAll = function () { + if (this._CALENDAR_HOME === null) { + return discoverHome(function () { + return self.getAll(); + }); + } + + return DavClient.propFind(DavClient.buildUrl(this._CALENDAR_HOME), this._PROPERTIES, 1, { 'requesttoken': OC.requestToken }).then(function (response) { + var calendars = []; + + if (!DavClient.wasRequestSuccessful(response.status)) { + throw "CalDAV client could not be initialized - Querying calendars failed"; + } + + for (var i = 0; i < response.body.length; i++) { + var body = response.body[i]; + if (body.propStat.length < 1) { + continue; + } + + self._takenUrls.push(body.href); + + var responseCode = getResponseCodeFromHTTPResponse(body.propStat[0].status); + if (!DavClient.wasRequestSuccessful(responseCode)) { + continue; + } + + var props = self._getSimplePropertiesFromRequest(body.propStat[0].properties); + if (!props || !props.components.vevent) { + continue; + } + + var calendar = Calendar(body.href, props); + calendars.push(calendar); + } + + return calendars; + }); + }; + + this.get = function (url) { + if (this._CALENDAR_HOME === null) { + return discoverHome(function () { + return self.get(url); + }); + } + + return DavClient.propFind(DavClient.buildUrl(url), this._PROPERTIES, 0, { 'requesttoken': OC.requestToken }).then(function (response) { + var body = response.body; + if (body.propStat.length < 1) { + //TODO - something went wrong + return; + } + + var responseCode = getResponseCodeFromHTTPResponse(body.propStat[0].status); + if (!DavClient.wasRequestSuccessful(responseCode)) { + //TODO - something went wrong + return; + } + + var props = self._getSimplePropertiesFromRequest(body.propStat[0].properties); + if (!props || !props.components.vevent) { + return; + } + + return Calendar(body.href, props); + }); + }; + + this.create = function (name, color, components) { + if (this._CALENDAR_HOME === null) { + return discoverHome(function () { + return self.create(name, color); + }); + } + + if (typeof components === 'undefined') { + components = ['vevent', 'vtodo']; + } + + var xmlDoc = document.implementation.createDocument('', '', null); + var cMkcalendar = xmlDoc.createElement('d:mkcol'); + cMkcalendar.setAttribute('xmlns:c', 'urn:ietf:params:xml:ns:caldav'); + cMkcalendar.setAttribute('xmlns:d', 'DAV:'); + cMkcalendar.setAttribute('xmlns:a', 'http://apple.com/ns/ical/'); + cMkcalendar.setAttribute('xmlns:o', 'http://owncloud.org/ns'); + xmlDoc.appendChild(cMkcalendar); + + var dSet = xmlDoc.createElement('d:set'); + cMkcalendar.appendChild(dSet); + + var dProp = xmlDoc.createElement('d:prop'); + dSet.appendChild(dProp); + + var dResourceType = xmlDoc.createElement('d:resourcetype'); + dProp.appendChild(dResourceType); + + var dCollection = xmlDoc.createElement('d:collection'); + dResourceType.appendChild(dCollection); + + var cCalendar = xmlDoc.createElement('c:calendar'); + dResourceType.appendChild(cCalendar); + + dProp.appendChild(this._createXMLForProperty(xmlDoc, 'displayname', name)); + dProp.appendChild(this._createXMLForProperty(xmlDoc, 'enabled', true)); + dProp.appendChild(this._createXMLForProperty(xmlDoc, 'color', color)); + dProp.appendChild(this._createXMLForProperty(xmlDoc, 'components', components)); + + var body = this._xmls.serializeToString(cMkcalendar); + + var uri = this._suggestUri(name); + var url = this._CALENDAR_HOME + uri + '/'; + var headers = { + 'Content-Type': 'application/xml; charset=utf-8', + 'requesttoken': OC.requestToken + }; + + return DavClient.request('MKCOL', url, headers, body).then(function (response) { + if (response.status === 201) { + self._takenUrls.push(url); + return self.get(url).then(function (calendar) { + calendar.enabled = true; + return self.update(calendar); + }); + } + }); + }; + + this.update = function (calendar) { + var xmlDoc = document.implementation.createDocument('', '', null); + var dPropUpdate = xmlDoc.createElement('d:propertyupdate'); + dPropUpdate.setAttribute('xmlns:c', 'urn:ietf:params:xml:ns:caldav'); + dPropUpdate.setAttribute('xmlns:d', 'DAV:'); + dPropUpdate.setAttribute('xmlns:a', 'http://apple.com/ns/ical/'); + dPropUpdate.setAttribute('xmlns:o', 'http://owncloud.org/ns'); + xmlDoc.appendChild(dPropUpdate); + + var dSet = xmlDoc.createElement('d:set'); + dPropUpdate.appendChild(dSet); + + var dProp = xmlDoc.createElement('d:prop'); + dSet.appendChild(dProp); + + var updatedProperties = calendar.getUpdated(); + if (updatedProperties.length === 0) { + //nothing to do here + return calendar; + } + for (var i = 0; i < updatedProperties.length; i++) { + dProp.appendChild(this._createXMLForProperty(xmlDoc, updatedProperties[i], calendar[updatedProperties[i]])); + } + + calendar.resetUpdated(); + + var url = calendar.url; + var body = this._xmls.serializeToString(dPropUpdate); + var headers = { + 'Content-Type': 'application/xml; charset=utf-8', + 'requesttoken': OC.requestToken + }; + + return DavClient.request('PROPPATCH', url, headers, body).then(function (response) { + return calendar; + }); + }; + + this.delete = function (calendar) { + return DavClient.request('DELETE', calendar.url, { 'requesttoken': OC.requestToken }, '').then(function (response) { + if (response.status === 204) { + return true; + } else { + // TODO - handle error case + return false; + } + }); + }; + + this.share = function (calendar, shareType, shareWith, writable, existingShare) { + var xmlDoc = document.implementation.createDocument('', '', null); + var oShare = xmlDoc.createElement('o:share'); + oShare.setAttribute('xmlns:d', 'DAV:'); + oShare.setAttribute('xmlns:o', 'http://owncloud.org/ns'); + xmlDoc.appendChild(oShare); + + var oSet = xmlDoc.createElement('o:set'); + oShare.appendChild(oSet); + + var dHref = xmlDoc.createElement('d:href'); + if (shareType === OC.Share.SHARE_TYPE_USER) { + dHref.textContent = 'principal:principals/users/'; + } else if (shareType === OC.Share.SHARE_TYPE_GROUP) { + dHref.textContent = 'principal:principals/groups/'; + } + dHref.textContent += shareWith; + oSet.appendChild(dHref); + + var oSummary = xmlDoc.createElement('o:summary'); + oSummary.textContent = t('calendar', '{calendar} shared by {owner}', { + calendar: calendar.displayname, + owner: calendar.owner + }); + oSet.appendChild(oSummary); + + if (writable) { + var oRW = xmlDoc.createElement('o:read-write'); + oSet.appendChild(oRW); + } + + var headers = { + 'Content-Type': 'application/xml; charset=utf-8', + requesttoken: oc_requesttoken + }; + var body = this._xmls.serializeToString(oShare); + return DavClient.request('POST', calendar.url, headers, body).then(function (response) { + if (response.status === 200) { + if (!existingShare) { + if (shareType === OC.Share.SHARE_TYPE_USER) { + calendar.shares.users.push({ + id: shareWith, + displayname: shareWith, + writable: writable + }); + } else if (shareType === OC.Share.SHARE_TYPE_GROUP) { + calendar.shares.groups.push({ + id: shareWith, + displayname: shareWith, + writable: writable + }); + } + } + } + }); + }; + + this.unshare = function (calendar, shareType, shareWith) { + var xmlDoc = document.implementation.createDocument('', '', null); + var oShare = xmlDoc.createElement('o:share'); + oShare.setAttribute('xmlns:d', 'DAV:'); + oShare.setAttribute('xmlns:o', 'http://owncloud.org/ns'); + xmlDoc.appendChild(oShare); + + var oRemove = xmlDoc.createElement('o:remove'); + oShare.appendChild(oRemove); + + var dHref = xmlDoc.createElement('d:href'); + if (shareType === OC.Share.SHARE_TYPE_USER) { + dHref.textContent = 'principal:principals/users/'; + } else if (shareType === OC.Share.SHARE_TYPE_GROUP) { + dHref.textContent = 'principal:principals/groups/'; + } + dHref.textContent += shareWith; + oRemove.appendChild(dHref); + + var headers = { + 'Content-Type': 'application/xml; charset=utf-8', + requesttoken: oc_requesttoken + }; + var body = this._xmls.serializeToString(oShare); + return DavClient.request('POST', calendar.url, headers, body).then(function (response) { + if (response.status === 200) { + if (shareType === OC.Share.SHARE_TYPE_USER) { + calendar.shares.users = calendar.shares.users.filter(function (user) { + return user.id !== shareWith; + }); + } else if (shareType === OC.Share.SHARE_TYPE_GROUP) { + calendar.shares.groups = calendar.shares.groups.filter(function (groups) { + return groups.id !== shareWith; + }); + } + //todo - remove entry from calendar object + return true; + } else { + return false; + } + }); + }; + + this._createXMLForProperty = function (xmlDoc, propName, value) { + switch (propName) { + case 'enabled': + var oEnabled = xmlDoc.createElement('o:calendar-enabled'); + oEnabled.textContent = value ? '1' : '0'; + return oEnabled; + + case 'displayname': + var dDisplayname = xmlDoc.createElement('d:displayname'); + dDisplayname.textContent = value; + return dDisplayname; + + case 'order': + var aOrder = xmlDoc.createElement('a:calendar-order'); + aOrder.textContent = value; + return aOrder; + + case 'color': + var aColor = xmlDoc.createElement('a:calendar-color'); + aColor.textContent = value; + return aColor; + + case 'components': + var cComponents = xmlDoc.createElement('c:supported-calendar-component-set'); + for (var i = 0; i < value.length; i++) { + var cComp = xmlDoc.createElement('c:comp'); + cComp.setAttribute('name', value[i].toUpperCase()); + cComponents.appendChild(cComp); + } + return cComponents; + } + }; + + this._getSimplePropertiesFromRequest = function (props) { + if (!props['{' + DavClient.NS_IETF + '}supported-calendar-component-set']) { + return; + } + + this._getACLFromResponse(props); + + var simple = { + displayname: props['{' + DavClient.NS_DAV + '}displayname'], + color: props['{' + DavClient.NS_APPLE + '}calendar-color'], + order: props['{' + DavClient.NS_APPLE + '}calendar-order'], + components: { + vevent: false, + vjournal: false, + vtodo: false + }, + owner: null, + shareable: props.canWrite, + shares: { + users: [], + groups: [] + }, + writable: props.canWrite + }; + + var components = props['{' + DavClient.NS_IETF + '}supported-calendar-component-set']; + for (var i = 0; i < components.length; i++) { + var name = components[i].attributes.getNamedItem('name').textContent.toLowerCase(); + if (simple.components.hasOwnProperty(name)) { + simple.components[name] = true; + } + } + + var owner = props['{' + DavClient.NS_DAV + '}owner']; + if (typeof owner !== 'undefined' && owner.length !== 0) { + owner = owner[0].textContent.slice(0, -1); + if (owner.indexOf('/remote.php/dav/principals/users/') !== -1) { + simple.owner = owner.substr(33 + owner.indexOf('/remote.php/dav/principals/users/')); + } + } + + var shares = props['{' + DavClient.NS_OWNCLOUD + '}invite']; + if (typeof shares !== 'undefined') { + for (var j = 0; j < shares.length; j++) { + var href = shares[j].getElementsByTagNameNS('DAV:', 'href'); + if (href.length === 0) { + continue; + } + href = href[0].textContent; + + var access = shares[j].getElementsByTagNameNS(DavClient.NS_OWNCLOUD, 'access'); + if (access.length === 0) { + continue; + } + access = access[0]; + + var readWrite = access.getElementsByTagNameNS(DavClient.NS_OWNCLOUD, 'read-write'); + readWrite = readWrite.length !== 0; + + if (href.startsWith('principal:principals/users/')) { + simple.shares.users.push({ + id: href.substr(27), + displayname: href.substr(27), + writable: readWrite + }); + } else if (href.startsWith('principal:principals/groups/')) { + simple.shares.groups.push({ + id: href.substr(28), + displayname: href.substr(28), + writable: readWrite + }); + } + } + } + + if (typeof props['{' + DavClient.NS_OWNCLOUD + '}calendar-enabled'] === 'undefined') { + if (typeof simple.owner !== 'undefined') { + simple.enabled = simple.owner === oc_current_user; + } else { + simple.enabled = false; + } + } else { + simple.enabled = props['{' + DavClient.NS_OWNCLOUD + '}calendar-enabled'] === '1'; + } + + if (typeof simple.color !== 'undefined' && simple.color.length !== 0) { + if (simple.color.length === 9) { + simple.color = simple.color.substr(0, 7); + } + } else { + simple.color = '#1d2d44'; + } + + simple.writableProperties = oc_current_user === simple.owner && simple.writable; + + return simple; + }; + + this._getACLFromResponse = function (props) { + var canWrite = false; + var acl = props['{' + DavClient.NS_DAV + '}acl']; + if (acl) { + for (var k = 0; k < acl.length; k++) { + var href = acl[k].getElementsByTagNameNS(DavClient.NS_DAV, 'href'); + if (href.length === 0) { + continue; + } + href = href[0].textContent; + if (href !== self._currentUserPrincipal) { + continue; + } + var writeNode = acl[k].getElementsByTagNameNS(DavClient.NS_DAV, 'write'); + if (writeNode.length > 0) { + canWrite = true; + } + } + } + props.canWrite = canWrite; + }; + + this._isUriAlreadyTaken = function (uri) { + return this._takenUrls.indexOf(this._CALENDAR_HOME + uri + '/') !== -1; + }; + + this._suggestUri = function (displayname) { + var uri = displayname.toString().toLowerCase().replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + + if (!this._isUriAlreadyTaken(uri)) { + return uri; + } + + if (uri.indexOf('-') === -1) { + uri = uri + '-1'; + if (!this._isUriAlreadyTaken(uri)) { + return uri; + } + } + + while (this._isUriAlreadyTaken(uri)) { + var positionLastDash = uri.lastIndexOf('-'); + var firstPart = uri.substr(0, positionLastDash); + var lastPart = uri.substr(positionLastDash + 1); + + if (lastPart.match(/^\d+$/)) { + lastPart = parseInt(lastPart); + lastPart++; + + uri = firstPart + '-' + lastPart; + } else if (lastPart === '') { + uri = uri + '1'; + } else { + uri = uri = '-1'; + } + } + + return uri; + }; +}]); + +app.service('DavClient', function () { + 'use strict'; + + var client = new dav.Client({ + baseUrl: OC.linkToRemote('dav/calendars'), + xmlNamespaces: { + 'DAV:': 'd', + 'urn:ietf:params:xml:ns:caldav': 'c', + 'http://apple.com/ns/ical/': 'aapl', + 'http://owncloud.org/ns': 'oc', + 'http://calendarserver.org/ns/': 'cs' + } + }); + + angular.extend(client, { + NS_DAV: 'DAV:', + NS_IETF: 'urn:ietf:params:xml:ns:caldav', + NS_APPLE: 'http://apple.com/ns/ical/', + NS_OWNCLOUD: 'http://owncloud.org/ns', + NS_CALENDARSERVER: 'http://calendarserver.org/ns/', + + buildUrl: function buildUrl(path) { + return window.location.protocol + '//' + window.location.host + path; + }, + wasRequestSuccessful: function wasRequestSuccessful(status) { + return status >= 200 && status <= 299; + } + }); + + return client; +}); + +app.service('EventsEditorDialogService', ["$uibModal", function ($uibModal) { + 'use strict'; + + var EDITOR_POPOVER = 'eventspopovereditor.html'; + var EDITOR_SIDEBAR = 'eventssidebareditor.html'; + var REPEAT_QUESTION = ''; //TODO in followup PR + + var self = this; + + self.calendar = null; + self.eventModal = null; + self.fcEvent = null; + self.promise = null; + self.scope = null; + self.simpleEvent = null; + self.unlockCallback = null; + + function cleanup() { + self.calendar = null; + self.eventModal = null; + self.fcEvent = null; + self.promise = null; + self.scope = null; + self.simpleEvent = null; + } + + function openDialog(template, position, rejectDialog, resolveDialog) { + self.eventModal = $uibModal.open({ + templateUrl: template, + controller: 'EditorController', + windowClass: template === EDITOR_POPOVER ? 'popover' : null, + appendTo: template === EDITOR_POPOVER ? angular.element('#popover-container') : angular.element('#app-content'), + resolve: { + vevent: function vevent() { + return self.fcEvent.vevent; + }, + simpleEvent: function simpleEvent() { + return self.simpleEvent; + }, + calendar: function calendar() { + return self.calendar; + }, + isNew: function isNew() { + return self.fcEvent.vevent.etag === null; + }, + emailAddress: function emailAddress() { + return angular.element('#fullcalendar').attr('data-emailAddress'); + } + }, + scope: self.scope + }); + + if (template === EDITOR_SIDEBAR) { + angular.element('#app-content').addClass('with-app-sidebar'); + } + + self.eventModal.rendered.then(function (result) { + angular.element('#popover-container').css('display', 'none'); + angular.forEach(position, function (v) { + angular.element('.modal').css(v.name, v.value); + }); + angular.element('#popover-container').css('display', 'block'); + }); + + self.eventModal.result.then(function (result) { + if (result.action === 'proceed') { + self.calendar = result.calendar; + openDialog(EDITOR_SIDEBAR, null, rejectDialog, resolveDialog); + } else { + if (template === EDITOR_SIDEBAR) { + angular.element('#app-content').removeClass('with-app-sidebar'); + } + + self.unlockCallback(); + resolveDialog({ + calendar: result.calendar, + vevent: result.vevent + }); + cleanup(); + } + }).catch(function (reason) { + if (template === EDITOR_SIDEBAR) { + angular.element('#app-content').removeClass('with-app-sidebar'); + } + + if (reason !== 'superseded') { + self.unlockCallback(); + cleanup(); + } + + rejectDialog(reason); + }); + } + + function openRepeatQuestion() { + //TODO in followup PR + } + + this.open = function (scope, fcEvent, positionCallback, lockCallback, unlockCallback) { + var skipPopover = angular.element('#fullcalendar').attr('data-skipPopover') === 'yes'; + + //don't reload editor for the same event + if (self.fcEvent === fcEvent) { + return self.promise; + } + + //is an editor already open? + if (self.fcEvent) { + self.eventModal.dismiss('superseded'); + } + + cleanup(); + self.promise = new Promise(function (resolve, reject) { + self.fcEvent = fcEvent; + self.simpleEvent = fcEvent.getSimpleEvent(); + if (fcEvent.vevent) { + self.calendar = fcEvent.vevent.calendar; + } + self.scope = scope; + + //calculate position of popover + var position = positionCallback(); + + //lock new fcEvent and unlock old fcEvent + lockCallback(); + if (self.unlockCallback) { + self.unlockCallback(); + } + self.unlockCallback = unlockCallback; + + //skip popover on small devices + if (angular.element(window).width() > 768 && !skipPopover) { + openDialog(EDITOR_POPOVER, position, reject, resolve); + } else { + openDialog(EDITOR_SIDEBAR, null, reject, resolve); + } + }); + + return self.promise; + }; + + return this; +}]); + +app.service('ICalFactory', function () { + 'use strict'; + + this.new = function () { + var root = new ICAL.Component(['vcalendar', [], []]); + + var version = angular.element('#fullcalendar').attr('data-appVersion'); + root.updatePropertyWithValue('prodid', '-//ownCloud calendar v' + version); + + root.updatePropertyWithValue('version', '2.0'); + root.updatePropertyWithValue('calscale', 'GREGORIAN'); + + return root; + }; +}); + +app.factory('is', function () { + 'use strict'; + + return { + loading: false + }; +}); + +app.factory('RandomStringService', function () { + 'use strict'; + + return { + generate: function generate() { + return Math.random().toString(36).substr(2); + } + }; +}); + +app.service('SettingsService', ['$rootScope', '$http', function ($rootScope, $http) { + 'use strict'; + + this.getView = function () { + return $http({ + method: 'GET', + url: $rootScope.baseUrl + 'config', + params: { key: 'view' } + }).then(function (response) { + return response.data.value; + }); + }; + + this.setView = function (view) { + return $http({ + method: 'POST', + url: $rootScope.baseUrl + 'config', + data: { + key: 'view', + value: view + } + }).then(function () { + return true; + }); + }; + + this.getSkipPopover = function () { + return $http({ + method: 'GET', + url: $rootScope.baseUrl + 'config', + params: { key: 'skipPopover' } + }).then(function (response) { + return response.data.value; + }); + }; + + this.setSkipPopover = function (value) { + return $http({ + method: 'POST', + url: $rootScope.baseUrl + 'config', + data: { + key: 'skipPopover', + value: value + } + }).then(function () { + return true; + }); + }; +}]); + +app.service('TimezoneListProvider', function () { + 'use strict'; + + return new Promise(function (resolve) { + resolve(['Africa\/Abidjan', 'Africa\/Accra', 'Africa\/Addis_Ababa', 'Africa\/Algiers', 'Africa\/Asmara', 'Africa\/Asmera', 'Africa\/Bamako', 'Africa\/Bangui', 'Africa\/Banjul', 'Africa\/Bissau', 'Africa\/Blantyre', 'Africa\/Brazzaville', 'Africa\/Bujumbura', 'Africa\/Cairo', 'Africa\/Casablanca', 'Africa\/Ceuta', 'Africa\/Conakry', 'Africa\/Dakar', 'Africa\/Dar_es_Salaam', 'Africa\/Djibouti', 'Africa\/Douala', 'Africa\/El_Aaiun', 'Africa\/Freetown', 'Africa\/Gaborone', 'Africa\/Harare', 'Africa\/Johannesburg', 'Africa\/Juba', 'Africa\/Kampala', 'Africa\/Khartoum', 'Africa\/Kigali', 'Africa\/Kinshasa', 'Africa\/Lagos', 'Africa\/Libreville', 'Africa\/Lome', 'Africa\/Luanda', 'Africa\/Lubumbashi', 'Africa\/Lusaka', 'Africa\/Malabo', 'Africa\/Maputo', 'Africa\/Maseru', 'Africa\/Mbabane', 'Africa\/Mogadishu', 'Africa\/Monrovia', 'Africa\/Nairobi', 'Africa\/Ndjamena', 'Africa\/Niamey', 'Africa\/Nouakchott', 'Africa\/Ouagadougou', 'Africa\/Porto-Novo', 'Africa\/Sao_Tome', 'Africa\/Timbuktu', 'Africa\/Tripoli', 'Africa\/Tunis', 'Africa\/Windhoek', 'America\/Adak', 'America\/Anchorage', 'America\/Anguilla', 'America\/Antigua', 'America\/Araguaina', 'America\/Argentina\/Buenos_Aires', 'America\/Argentina\/Catamarca', 'America\/Argentina\/ComodRivadavia', 'America\/Argentina\/Cordoba', 'America\/Argentina\/Jujuy', 'America\/Argentina\/La_Rioja', 'America\/Argentina\/Mendoza', 'America\/Argentina\/Rio_Gallegos', 'America\/Argentina\/Salta', 'America\/Argentina\/San_Juan', 'America\/Argentina\/San_Luis', 'America\/Argentina\/Tucuman', 'America\/Argentina\/Ushuaia', 'America\/Aruba', 'America\/Asuncion', 'America\/Atikokan', 'America\/Bahia', 'America\/Bahia_Banderas', 'America\/Barbados', 'America\/Belem', 'America\/Belize', 'America\/Blanc-Sablon', 'America\/Boa_Vista', 'America\/Bogota', 'America\/Boise', 'America\/Cambridge_Bay', 'America\/Campo_Grande', 'America\/Cancun', 'America\/Caracas', 'America\/Cayenne', 'America\/Cayman', 'America\/Chicago', 'America\/Chihuahua', 'America\/Costa_Rica', 'America\/Creston', 'America\/Cuiaba', 'America\/Curacao', 'America\/Danmarkshavn', 'America\/Dawson', 'America\/Dawson_Creek', 'America\/Denver', 'America\/Detroit', 'America\/Dominica', 'America\/Edmonton', 'America\/Eirunepe', 'America\/El_Salvador', 'America\/Fortaleza', 'America\/Glace_Bay', 'America\/Godthab', 'America\/Goose_Bay', 'America\/Grand_Turk', 'America\/Grenada', 'America\/Guadeloupe', 'America\/Guatemala', 'America\/Guayaquil', 'America\/Guyana', 'America\/Halifax', 'America\/Havana', 'America\/Hermosillo', 'America\/Indiana\/Indianapolis', 'America\/Indiana\/Knox', 'America\/Indiana\/Marengo', 'America\/Indiana\/Petersburg', 'America\/Indiana\/Tell_City', 'America\/Indiana\/Vevay', 'America\/Indiana\/Vincennes', 'America\/Indiana\/Winamac', 'America\/Inuvik', 'America\/Iqaluit', 'America\/Jamaica', 'America\/Juneau', 'America\/Kentucky\/Louisville', 'America\/Kentucky\/Monticello', 'America\/Kralendijk', 'America\/La_Paz', 'America\/Lima', 'America\/Los_Angeles', 'America\/Louisville', 'America\/Lower_Princes', 'America\/Maceio', 'America\/Managua', 'America\/Manaus', 'America\/Marigot', 'America\/Martinique', 'America\/Matamoros', 'America\/Mazatlan', 'America\/Menominee', 'America\/Merida', 'America\/Metlakatla', 'America\/Mexico_City', 'America\/Miquelon', 'America\/Moncton', 'America\/Monterrey', 'America\/Montevideo', 'America\/Montreal', 'America\/Montserrat', 'America\/Nassau', 'America\/New_York', 'America\/Nipigon', 'America\/Nome', 'America\/Noronha', 'America\/North_Dakota\/Beulah', 'America\/North_Dakota\/Center', 'America\/North_Dakota\/New_Salem', 'America\/Ojinaga', 'America\/Panama', 'America\/Pangnirtung', 'America\/Paramaribo', 'America\/Phoenix', 'America\/Port-au-Prince', 'America\/Port_of_Spain', 'America\/Porto_Velho', 'America\/Puerto_Rico', 'America\/Rainy_River', 'America\/Rankin_Inlet', 'America\/Recife', 'America\/Regina', 'America\/Resolute', 'America\/Rio_Branco', 'America\/Santa_Isabel', 'America\/Santarem', 'America\/Santiago', 'America\/Santo_Domingo', 'America\/Sao_Paulo', 'America\/Scoresbysund', 'America\/Shiprock', 'America\/Sitka', 'America\/St_Barthelemy', 'America\/St_Johns', 'America\/St_Kitts', 'America\/St_Lucia', 'America\/St_Thomas', 'America\/St_Vincent', 'America\/Swift_Current', 'America\/Tegucigalpa', 'America\/Thule', 'America\/Thunder_Bay', 'America\/Tijuana', 'America\/Toronto', 'America\/Tortola', 'America\/Vancouver', 'America\/Whitehorse', 'America\/Winnipeg', 'America\/Yakutat', 'America\/Yellowknife', 'Antarctica\/Casey', 'Antarctica\/Davis', 'Antarctica\/DumontDUrville', 'Antarctica\/Macquarie', 'Antarctica\/Mawson', 'Antarctica\/McMurdo', 'Antarctica\/Palmer', 'Antarctica\/Rothera', 'Antarctica\/South_Pole', 'Antarctica\/Syowa', 'Antarctica\/Vostok', 'Arctic\/Longyearbyen', 'Asia\/Aden', 'Asia\/Almaty', 'Asia\/Amman', 'Asia\/Anadyr', 'Asia\/Aqtau', 'Asia\/Aqtobe', 'Asia\/Ashgabat', 'Asia\/Baghdad', 'Asia\/Bahrain', 'Asia\/Baku', 'Asia\/Bangkok', 'Asia\/Beirut', 'Asia\/Bishkek', 'Asia\/Brunei', 'Asia\/Calcutta', 'Asia\/Choibalsan', 'Asia\/Chongqing', 'Asia\/Colombo', 'Asia\/Damascus', 'Asia\/Dhaka', 'Asia\/Dili', 'Asia\/Dubai', 'Asia\/Dushanbe', 'Asia\/Gaza', 'Asia\/Harbin', 'Asia\/Hebron', 'Asia\/Ho_Chi_Minh', 'Asia\/Hong_Kong', 'Asia\/Hovd', 'Asia\/Irkutsk', 'Asia\/Istanbul', 'Asia\/Jakarta', 'Asia\/Jayapura', 'Asia\/Jerusalem', 'Asia\/Kabul', 'Asia\/Kamchatka', 'Asia\/Karachi', 'Asia\/Kashgar', 'Asia\/Kathmandu', 'Asia\/Katmandu', 'Asia\/Khandyga', 'Asia\/Kolkata', 'Asia\/Krasnoyarsk', 'Asia\/Kuala_Lumpur', 'Asia\/Kuching', 'Asia\/Kuwait', 'Asia\/Macau', 'Asia\/Magadan', 'Asia\/Makassar', 'Asia\/Manila', 'Asia\/Muscat', 'Asia\/Nicosia', 'Asia\/Novokuznetsk', 'Asia\/Novosibirsk', 'Asia\/Omsk', 'Asia\/Oral', 'Asia\/Phnom_Penh', 'Asia\/Pontianak', 'Asia\/Pyongyang', 'Asia\/Qatar', 'Asia\/Qyzylorda', 'Asia\/Rangoon', 'Asia\/Riyadh', 'Asia\/Saigon', 'Asia\/Sakhalin', 'Asia\/Samarkand', 'Asia\/Seoul', 'Asia\/Shanghai', 'Asia\/Singapore', 'Asia\/Taipei', 'Asia\/Tashkent', 'Asia\/Tbilisi', 'Asia\/Tehran', 'Asia\/Thimphu', 'Asia\/Tokyo', 'Asia\/Ulaanbaatar', 'Asia\/Urumqi', 'Asia\/Ust-Nera', 'Asia\/Vientiane', 'Asia\/Vladivostok', 'Asia\/Yakutsk', 'Asia\/Yekaterinburg', 'Asia\/Yerevan', 'Atlantic\/Azores', 'Atlantic\/Bermuda', 'Atlantic\/Canary', 'Atlantic\/Cape_Verde', 'Atlantic\/Faeroe', 'Atlantic\/Faroe', 'Atlantic\/Jan_Mayen', 'Atlantic\/Madeira', 'Atlantic\/Reykjavik', 'Atlantic\/South_Georgia', 'Atlantic\/St_Helena', 'Atlantic\/Stanley', 'Australia\/Adelaide', 'Australia\/Brisbane', 'Australia\/Broken_Hill', 'Australia\/Currie', 'Australia\/Darwin', 'Australia\/Eucla', 'Australia\/Hobart', 'Australia\/Lindeman', 'Australia\/Lord_Howe', 'Australia\/Melbourne', 'Australia\/Perth', 'Australia\/Sydney', 'Europe\/Amsterdam', 'Europe\/Andorra', 'Europe\/Athens', 'Europe\/Belfast', 'Europe\/Belgrade', 'Europe\/Berlin', 'Europe\/Bratislava', 'Europe\/Brussels', 'Europe\/Bucharest', 'Europe\/Budapest', 'Europe\/Busingen', 'Europe\/Chisinau', 'Europe\/Copenhagen', 'Europe\/Dublin', 'Europe\/Gibraltar', 'Europe\/Guernsey', 'Europe\/Helsinki', 'Europe\/Isle_of_Man', 'Europe\/Istanbul', 'Europe\/Jersey', 'Europe\/Kaliningrad', 'Europe\/Kiev', 'Europe\/Lisbon', 'Europe\/Ljubljana', 'Europe\/London', 'Europe\/Luxembourg', 'Europe\/Madrid', 'Europe\/Malta', 'Europe\/Mariehamn', 'Europe\/Minsk', 'Europe\/Monaco', 'Europe\/Moscow', 'Europe\/Nicosia', 'Europe\/Oslo', 'Europe\/Paris', 'Europe\/Podgorica', 'Europe\/Prague', 'Europe\/Riga', 'Europe\/Rome', 'Europe\/Samara', 'Europe\/San_Marino', 'Europe\/Sarajevo', 'Europe\/Simferopol', 'Europe\/Skopje', 'Europe\/Sofia', 'Europe\/Stockholm', 'Europe\/Tallinn', 'Europe\/Tirane', 'Europe\/Uzhgorod', 'Europe\/Vaduz', 'Europe\/Vatican', 'Europe\/Vienna', 'Europe\/Vilnius', 'Europe\/Volgograd', 'Europe\/Warsaw', 'Europe\/Zagreb', 'Europe\/Zaporozhye', 'Europe\/Zurich', 'Indian\/Antananarivo', 'Indian\/Chagos', 'Indian\/Christmas', 'Indian\/Cocos', 'Indian\/Comoro', 'Indian\/Kerguelen', 'Indian\/Mahe', 'Indian\/Maldives', 'Indian\/Mauritius', 'Indian\/Mayotte', 'Indian\/Reunion', 'Pacific\/Apia', 'Pacific\/Auckland', 'Pacific\/Chatham', 'Pacific\/Chuuk', 'Pacific\/Easter', 'Pacific\/Efate', 'Pacific\/Enderbury', 'Pacific\/Fakaofo', 'Pacific\/Fiji', 'Pacific\/Funafuti', 'Pacific\/Galapagos', 'Pacific\/Gambier', 'Pacific\/Guadalcanal', 'Pacific\/Guam', 'Pacific\/Honolulu', 'Pacific\/Johnston', 'Pacific\/Kiritimati', 'Pacific\/Kosrae', 'Pacific\/Kwajalein', 'Pacific\/Majuro', 'Pacific\/Marquesas', 'Pacific\/Midway', 'Pacific\/Nauru', 'Pacific\/Niue', 'Pacific\/Norfolk', 'Pacific\/Noumea', 'Pacific\/Pago_Pago', 'Pacific\/Palau', 'Pacific\/Pitcairn', 'Pacific\/Pohnpei', 'Pacific\/Ponape', 'Pacific\/Port_Moresby', 'Pacific\/Rarotonga', 'Pacific\/Saipan', 'Pacific\/Tahiti', 'Pacific\/Tarawa', 'Pacific\/Tongatapu', 'Pacific\/Truk', 'Pacific\/Wake', 'Pacific\/Wallis', 'Pacific\/Yap', 'UTC', 'GMT', 'Z']); + }); +}); + +app.service('TimezoneService', ['$rootScope', '$http', 'Timezone', 'TimezoneListProvider', function ($rootScope, $http, Timezone, TimezoneListProvider) { + 'use strict'; + + var _this = this; + this._timezones = {}; + + this._timezones.UTC = new Timezone(ICAL.TimezoneService.get('UTC')); + this._timezones.GMT = this._timezones.UTC; + this._timezones.Z = this._timezones.UTC; + this._timezones.FLOATING = new Timezone(ICAL.Timezone.localTimezone); + + this.listAll = function () { + return TimezoneListProvider; + }; + + this.get = function (tzid) { + tzid = tzid.toUpperCase(); + + if (_this._timezones[tzid]) { + return new Promise(function (resolve) { + resolve(_this._timezones[tzid]); + }); + } + + _this._timezones[tzid] = $http({ + method: 'GET', + url: $rootScope.baseUrl + 'timezones/' + tzid + '.ics' + }).then(function (response) { + if (response.status >= 200 && response.status <= 299) { + var timezone = new Timezone(response.data); + _this._timezones[tzid] = timezone; + + return timezone; + } + }); + + return _this._timezones[tzid]; + }; + + this.getCurrent = function () { + return this.get(this.current()); + }; + + this.current = function () { + var tz = jstz.determine(); + var tzname = tz ? tz.name() : 'UTC'; + + switch (tzname) { + case 'Etc/UTC': + tzname = 'UTC'; + break; + + default: + break; + } + + return tzname; + }; +}]); + +app.service('VEventService', ['DavClient', 'VEvent', 'RandomStringService', function (DavClient, VEvent, RandomStringService) { + 'use strict'; + + var _this = this; + + this._xmls = new XMLSerializer(); + + this.getAll = function (calendar, start, end) { + var xmlDoc = document.implementation.createDocument('', '', null); + var cCalQuery = xmlDoc.createElement('c:calendar-query'); + cCalQuery.setAttribute('xmlns:c', 'urn:ietf:params:xml:ns:caldav'); + cCalQuery.setAttribute('xmlns:d', 'DAV:'); + cCalQuery.setAttribute('xmlns:a', 'http://apple.com/ns/ical/'); + cCalQuery.setAttribute('xmlns:o', 'http://owncloud.org/ns'); + xmlDoc.appendChild(cCalQuery); + + var dProp = xmlDoc.createElement('d:prop'); + cCalQuery.appendChild(dProp); + + var dGetEtag = xmlDoc.createElement('d:getetag'); + dProp.appendChild(dGetEtag); + + var cCalendarData = xmlDoc.createElement('c:calendar-data'); + dProp.appendChild(cCalendarData); + + var cFilter = xmlDoc.createElement('c:filter'); + cCalQuery.appendChild(cFilter); + + var cCompFilterVCal = xmlDoc.createElement('c:comp-filter'); + cCompFilterVCal.setAttribute('name', 'VCALENDAR'); + cFilter.appendChild(cCompFilterVCal); + + var cCompFilterVEvent = xmlDoc.createElement('c:comp-filter'); + cCompFilterVEvent.setAttribute('name', 'VEVENT'); + cCompFilterVCal.appendChild(cCompFilterVEvent); + + var cTimeRange = xmlDoc.createElement('c:time-range'); + cTimeRange.setAttribute('start', this._getTimeRangeStamp(start)); + cTimeRange.setAttribute('end', this._getTimeRangeStamp(end)); + cCompFilterVEvent.appendChild(cTimeRange); + + var url = calendar.url; + var headers = { + 'Content-Type': 'application/xml; charset=utf-8', + 'Depth': 1, + 'requesttoken': OC.requestToken + }; + var body = this._xmls.serializeToString(cCalQuery); + + return DavClient.request('REPORT', url, headers, body).then(function (response) { + if (!DavClient.wasRequestSuccessful(response.status)) { + //TODO - something went wrong + return; + } + + var vevents = []; + + for (var i in response.body) { + var object = response.body[i]; + var properties = object.propStat[0].properties; + + var data = properties['{urn:ietf:params:xml:ns:caldav}calendar-data']; + var etag = properties['{DAV:}getetag']; + var uri = object.href.substr(object.href.lastIndexOf('/') + 1); + + var vevent; + //try { + vevent = new VEvent(calendar, data, etag, uri); + //} catch(e) { + // console.log(e); + // continue; + //} + vevents.push(vevent); + } + + return vevents; + }); + }; + + this.get = function (calendar, uri) { + var url = calendar.url + uri; + return DavClient.request('GET', url, { 'requesttoken': OC.requestToken }, '').then(function (response) { + return new VEvent(calendar, response.body, response.xhr.getResponseHeader('ETag'), uri); + }); + }; + + this.create = function (calendar, data, returnEvent) { + if (typeof returnEvent === 'undefined') { + returnEvent = true; + } + + var headers = { + 'Content-Type': 'text/calendar; charset=utf-8', + 'requesttoken': OC.requestToken + }; + var uri = this._generateRandomUri(); + var url = calendar.url + uri; + + return DavClient.request('PUT', url, headers, data).then(function (response) { + if (!DavClient.wasRequestSuccessful(response.status)) { + return false; + // TODO - something went wrong, do smth about it + } + + return returnEvent ? _this.get(calendar, uri) : true; + }); + }; + + this.update = function (event) { + var url = event.calendar.url + event.uri; + var headers = { + 'Content-Type': 'text/calendar; charset=utf-8', + 'If-Match': event.etag, + 'requesttoken': OC.requestToken + }; + + return DavClient.request('PUT', url, headers, event.data).then(function (response) { + event.etag = response.xhr.getResponseHeader('ETag'); + return DavClient.wasRequestSuccessful(response.status); + }); + }; + + this.delete = function (event) { + var url = event.calendar.url + event.uri; + var headers = { + 'If-Match': event.etag, + 'requesttoken': OC.requestToken + }; + + return DavClient.request('DELETE', url, headers, '').then(function (response) { + return DavClient.wasRequestSuccessful(response.status); + }); + }; + + this._generateRandomUri = function () { + var uri = 'ownCloud-'; + uri += RandomStringService.generate(); + uri += RandomStringService.generate(); + uri += '.ics'; + + return uri; + }; + + this._getTimeRangeStamp = function (momentObject) { + return momentObject.format('YYYYMMDD') + 'T' + momentObject.format('HHmmss') + 'Z'; + }; +}]); + +app.service('ColorUtility', function () { + 'use strict'; + + var self = this; + + /** + * List of default colors + * @type {string[]} + */ + this.colors = []; + + /** + * generate an appropriate text color based on background color + * @param red + * @param green + * @param blue + * @returns {string} + */ + this.generateTextColorFromRGB = function (red, green, blue) { + var brightness = (red * 299 + green * 587 + blue * 114) / 1000; + return brightness > 130 ? '#000000' : '#FAFAFA'; + }; + + /** + * extract decimal values from hex rgb string + * @param colorString + * @returns {*} + */ + this.extractRGBFromHexString = function (colorString) { + var fallbackColor = { + r: 255, + g: 255, + b: 255 + }, + matchedString; + + if (typeof colorString !== 'string') { + return fallbackColor; + } + + switch (colorString.length) { + case 4: + matchedString = colorString.match(/^#([0-9a-f]{3})$/i); + return Array.isArray(matchedString) && matchedString[1] ? { + r: parseInt(matchedString[1].charAt(0), 16) * 0x11, + g: parseInt(matchedString[1].charAt(1), 16) * 0x11, + b: parseInt(matchedString[1].charAt(2), 16) * 0x11 + } : fallbackColor; + + case 7: + case 9: + var regex = new RegExp('^#([0-9a-f]{' + (colorString.length - 1) + '})$', 'i'); + matchedString = colorString.match(regex); + return Array.isArray(matchedString) && matchedString[1] ? { + r: parseInt(matchedString[1].substr(0, 2), 16), + g: parseInt(matchedString[1].substr(2, 2), 16), + b: parseInt(matchedString[1].substr(4, 2), 16) + } : fallbackColor; + + default: + return fallbackColor; + } + }; + + /** + * Make sure string for Hex always uses two digits + * @param str + * @returns {string} + * @private + */ + this._ensureTwoDigits = function (str) { + return str.length === 1 ? '0' + str : str; + }; + + /** + * convert three Numbers to rgb hex string + * @param r + * @param g + * @param b + * @returns {string} + */ + this.rgbToHex = function (r, g, b) { + if (Array.isArray(r)) { + var _r = r; + + var _r2 = _slicedToArray(_r, 3); + + r = _r2[0]; + g = _r2[1]; + b = _r2[2]; + } + + return '#' + this._ensureTwoDigits(parseInt(r, 10).toString(16)) + this._ensureTwoDigits(parseInt(g, 10).toString(16)) + this._ensureTwoDigits(parseInt(b, 10).toString(16)); + }; + + /** + * convert HSL to RGB + * @param h + * @param s + * @param l + * @returns array + */ + this._hslToRgb = function (h, s, l) { + if (Array.isArray(h)) { + var _h = h; + + var _h2 = _slicedToArray(_h, 3); + + h = _h2[0]; + s = _h2[1]; + l = _h2[2]; + } + + s /= 100; + l /= 100; + + return hslToRgb(h, s, l); + }; + + /** + * generates a random color + * @returns {string} + */ + this.randomColor = function () { + if (typeof String.prototype.toHsl === 'function') { + var hsl = Math.random().toString().toHsl(); + return self.rgbToHex(self._hslToRgb(hsl)); + } else { + return self.colors[Math.floor(Math.random() * self.colors.length)]; + } + }; + + // initialize default colors + if (typeof String.prototype.toHsl === 'function') { + //0 40 80 120 160 200 240 280 320 + var hashValues = ['15', '9', '4', 'b', '6', '11', '74', 'f', '57']; + angular.forEach(hashValues, function (hashValue) { + var hsl = hashValue.toHsl(); + self.colors.push(self.rgbToHex(self._hslToRgb(hsl))); + }); + } else { + this.colors = ['#31CC7C', '#317CCC', '#FF7A66', '#F1DB50', '#7C31CC', '#CC317C', '#3A3B3D', '#CACBCD']; + } +}); + +app.service('ICalSplitterUtility', ["ICalFactory", "SplittedICal", function (ICalFactory, SplittedICal) { + 'use strict'; + + var calendarColorIdentifier = 'x-apple-calendar-color'; + var calendarNameIdentifier = 'x-wr-calname'; + var componentNames = ['vevent', 'vjournal', 'vtodo']; + + /** + * split ics strings into a SplittedICal object + * @param {string} iCalString + * @returns {SplittedICal} + */ + this.split = function (iCalString) { + var jcal = ICAL.parse(iCalString); + var components = new ICAL.Component(jcal); + + var objects = {}; + var timezones = components.getAllSubcomponents('vtimezone'); + + componentNames.forEach(function (componentName) { + var vobjects = components.getAllSubcomponents(componentName); + objects[componentName] = {}; + + vobjects.forEach(function (vobject) { + var uid = vobject.getFirstPropertyValue('uid'); + objects[componentName][uid] = objects[componentName][uid] || []; + objects[componentName][uid].push(vobject); + }); + }); + + var name = components.getFirstPropertyValue(calendarNameIdentifier); + var color = components.getFirstPropertyValue(calendarColorIdentifier); + + var split = SplittedICal(name, color); + componentNames.forEach(function (componentName) { + var _loop = function _loop(objectKey) { + /* jshint loopfunc:true */ + if (!objects[componentName].hasOwnProperty(objectKey)) { + return 'continue'; + } + + var component = ICalFactory.new(); + timezones.forEach(function (timezone) { + component.addSubcomponent(timezone); + }); + objects[componentName][objectKey].forEach(function (object) { + component.addSubcomponent(object); + }); + split.addObject(componentName, component.toString()); + }; + + for (var objectKey in objects[componentName]) { + var _ret = _loop(objectKey); + + if (_ret === 'continue') continue; + } + }); + + return split; + }; +}]); + +app.service('PopoverPositioningUtility', ["$window", function ($window) { + 'use strict'; + + var context = { + popoverHeight: 300, + popoverWidth: 450 + }; + + Object.defineProperties(context, { + headerHeight: { + get: function get() { + return angular.element('#header').height(); + } + }, + navigationWidth: { + get: function get() { + return angular.element('#app-navigation').width(); + } + }, + windowX: { + get: function get() { + return $window.innerWidth - context.navigationWidth; + } + }, + windowY: { + get: function get() { + return $window.innerHeight - context.headerHeight; + } + } + }); + + context.isAgendaDayView = function (view) { + return view.name === 'agendaDay'; + }; + + context.isAgendaView = function (view) { + return view.name.startsWith('agenda'); + }; + + context.isInTheUpperPart = function (top) { + return (top - context.headerHeight) / context.windowY < 0.5; + }; + + context.isInTheLeftQuarter = function (left) { + return (left - context.navigationWidth) / context.windowX < 0.25; + }; + + context.isInTheRightQuarter = function (left) { + return (left - context.navigationWidth) / context.windowX > 0.75; + }; + + /** + * calculate the position of a popover + * @param {Number} left + * @param {Number}top + * @param {Number}right + * @param {Number}bottom + * @param {*} view + */ + this.calculate = function (left, top, right, bottom, view) { + var position = [], + eventWidth = right - left; + + if (context.isInTheUpperPart(top)) { + if (context.isAgendaView(view)) { + position.push({ + name: 'top', + value: top - context.headerHeight + 30 + }); + } else { + position.push({ + name: 'top', + value: bottom - context.headerHeight + 20 + }); + } + } else { + position.push({ + name: 'top', + value: top - context.headerHeight - context.popoverHeight - 20 + }); + } + + if (context.isAgendaDayView(view)) { + position.push({ + name: 'left', + value: left - context.popoverWidth / 2 - 20 + eventWidth / 2 + }); + } else { + if (context.isInTheLeftQuarter(left)) { + position.push({ + name: 'left', + value: left - 20 + eventWidth / 2 + }); + } else if (context.isInTheRightQuarter(left)) { + position.push({ + name: 'left', + value: left - context.popoverWidth - 20 + eventWidth / 2 + }); + } else { + position.push({ + name: 'left', + value: left - context.popoverWidth / 2 - 20 + eventWidth / 2 + }); + } + } + + return position; + }; + + /** + * calculate the position of a popover by a given target + * @param {*} target + * @param {*} view + */ + this.calculateByTarget = function (target, view) { + var clientRect = target.getClientRects()[0]; + + var left = clientRect.left, + top = clientRect.top, + right = clientRect.right, + bottom = clientRect.bottom; + + return this.calculate(left, top, right, bottom, view); + }; +}]); + +app.service('StringUtility', function () { + 'use strict'; + + this.uid = function (prefix, suffix) { + prefix = prefix || ''; + suffix = suffix || ''; + + if (prefix !== '') { + prefix += '-'; + } + if (suffix !== '') { + suffix = '.' + suffix; + } + + return prefix + Math.random().toString(36).substr(2).toUpperCase() + Math.random().toString(36).substr(2).toUpperCase() + suffix; + }; + + this.uri = function (start, isAvailable) { + start = start || ''; + + var uri = start.toString().toLowerCase().replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + + if (uri === '') { + uri = '-'; + } + + if (isAvailable(uri)) { + return uri; + } + + if (uri.indexOf('-') === -1) { + uri = uri + '-1'; + if (isAvailable(uri)) { + return uri; + } + } + + // === false because !undefined = true, possible infinite loop + do { + var positionLastDash = uri.lastIndexOf('-'); + var firstPart = uri.substr(0, positionLastDash); + var lastPart = uri.substr(positionLastDash + 1); + + if (lastPart.match(/^\d+$/)) { + lastPart = parseInt(lastPart); + lastPart++; + + uri = firstPart + '-' + lastPart; + } else { + uri = uri + '-1'; + } + } while (isAvailable(uri) === false); + + return uri; + }; +}); + +app.service('XMLUtility', function () { + 'use strict'; + + var context = {}; + context.XMLify = function (xmlDoc, parent, json) { + var element = xmlDoc.createElement(json.name); + + for (var key in json.attributes) { + if (json.attributes.hasOwnProperty(key)) { + element.setAttribute(key, json.attributes[key]); + } + } + + if (json.value) { + element.textContent = json.value; + } else if (json.children) { + for (var _key in json.children) { + if (json.children.hasOwnProperty(_key)) { + context.XMLify(xmlDoc, element, json.children[_key]); + } + } + } + + parent.appendChild(element); + }; + + var serializer = new XMLSerializer(); + + this.getRootSceleton = function () { + if (arguments.length === 0) { + return [{}, null]; + } + + var sceleton = { + name: arguments[0], + attributes: { + 'xmlns:c': 'urn:ietf:params:xml:ns:caldav', + 'xmlns:d': 'DAV:', + 'xmlns:a': 'http://apple.com/ns/ical/', + 'xmlns:o': 'http://owncloud.org/ns' + }, + children: [] + }; + + var childrenWrapper = sceleton.children; + + var args = Array.prototype.slice.call(arguments, 1); + args.forEach(function (argument) { + var level = { + name: argument, + children: [] + }; + childrenWrapper.push(level); + childrenWrapper = level.children; + }); + + return [sceleton, childrenWrapper]; + }; + + this.serialize = function (json) { + json = json || {}; + if ((typeof json === 'undefined' ? 'undefined' : _typeof(json)) !== 'object' || !json.hasOwnProperty('name')) { + return ''; + } + + var root = document.implementation.createDocument('', '', null); + context.XMLify(root, root, json); + + return serializer.serializeToString(root.firstChild); + }; +}); + diff --git a/templates/editor.popover.php b/templates/editor.popover.php index d8ef9ea198..25508f2dfd 100644 --- a/templates/editor.popover.php +++ b/templates/editor.popover.php @@ -4,6 +4,7 @@