summaryrefslogtreecommitdiff
path: root/calendar/base
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
committerMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
commitc3dc8a1f81c2148a64bc99a194da4c10614e9b95 (patch)
tree6915845b08018db4ee37f09a7a8ea9b4c17ebb27 /calendar/base
parentc0d30f29a0a1d418442c9dc05c83ac6ef2921d15 (diff)
downloadaura-central-c3dc8a1f81c2148a64bc99a194da4c10614e9b95.tar.gz
Manually re-add calendar
Diffstat (limited to 'calendar/base')
-rw-r--r--calendar/base/content/agenda-listbox.js1130
-rw-r--r--calendar/base/content/agenda-listbox.xml289
-rw-r--r--calendar/base/content/calendar-base-view.xml924
-rw-r--r--calendar/base/content/calendar-bindings.css46
-rw-r--r--calendar/base/content/calendar-calendars-list.xul76
-rw-r--r--calendar/base/content/calendar-chrome-startup.js165
-rw-r--r--calendar/base/content/calendar-clipboard.js201
-rw-r--r--calendar/base/content/calendar-common-sets.js950
-rw-r--r--calendar/base/content/calendar-common-sets.xul577
-rw-r--r--calendar/base/content/calendar-daypicker.xml265
-rw-r--r--calendar/base/content/calendar-dnd-listener.js596
-rw-r--r--calendar/base/content/calendar-extract.js274
-rw-r--r--calendar/base/content/calendar-invitations-manager.js422
-rw-r--r--calendar/base/content/calendar-item-bindings.xml97
-rw-r--r--calendar/base/content/calendar-item-editing.js742
-rw-r--r--calendar/base/content/calendar-management.js444
-rw-r--r--calendar/base/content/calendar-menus.xml149
-rw-r--r--calendar/base/content/calendar-month-view.xml1137
-rw-r--r--calendar/base/content/calendar-multiday-view.xml3886
-rw-r--r--calendar/base/content/calendar-statusbar.js111
-rw-r--r--calendar/base/content/calendar-task-editing.js250
-rw-r--r--calendar/base/content/calendar-task-tree.js312
-rw-r--r--calendar/base/content/calendar-task-tree.xml1195
-rw-r--r--calendar/base/content/calendar-task-view.js300
-rw-r--r--calendar/base/content/calendar-task-view.xul244
-rw-r--r--calendar/base/content/calendar-ui-utils.js654
-rw-r--r--calendar/base/content/calendar-unifinder-todo.js60
-rw-r--r--calendar/base/content/calendar-unifinder-todo.xul43
-rw-r--r--calendar/base/content/calendar-unifinder.js959
-rw-r--r--calendar/base/content/calendar-unifinder.xul140
-rw-r--r--calendar/base/content/calendar-view-bindings.css72
-rw-r--r--calendar/base/content/calendar-view-core.xml389
-rw-r--r--calendar/base/content/calendar-views.js723
-rw-r--r--calendar/base/content/calendar-views.xml289
-rw-r--r--calendar/base/content/calendar-views.xul115
-rw-r--r--calendar/base/content/dialogs/calendar-alarm-dialog.js359
-rw-r--r--calendar/base/content/dialogs/calendar-alarm-dialog.xul49
-rw-r--r--calendar/base/content/dialogs/calendar-conflicts-dialog.xul59
-rw-r--r--calendar/base/content/dialogs/calendar-creation.js49
-rw-r--r--calendar/base/content/dialogs/calendar-dialog-utils.js693
-rw-r--r--calendar/base/content/dialogs/calendar-error-prompt.xul67
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-attendees.js1004
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-attendees.xml1604
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-attendees.xul198
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml1599
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-recurrence-preview.xml245
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-recurrence.js804
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-recurrence.xul514
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-reminder.js446
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-reminder.xul121
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-timezone.js138
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog-timezone.xul46
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog.css64
-rw-r--r--calendar/base/content/dialogs/calendar-event-dialog.xul648
-rw-r--r--calendar/base/content/dialogs/calendar-invitations-dialog.css15
-rw-r--r--calendar/base/content/dialogs/calendar-invitations-dialog.js119
-rw-r--r--calendar/base/content/dialogs/calendar-invitations-dialog.xul49
-rw-r--r--calendar/base/content/dialogs/calendar-invitations-list.xml240
-rw-r--r--calendar/base/content/dialogs/calendar-migration-dialog.js647
-rw-r--r--calendar/base/content/dialogs/calendar-migration-dialog.xul48
-rw-r--r--calendar/base/content/dialogs/calendar-occurrence-prompt.xul85
-rw-r--r--calendar/base/content/dialogs/calendar-print-dialog.js320
-rw-r--r--calendar/base/content/dialogs/calendar-print-dialog.xul131
-rw-r--r--calendar/base/content/dialogs/calendar-properties-dialog.js178
-rw-r--r--calendar/base/content/dialogs/calendar-properties-dialog.xul115
-rw-r--r--calendar/base/content/dialogs/calendar-providerUninstall-dialog.js38
-rw-r--r--calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul37
-rw-r--r--calendar/base/content/dialogs/calendar-subscriptions-dialog.css13
-rw-r--r--calendar/base/content/dialogs/calendar-subscriptions-dialog.js154
-rw-r--r--calendar/base/content/dialogs/calendar-subscriptions-dialog.xul85
-rw-r--r--calendar/base/content/dialogs/calendar-summary-dialog.js401
-rw-r--r--calendar/base/content/dialogs/calendar-summary-dialog.xul300
-rw-r--r--calendar/base/content/dialogs/chooseCalendarDialog.xul91
-rw-r--r--calendar/base/content/import-export.js358
-rw-r--r--calendar/base/content/preferences/alarms.js137
-rw-r--r--calendar/base/content/preferences/alarms.xul239
-rw-r--r--calendar/base/content/preferences/categories.js339
-rw-r--r--calendar/base/content/preferences/categories.xul60
-rw-r--r--calendar/base/content/preferences/editCategory.js111
-rw-r--r--calendar/base/content/preferences/editCategory.xul41
-rw-r--r--calendar/base/content/preferences/general.js122
-rw-r--r--calendar/base/content/preferences/general.xul309
-rw-r--r--calendar/base/content/preferences/views.js99
-rw-r--r--calendar/base/content/preferences/views.xul306
-rw-r--r--calendar/base/content/today-pane.js482
-rw-r--r--calendar/base/content/today-pane.xul293
-rw-r--r--calendar/base/content/widgets/calendar-alarm-widget.xml351
-rw-r--r--calendar/base/content/widgets/calendar-list-tree.xml1110
-rw-r--r--calendar/base/content/widgets/calendar-subscriptions-list.xml129
-rw-r--r--calendar/base/content/widgets/calendar-widget-bindings.css70
-rw-r--r--calendar/base/content/widgets/calendar-widgets.xml731
-rw-r--r--calendar/base/content/widgets/minimonth.xml1221
-rw-r--r--calendar/base/jar.mn203
-rw-r--r--calendar/base/modules/calAlarmUtils.jsm170
-rw-r--r--calendar/base/modules/calAsyncUtils.jsm128
-rw-r--r--calendar/base/modules/calAuthUtils.jsm385
-rw-r--r--calendar/base/modules/calExtract.jsm1296
-rw-r--r--calendar/base/modules/calHashedArray.jsm261
-rw-r--r--calendar/base/modules/calItemUtils.jsm179
-rw-r--r--calendar/base/modules/calIteratorUtils.jsm190
-rw-r--r--calendar/base/modules/calItipUtils.jsm1676
-rw-r--r--calendar/base/modules/calPrintUtils.jsm200
-rw-r--r--calendar/base/modules/calProviderUtils.jsm856
-rw-r--r--calendar/base/modules/calRecurrenceUtils.jsm405
-rw-r--r--calendar/base/modules/calUtils.jsm1001
-rw-r--r--calendar/base/modules/calViewUtils.jsm71
-rw-r--r--calendar/base/modules/calXMLUtils.jsm174
-rw-r--r--calendar/base/modules/ical.js9354
-rw-r--r--calendar/base/modules/moz.build23
-rw-r--r--calendar/base/moz.build59
-rw-r--r--calendar/base/public/calBaseCID.h71
-rw-r--r--calendar/base/public/calIAlarm.idl159
-rw-r--r--calendar/base/public/calIAlarmService.idl110
-rw-r--r--calendar/base/public/calIAttachment.idl57
-rw-r--r--calendar/base/public/calIAttendee.idl80
-rw-r--r--calendar/base/public/calICalendar.idl641
-rw-r--r--calendar/base/public/calICalendarACLManager.idl89
-rw-r--r--calendar/base/public/calICalendarManager.idl115
-rw-r--r--calendar/base/public/calICalendarProvider.idl80
-rw-r--r--calendar/base/public/calICalendarSearchProvider.idl65
-rw-r--r--calendar/base/public/calICalendarView.idl233
-rw-r--r--calendar/base/public/calICalendarViewController.idl71
-rw-r--r--calendar/base/public/calIChangeLog.idl147
-rw-r--r--calendar/base/public/calIDateTime.idl232
-rw-r--r--calendar/base/public/calIDateTimeFormatter.idl162
-rw-r--r--calendar/base/public/calIDecoratedView.idl140
-rw-r--r--calendar/base/public/calIDeletedItems.idl26
-rw-r--r--calendar/base/public/calIDuration.idl113
-rw-r--r--calendar/base/public/calIErrors.idl118
-rw-r--r--calendar/base/public/calIEvent.idl42
-rw-r--r--calendar/base/public/calIFreeBusyProvider.idl109
-rw-r--r--calendar/base/public/calIICSService.idl280
-rw-r--r--calendar/base/public/calIIcsParser.idl112
-rw-r--r--calendar/base/public/calIIcsSerializer.idl76
-rw-r--r--calendar/base/public/calIImportExport.idl64
-rw-r--r--calendar/base/public/calIItemBase.idl352
-rw-r--r--calendar/base/public/calIItipItem.idl114
-rw-r--r--calendar/base/public/calIItipTransport.idl48
-rw-r--r--calendar/base/public/calIOperation.idl46
-rw-r--r--calendar/base/public/calIPeriod.idl67
-rw-r--r--calendar/base/public/calIPrintFormatter.idl44
-rw-r--r--calendar/base/public/calIRecurrenceDate.idl24
-rw-r--r--calendar/base/public/calIRecurrenceInfo.idl180
-rw-r--r--calendar/base/public/calIRecurrenceItem.idl56
-rw-r--r--calendar/base/public/calIRecurrenceRule.idl59
-rw-r--r--calendar/base/public/calIRelation.idl45
-rw-r--r--calendar/base/public/calISchedulingSupport.idl47
-rw-r--r--calendar/base/public/calIStartupService.idl30
-rw-r--r--calendar/base/public/calIStatusObserver.idl60
-rw-r--r--calendar/base/public/calITimezone.idl60
-rw-r--r--calendar/base/public/calITimezoneProvider.idl47
-rw-r--r--calendar/base/public/calITodo.idl72
-rw-r--r--calendar/base/public/calITransactionManager.idl81
-rw-r--r--calendar/base/public/calIWeekInfoService.idl50
-rw-r--r--calendar/base/public/moz.build61
-rw-r--r--calendar/base/src/WindowsNTToZoneInfoTZId.properties98
-rw-r--r--calendar/base/src/calAlarm.js698
-rw-r--r--calendar/base/src/calAlarmMonitor.js179
-rw-r--r--calendar/base/src/calAlarmService.js581
-rw-r--r--calendar/base/src/calApplicationUtils.js43
-rw-r--r--calendar/base/src/calAttachment.js179
-rw-r--r--calendar/base/src/calAttendee.js205
-rw-r--r--calendar/base/src/calCachedCalendar.js884
-rw-r--r--calendar/base/src/calCalendarManager.js1123
-rw-r--r--calendar/base/src/calCalendarSearchService.js97
-rw-r--r--calendar/base/src/calDateTimeFormatter.js296
-rw-r--r--calendar/base/src/calDefaultACLManager.js122
-rw-r--r--calendar/base/src/calDefaultACLManager.manifest2
-rw-r--r--calendar/base/src/calDeletedItems.js199
-rw-r--r--calendar/base/src/calEvent.js208
-rw-r--r--calendar/base/src/calFilter.js911
-rw-r--r--calendar/base/src/calFreeBusyService.js94
-rw-r--r--calendar/base/src/calIcsParser.js342
-rw-r--r--calendar/base/src/calIcsSerializer.js83
-rw-r--r--calendar/base/src/calInternalInterfaces.idl29
-rw-r--r--calendar/base/src/calItemBase.js1135
-rw-r--r--calendar/base/src/calItemModule.js67
-rw-r--r--calendar/base/src/calItemModule.manifest75
-rw-r--r--calendar/base/src/calItipItem.js215
-rw-r--r--calendar/base/src/calProtocolHandler.js96
-rw-r--r--calendar/base/src/calRecurrenceDate.js116
-rw-r--r--calendar/base/src/calRecurrenceInfo.js807
-rw-r--r--calendar/base/src/calRelation.js131
-rw-r--r--calendar/base/src/calSleepMonitor.js71
-rw-r--r--calendar/base/src/calSleepMonitor.manifest3
-rw-r--r--calendar/base/src/calStartupService.js113
-rw-r--r--calendar/base/src/calTimezone.js109
-rw-r--r--calendar/base/src/calTimezoneService.js817
-rw-r--r--calendar/base/src/calTimezoneService.manifest2
-rw-r--r--calendar/base/src/calTodo.js249
-rw-r--r--calendar/base/src/calTransactionManager.js215
-rw-r--r--calendar/base/src/calUtils.js1914
-rw-r--r--calendar/base/src/calWeekInfoService.js118
-rw-r--r--calendar/base/src/moz.build58
-rw-r--r--calendar/base/themes/common/calendar-alarms.css68
-rw-r--r--calendar/base/themes/common/calendar-attendees.css264
-rw-r--r--calendar/base/themes/common/calendar-creation-wizard.css15
-rw-r--r--calendar/base/themes/common/calendar-daypicker.css53
-rw-r--r--calendar/base/themes/common/calendar-itip-icons.svg121
-rw-r--r--calendar/base/themes/common/calendar-management.css49
-rw-r--r--calendar/base/themes/common/calendar-occurrence-prompt.css72
-rw-r--r--calendar/base/themes/common/calendar-printing.css43
-rw-r--r--calendar/base/themes/common/calendar-providerUninstall-dialog.css8
-rw-r--r--calendar/base/themes/common/calendar-task-tree.css136
-rw-r--r--calendar/base/themes/common/calendar-task-view.css139
-rw-r--r--calendar/base/themes/common/calendar-toolbar-osxlion.svg64
-rw-r--r--calendar/base/themes/common/calendar-toolbar.svg151
-rw-r--r--calendar/base/themes/common/calendar-unifinder.css39
-rw-r--r--calendar/base/themes/common/calendar-views.css955
-rw-r--r--calendar/base/themes/common/dialogs/calendar-alarm-dialog.css116
-rw-r--r--calendar/base/themes/common/dialogs/calendar-event-dialog.css565
-rw-r--r--calendar/base/themes/common/dialogs/calendar-invitations-dialog.css79
-rw-r--r--calendar/base/themes/common/dialogs/calendar-properties-dialog.css11
-rw-r--r--calendar/base/themes/common/dialogs/calendar-subscriptions-dialog.css52
-rw-r--r--calendar/base/themes/common/dialogs/calendar-timezone-highlighter.css136
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-event-dialog-attendees.pngbin0 -> 8696 bytes
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-event-dialog.pngbin0 -> 4503 bytes
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-event-tab.pngbin0 -> 424 bytes
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-button-images.pngbin0 -> 756 bytes
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-list-images.pngbin0 -> 2150 bytes
-rw-r--r--calendar/base/themes/common/dialogs/images/calendar-task-tab.pngbin0 -> 661 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-alarm-dialog.icobin0 -> 9574 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-alarm-dialog.pngbin0 -> 1802 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-event-dialog.icobin0 -> 6886 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-event-dialog.pngbin0 -> 631 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-event-summary-dialog.icobin0 -> 9062 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-event-summary-dialog.pngbin0 -> 775 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-task-dialog.icobin0 -> 6886 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-task-dialog.pngbin0 -> 1131 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-task-summary-dialog.icobin0 -> 9062 bytes
-rw-r--r--calendar/base/themes/common/icons/calendar-task-summary-dialog.pngbin0 -> 1250 bytes
-rw-r--r--calendar/base/themes/common/images/alarm-flashing.pngbin0 -> 2788 bytes
-rw-r--r--calendar/base/themes/common/images/alarm-icons.pngbin0 -> 1013 bytes
-rw-r--r--calendar/base/themes/common/images/attendee-icons.pngbin0 -> 5592 bytes
-rw-r--r--calendar/base/themes/common/images/calendar-overlay.pngbin0 -> 441 bytes
-rw-r--r--calendar/base/themes/common/images/calendar-status.pngbin0 -> 697 bytes
-rw-r--r--calendar/base/themes/common/images/checkbox-images.pngbin0 -> 521 bytes
-rw-r--r--calendar/base/themes/common/images/classification.pngbin0 -> 325 bytes
-rw-r--r--calendar/base/themes/common/images/day-box-item-image.pngbin0 -> 1104 bytes
-rw-r--r--calendar/base/themes/common/images/event-grippy-bottom.pngbin0 -> 179 bytes
-rw-r--r--calendar/base/themes/common/images/event-grippy-left.pngbin0 -> 209 bytes
-rw-r--r--calendar/base/themes/common/images/event-grippy-right.pngbin0 -> 210 bytes
-rw-r--r--calendar/base/themes/common/images/event-grippy-top.pngbin0 -> 178 bytes
-rw-r--r--calendar/base/themes/common/images/ok-cancel.pngbin0 -> 4390 bytes
-rw-r--r--calendar/base/themes/common/images/task-images.pngbin0 -> 297 bytes
-rw-r--r--calendar/base/themes/common/images/timezone_map.pngbin0 -> 14423 bytes
-rw-r--r--calendar/base/themes/common/images/timezones.pngbin0 -> 101805 bytes
-rw-r--r--calendar/base/themes/common/today-pane-cycler.svg15
-rw-r--r--calendar/base/themes/common/today-pane.css240
-rw-r--r--calendar/base/themes/common/widgets/calendar-widgets.css76
-rw-r--r--calendar/base/themes/common/widgets/images/drag-center.svg11
-rw-r--r--calendar/base/themes/common/widgets/images/nav-arrow.svg8
-rw-r--r--calendar/base/themes/common/widgets/images/nav-today-hov.svg9
-rw-r--r--calendar/base/themes/common/widgets/images/nav-today.svg10
-rw-r--r--calendar/base/themes/common/widgets/images/view-navigation-hov.svg8
-rw-r--r--calendar/base/themes/common/widgets/images/view-navigation.svg8
-rw-r--r--calendar/base/themes/common/widgets/minimonth.css202
-rw-r--r--calendar/base/themes/linux/calendar-daypicker.css18
-rw-r--r--calendar/base/themes/linux/calendar-management.css25
-rw-r--r--calendar/base/themes/linux/calendar-task-tree.css26
-rw-r--r--calendar/base/themes/linux/calendar-task-view.css106
-rw-r--r--calendar/base/themes/linux/calendar-unifinder.css36
-rw-r--r--calendar/base/themes/linux/calendar-views.css60
-rw-r--r--calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css17
-rw-r--r--calendar/base/themes/linux/dialogs/calendar-event-dialog.css101
-rw-r--r--calendar/base/themes/linux/dialogs/calendar-invitations-dialog.css5
-rw-r--r--calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar-small.pngbin0 -> 27333 bytes
-rw-r--r--calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar.pngbin0 -> 62948 bytes
-rw-r--r--calendar/base/themes/linux/images/cal-icon24.pngbin0 -> 814 bytes
-rw-r--r--calendar/base/themes/linux/images/cal-icon32.pngbin0 -> 611 bytes
-rw-r--r--calendar/base/themes/linux/images/calendar-occurrence-prompt.pngbin0 -> 1857 bytes
-rw-r--r--calendar/base/themes/linux/images/tasks-actions.pngbin0 -> 1565 bytes
-rw-r--r--calendar/base/themes/linux/images/toolbar-large.pngbin0 -> 47711 bytes
-rw-r--r--calendar/base/themes/linux/images/toolbar-small.pngbin0 -> 17728 bytes
-rw-r--r--calendar/base/themes/linux/preferences/Options.pngbin0 -> 25387 bytes
-rw-r--r--calendar/base/themes/linux/preferences/preferences.css94
-rw-r--r--calendar/base/themes/linux/today-pane.css92
-rw-r--r--calendar/base/themes/linux/widgets/calendar-widgets.css38
-rw-r--r--calendar/base/themes/windows/calendar-daypicker.css18
-rw-r--r--calendar/base/themes/windows/calendar-management.css25
-rw-r--r--calendar/base/themes/windows/calendar-task-tree.css82
-rw-r--r--calendar/base/themes/windows/calendar-task-view.css206
-rw-r--r--calendar/base/themes/windows/calendar-unifinder.css55
-rw-r--r--calendar/base/themes/windows/calendar-views.css75
-rw-r--r--calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css17
-rw-r--r--calendar/base/themes/windows/dialogs/calendar-event-dialog.css300
-rw-r--r--calendar/base/themes/windows/dialogs/calendar-invitations-dialog.css5
-rw-r--r--calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-aero.pngbin0 -> 12592 bytes
-rw-r--r--calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-inverted.pngbin0 -> 5128 bytes
-rw-r--r--calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-small.pngbin0 -> 43403 bytes
-rw-r--r--calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar.pngbin0 -> 90471 bytes
-rw-r--r--calendar/base/themes/windows/images/cal-icon24.pngbin0 -> 637 bytes
-rw-r--r--calendar/base/themes/windows/images/cal-icon32.pngbin0 -> 578 bytes
-rw-r--r--calendar/base/themes/windows/images/calendar-occurrence-prompt-aero.pngbin0 -> 2020 bytes
-rw-r--r--calendar/base/themes/windows/images/calendar-occurrence-prompt.pngbin0 -> 2571 bytes
-rw-r--r--calendar/base/themes/windows/images/tasks-actions-aero.pngbin0 -> 1433 bytes
-rw-r--r--calendar/base/themes/windows/images/tasks-actions-inverted.pngbin0 -> 729 bytes
-rw-r--r--calendar/base/themes/windows/images/tasks-actions.pngbin0 -> 1866 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-aero-inverted.pngbin0 -> 2296 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-aero.pngbin0 -> 3952 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-large-aero.pngbin0 -> 44840 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-large.pngbin0 -> 53778 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-small-aero.pngbin0 -> 16245 bytes
-rw-r--r--calendar/base/themes/windows/images/toolbar-small.pngbin0 -> 27827 bytes
-rw-r--r--calendar/base/themes/windows/preferences/Options.pngbin0 -> 25387 bytes
-rw-r--r--calendar/base/themes/windows/preferences/preferences.css94
-rw-r--r--calendar/base/themes/windows/today-pane.css165
-rw-r--r--calendar/base/themes/windows/widgets/calendar-widgets.css60
308 files changed, 76200 insertions, 0 deletions
diff --git a/calendar/base/content/agenda-listbox.js b/calendar/base/content/agenda-listbox.js
new file mode 100644
index 000000000..ced5811ee
--- /dev/null
+++ b/calendar/base/content/agenda-listbox.js
@@ -0,0 +1,1130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function Synthetic(aOpen, aDuration, aMultiday) {
+ this.open = aOpen;
+ this.duration = aDuration;
+ this.multiday = aMultiday;
+}
+
+var agendaListbox = {
+ agendaListboxControl: null,
+ mPendingRefreshJobs: null,
+ kDefaultTimezone: null,
+ showsToday: false,
+ soonDays: 5
+};
+
+/**
+ * Initialize the agenda listbox, used on window load.
+ */
+agendaListbox.init = function() {
+ this.agendaListboxControl = document.getElementById("agenda-listbox");
+ this.agendaListboxControl.removeAttribute("suppressonselect");
+ let showTodayHeader = (document.getElementById("today-header-hidden").getAttribute("checked") == "true");
+ let showTomorrowHeader = (document.getElementById("tomorrow-header-hidden").getAttribute("checked") == "true");
+ let showSoonHeader = (document.getElementById("nextweek-header-hidden").getAttribute("checked") == "true");
+ this.today = new Synthetic(showTodayHeader, 1, false);
+ this.addPeriodListItem(this.today, "today-header");
+ this.tomorrow = new Synthetic(showTomorrowHeader, 1, false);
+ this.soonDays = getSoondaysPreference();
+ this.soon = new Synthetic(showSoonHeader, this.soonDays, true);
+ this.periods = [this.today, this.tomorrow, this.soon];
+ this.mPendingRefreshJobs = new Map();
+
+ let prefObserver = {
+ observe: function(aSubject, aTopic, aPrefName) {
+ switch (aPrefName) {
+ case "calendar.agendaListbox.soondays":
+ agendaListbox.soonDays = getSoondaysPreference();
+ agendaListbox.updateSoonSection();
+ break;
+ }
+ }
+ };
+ Services.prefs.addObserver("calendar.agendaListbox", prefObserver, false);
+
+ // Make sure the agenda listbox is unloaded
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("calendar.agendaListbox", prefObserver);
+ this.uninit();
+ }, false);
+};
+
+/**
+ * Clean up the agenda listbox, used on window unload.
+ */
+agendaListbox.uninit = function() {
+ if (this.calendar) {
+ this.calendar.removeObserver(this.calendarObserver);
+ }
+
+ for (let period of this.periods) {
+ if (period.listItem) {
+ period.listItem.getCheckbox()
+ .removeEventListener("CheckboxStateChange",
+ this.onCheckboxChange,
+ true);
+ }
+ }
+};
+
+/**
+ * Adds a period item to the listbox. This is a section of the today pane like
+ * "Today", "Tomorrow", and is usually a <agenda-checkbox-richlist-item> tag. A
+ * copy of the template node is made and added to the agenda listbox.
+ *
+ * @param aPeriod The period item to add.
+ * @param aItemId The id of an <agenda-checkbox-richlist-item> to add to,
+ * without the "-hidden" suffix.
+ */
+agendaListbox.addPeriodListItem = function(aPeriod, aItemId) {
+ aPeriod.listItem = document.getElementById(aItemId + "-hidden").cloneNode(true);
+ agendaListbox.agendaListboxControl.appendChild(aPeriod.listItem);
+ aPeriod.listItem.id = aItemId;
+ aPeriod.listItem.getCheckbox().setChecked(aPeriod.open);
+ aPeriod.listItem.getCheckbox().addEventListener("CheckboxStateChange", this.onCheckboxChange, true);
+};
+
+/**
+ * Remove a period item from the agenda listbox.
+ * @see agendaListbox::addPeriodListItem
+ */
+agendaListbox.removePeriodListItem = function(aPeriod) {
+ if (aPeriod.listItem) {
+ aPeriod.listItem.getCheckbox().removeEventListener("CheckboxStateChange", this.onCheckboxChange, true);
+ if (aPeriod.listItem) {
+ aPeriod.listItem.remove();
+ aPeriod.listItem = null;
+ }
+ }
+};
+
+/**
+ * Handler function called when changing the checkbox state on period items.
+ *
+ * @param event The DOM event that triggered the checkbox state change.
+ */
+agendaListbox.onCheckboxChange = function(event) {
+ let periodCheckbox = event.target;
+ let lopen = (periodCheckbox.getAttribute("checked") == "true");
+ let listItem = getParentNodeOrThis(periodCheckbox, "agenda-checkbox-richlist-item");
+ let period = listItem.getItem();
+ period.open = lopen;
+ // as the agenda-checkboxes are only transient we have to set the "checked"
+ // attribute at their hidden origins to make that attribute persistent.
+ document.getElementById(listItem.id + "-hidden").setAttribute("checked",
+ periodCheckbox.getAttribute("checked"));
+ if (lopen) {
+ agendaListbox.refreshCalendarQuery(period.start, period.end);
+ } else {
+ listItem = listItem.nextSibling;
+ let leaveloop;
+ do {
+ leaveloop = (listItem == null);
+ if (!leaveloop) {
+ let nextItemSibling = listItem.nextSibling;
+ leaveloop = !agendaListbox.isEventListItem(listItem);
+ if (!leaveloop) {
+ listItem.remove();
+ listItem = nextItemSibling;
+ }
+ }
+ } while (!leaveloop);
+ }
+ calendarController.onSelectionChanged({ detail: [] });
+};
+
+/**
+ * Handler function called when an agenda listbox item is selected
+ *
+ * @param aListItem The agenda-base-richlist-item that was selected.
+ */
+agendaListbox.onSelect = function(aListItem) {
+ let listbox = document.getElementById("agenda-listbox");
+ let item = aListItem || listbox.selectedItem;
+ if (aListItem) {
+ listbox.selectedItem = item;
+ }
+ calendarController.onSelectionChanged({ detail: agendaListbox.getSelectedItems() });
+};
+
+/**
+ * Handler function called when the agenda listbox becomes focused
+ */
+agendaListbox.onFocus = function() {
+ calendarController.onSelectionChanged({ detail: agendaListbox.getSelectedItems() });
+};
+
+/**
+ * Handler function called when the agenda listbox loses focus.
+ */
+agendaListbox.onBlur = function() {
+ calendarController.onSelectionChanged({ detail: [] });
+};
+
+
+/**
+ * Handler function called when a key was pressed on the agenda listbox
+ */
+agendaListbox.onKeyPress = function(aEvent) {
+ let listItem = aEvent.target;
+ if (listItem.localName == "richlistbox") {
+ listItem = listItem.selectedItem;
+ }
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_RETURN:
+ document.getElementById("agenda_edit_event_command").doCommand();
+ break;
+ case aEvent.DOM_VK_DELETE:
+ document.getElementById("agenda_delete_event_command").doCommand();
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ case aEvent.DOM_VK_LEFT:
+ if (!this.isEventListItem(listItem)) {
+ listItem.getCheckbox().setChecked(false);
+ }
+ break;
+ case aEvent.DOM_VK_RIGHT:
+ if (!this.isEventListItem(listItem)) {
+ listItem.getCheckbox().setChecked(true);
+ }
+ break;
+ }
+};
+
+/**
+ * Calls the event dialog to edit the currently selected item
+ */
+agendaListbox.editSelectedItem = function() {
+ let listItem = document.getElementById("agenda-listbox").selectedItem;
+ if (listItem) {
+ modifyEventWithDialog(listItem.occurrence, null, true);
+ }
+};
+
+/**
+ * Finds the appropriate period for the given item, i.e finds "Tomorrow" if the
+ * item occurrs tomorrow.
+ *
+ * @param aItem The item to find the period for.
+ */
+agendaListbox.findPeriodsForItem = function(aItem) {
+ let retPeriods = [];
+ for (let i = 0; i < this.periods.length; i++) {
+ if (this.periods[i].open) {
+ if (checkIfInRange(aItem, this.periods[i].start, this.periods[i].end)) {
+ retPeriods.push(this.periods[i]);
+ }
+ }
+ }
+ return retPeriods;
+};
+
+/**
+ * Gets the start of the earliest period shown in the agenda listbox
+ */
+agendaListbox.getStart = function() {
+ let retStart = null;
+ for (let i = 0; i < this.periods.length; i++) {
+ if (this.periods[i].open) {
+ retStart = this.periods[i].start;
+ break;
+ }
+ }
+ return retStart;
+};
+
+/**
+ * Gets the end of the latest period shown in the agenda listbox
+ */
+agendaListbox.getEnd = function() {
+ let retEnd = null;
+ for (let i = this.periods.length - 1; i >= 0; i--) {
+ if (this.periods[i].open) {
+ retEnd = this.periods[i].end;
+ break;
+ }
+ }
+ return retEnd;
+};
+
+/**
+ * Adds an item to an agenda period before another existing item.
+ *
+ * @param aNewItem The calIItemBase to add.
+ * @param aAgendaItem The existing item to insert before.
+ * @param aPeriod The period to add the item to.
+ * @param visible If true, the item should be visible.
+ * @return The newly created XUL element.
+ */
+agendaListbox.addItemBefore = function(aNewItem, aAgendaItem, aPeriod, visible) {
+ let newelement = null;
+ if (aNewItem.startDate.isDate) {
+ newelement = createXULElement("agenda-allday-richlist-item");
+ } else {
+ newelement = createXULElement("agenda-richlist-item");
+ }
+ // set the item at the richlistItem. When the duration of the period
+ // is bigger than 1 (day) the starttime of the item has to include
+ // information about the day of the item
+ if (aAgendaItem == null) {
+ this.agendaListboxControl.appendChild(newelement);
+ } else {
+ this.agendaListboxControl.insertBefore(newelement, aAgendaItem);
+ }
+ newelement.setOccurrence(aNewItem, aPeriod);
+ newelement.removeAttribute("selected");
+ return newelement;
+};
+
+/**
+ * Adds an item to the agenda listbox. This function finds the correct period
+ * for the item and inserts it correctly so the period stays sorted.
+ *
+ * @param aItem The calIItemBase to add.
+ * @return The newly created XUL element.
+ */
+agendaListbox.addItem = function(aItem) {
+ if (!isEvent(aItem)) {
+ return null;
+ }
+ let periods = this.findPeriodsForItem(aItem);
+ if (periods.length == 0) {
+ return null;
+ }
+ let newlistItem = null;
+ for (let i = 0; i < periods.length; i++) {
+ let period = periods[i];
+ let complistItem = period.listItem;
+ let visible = complistItem.getCheckbox().checked;
+ if (aItem.startDate.isDate && period.duration == 1 && aItem.duration.days == 1) {
+ if (this.getListItems(aItem, period).length == 0) {
+ this.addItemBefore(aItem, period.listItem.nextSibling, period, visible);
+ }
+ } else {
+ do {
+ complistItem = complistItem.nextSibling;
+ if (this.isEventListItem(complistItem)) {
+ let compitem = complistItem.occurrence;
+ if (this.isSameEvent(aItem, compitem)) {
+ // The same event occurs on several calendars but we only
+ // display the first one.
+ // TODO: find a way to display this special circumstance
+ break;
+ } else if (this.isBefore(aItem, compitem, period)) {
+ if (this.isSameEvent(aItem, compitem)) {
+ newlistItem = this.addItemBefore(aItem, complistItem, period, visible);
+ break;
+ } else {
+ newlistItem = this.addItemBefore(aItem, complistItem, period, visible);
+ break;
+ }
+ }
+ } else {
+ newlistItem = this.addItemBefore(aItem, complistItem, period, visible);
+ break;
+ }
+ } while (complistItem);
+ }
+ }
+ return newlistItem;
+};
+
+/**
+ * Checks if the given item happens before the comparison item.
+ *
+ * @param aItem The item to compare.
+ * @param aCompItem The item to compare with.
+ * @param aPeriod The period where the items are inserted.
+ * @return True, if the aItem happens before aCompItem.
+ */
+agendaListbox.isBefore = function(aItem, aCompItem, aPeriod) {
+ let itemDate = this.comparisonDate(aItem, aPeriod);
+ let compItemDate = this.comparisonDate(aCompItem, aPeriod);
+ let itemDateEndDate = itemDate.clone();
+ itemDateEndDate.day++;
+
+ if (compItemDate.day == itemDate.day) {
+ // In the same day the order is:
+ // - all-day events (single day);
+ // - all-day events spanning multiple days: start, end, intermediate;
+ // - events and events spanning multiple days: start, end, (sorted by
+ // time) and intermediate.
+ if (itemDate.isDate && aItem.duration.days == 1) {
+ // all-day events with duration one day
+ return true;
+ } else if (itemDate.isDate) {
+ if (aItem.startDate.compare(itemDate) == 0) {
+ // starting day of an all-day events spannig multiple days
+ return !compItemDate.isDate || aCompItem.duration.days != 1;
+ } else if (aItem.endDate.compare(itemDateEndDate) == 0) {
+ // ending day of an all-day events spannig multiple days
+ return !compItemDate.isDate ||
+ (aCompItem.duration.days != 1 &&
+ aCompItem.startDate.compare(compItemDate) != 0);
+ } else {
+ // intermediate day of an all-day events spannig multiple days
+ return !compItemDate.isDate;
+ }
+ } else if (aCompItem.startDate.isDate) {
+ return false;
+ }
+ }
+ // Non all-day event sorted by date-time. When equal, sorted by start
+ // date-time then by end date-time.
+ let comp = itemDate.compare(compItemDate);
+ if (comp == 0) {
+ comp = aItem.startDate.compare(aCompItem.startDate);
+ if (comp == 0) {
+ comp = aItem.endDate.compare(aCompItem.endDate);
+ }
+ }
+ return (comp <= 0);
+};
+
+/**
+ * Returns the start or end date of an item according to which of them
+ * must be displayed in a given period of the agenda
+ *
+ * @param aItem The item to compare.
+ * @param aPeriod The period where the item is inserted.
+ * @return The start or end date of the item showed in the agenda.
+ */
+agendaListbox.comparisonDate = function(aItem, aPeriod) {
+ let periodStartDate = aPeriod.start.clone();
+ periodStartDate.isDate = true;
+ let periodEndDate = aPeriod.end.clone();
+ periodEndDate.day--;
+ let startDate = aItem.startDate.clone();
+ startDate.isDate = true;
+ let endDate = aItem.endDate.clone();
+
+ let endDateToReturn = aItem.endDate.clone();
+ if (aItem.startDate.isDate && aPeriod.duration == 1) {
+ endDateToReturn = periodEndDate.clone();
+ } else if (endDate.isDate) {
+ endDateToReturn.day--;
+ } else if (endDate.hour == 0 && endDate.minute == 0) {
+ // End at midnight -> end date in the day where midnight occurs
+ endDateToReturn.day--;
+ endDateToReturn.hour = 23;
+ endDateToReturn.minute = 59;
+ endDateToReturn.second = 59;
+ }
+ endDate.isDate = true;
+ if (startDate.compare(endDate) != 0 &&
+ startDate.compare(periodStartDate) < 0) {
+ // returns a end date when the item is a multiday event AND
+ // it starts before the given period
+ return endDateToReturn;
+ }
+ return aItem.startDate.clone();
+};
+
+/**
+ * Gets the listitems for a given item, possibly in a given period.
+ *
+ * @param aItem The item to get the list items for.
+ * @param aPeriod (optional) the period to search in.
+ * @return An array of list items for the given item.
+ */
+agendaListbox.getListItems = function(aItem, aPeriod) {
+ let retlistItems = [];
+ let periods = [aPeriod];
+ if (!aPeriod) {
+ periods = this.findPeriodsForItem(aItem);
+ }
+ if (periods.length > 0) {
+ for (let i = 0; i < periods.length; i++) {
+ let period = periods[i];
+ let complistItem = period.listItem;
+ let leaveloop;
+ do {
+ complistItem = complistItem.nextSibling;
+ leaveloop = !this.isEventListItem(complistItem);
+ if (!leaveloop) {
+ if (this.isSameEvent(aItem, complistItem.occurrence)) {
+ retlistItems.push(complistItem);
+ break;
+ }
+ }
+ } while (!leaveloop);
+ }
+ }
+ return retlistItems;
+};
+
+/**
+ * Removes the given item from the agenda listbox
+ *
+ * @param aItem The item to remove.
+ * @param aMoveSelection If true, the selection will be moved to the next
+ * sibling that is not an period item.
+ * @return Returns true if the removed item was selected.
+ */
+agendaListbox.deleteItem = function(aItem, aMoveSelection) {
+ let isSelected = false;
+ let listItems = this.getListItems(aItem);
+ if (listItems.length > 0) {
+ for (let i = listItems.length - 1; i >= 0; i--) {
+ let listItem = listItems[i];
+ let isSelected2 = listItem.selected;
+ if (isSelected2 && !isSelected) {
+ isSelected = true;
+ if (aMoveSelection) {
+ this.moveSelection();
+ }
+ }
+ listItem.remove();
+ }
+ }
+ return isSelected;
+};
+
+/**
+ * Remove all items belonging to the specified calendar.
+ *
+ * @param aCalendar The item to compare.
+ */
+agendaListbox.deleteItemsFromCalendar = function(aCalendar) {
+ let childNodes = Array.from(this.agendaListboxControl.childNodes);
+ for (let childNode of childNodes) {
+ if (childNode && childNode.occurrence &&
+ childNode.occurrence.calendar.id == aCalendar.id) {
+ childNode.remove();
+ }
+ }
+};
+
+/**
+ * Compares two items to see if they have the same id and their start date
+ * matches
+ *
+ * @param aItem The item to compare.
+ * @param aCompItem The item to compare with.
+ * @return True, if the items match with the above noted criteria.
+ */
+agendaListbox.isSameEvent = function(aItem, aCompItem) {
+ return aItem.id == aCompItem.id &&
+ aItem[calGetStartDateProp(aItem)].compare(aCompItem[calGetStartDateProp(aCompItem)]) == 0;
+};
+
+/**
+ * Checks if the currently selected node in the listbox is an Event item (not a
+ * period item).
+ *
+ * @return True, if the node is not a period item.
+ */
+agendaListbox.isEventSelected = function() {
+ let listItem = this.agendaListboxControl.selectedItem;
+ if (listItem) {
+ return this.isEventListItem(listItem);
+ }
+ return false;
+};
+
+/**
+ * Delete the selected item from its calendar (if it is an event item)
+ *
+ * @param aDoNotConfirm If true, the user will not be prompted.
+ */
+agendaListbox.deleteSelectedItem = function(aDoNotConfirm) {
+ let listItem = this.agendaListboxControl.selectedItem;
+ if (this.isEventListItem(listItem)) {
+ let selectedItems = [listItem.occurrence];
+ calendarViewController.deleteOccurrences(selectedItems.length,
+ selectedItems,
+ false,
+ aDoNotConfirm);
+ }
+};
+
+/**
+ * If a Period item is targeted by the passed DOM event, opens the event dialog
+ * with the period's start date prefilled.
+ *
+ * @param aEvent The DOM event that targets the period.
+ */
+agendaListbox.createNewEvent = function(aEvent) {
+ if (!this.isEventListItem(aEvent.target)) {
+ // Create new event for the date currently displayed in the agenda. Setting
+ // isDate = true automatically makes the start time be the next full hour.
+ let eventStart = agendaListbox.today.start.clone();
+ eventStart.isDate = true;
+ if (calendarController.isCommandEnabled("calendar_new_event_command")) {
+ createEventWithDialog(getSelectedCalendar(), eventStart);
+ }
+ }
+};
+
+/**
+ * Sets up the context menu for the agenda listbox
+ *
+ * @param popup The <menupopup> element to set up.
+ */
+agendaListbox.setupContextMenu = function(popup) {
+ let listItem = this.agendaListboxControl.selectedItem;
+ let enabled = this.isEventListItem(listItem);
+ let menuitems = popup.childNodes;
+ for (let i = 0; i < menuitems.length; i++) {
+ setBooleanAttribute(menuitems[i], "disabled", !enabled);
+ }
+
+ let menu = document.getElementById("calendar-today-pane-menu-attendance-menu");
+ setupAttendanceMenu(menu, agendaListbox.getSelectedItems({}));
+};
+
+
+/**
+ * Refreshes the agenda listbox. If aStart or aEnd is not passed, the agenda
+ * listbox's limiting dates will be used.
+ *
+ * @param aStart (optional) The start date for the item query.
+ * @param aEnd (optional) The end date for the item query.
+ * @param aCalendar (optional) If specified, the single calendar from
+ * which the refresh will occur.
+ */
+agendaListbox.refreshCalendarQuery = function(aStart, aEnd, aCalendar) {
+ let refreshJob = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ agendaListbox: this,
+ calendar: null,
+ calId: null,
+ operation: null,
+ cancelled: false,
+
+ onOperationComplete: function(aOpCalendar, aStatus, aOperationType, aId, aDateTime) {
+ if (this.agendaListbox.mPendingRefreshJobs.has(this.calId)) {
+ this.agendaListbox.mPendingRefreshJobs.delete(this.calId);
+ }
+
+ if (!this.cancelled) {
+ setCurrentEvent();
+ }
+ },
+
+ onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (this.cancelled || !Components.isSuccessCode(aStatus)) {
+ return;
+ }
+ for (let item of aItems) {
+ this.agendaListbox.addItem(item);
+ }
+ },
+
+ cancel: function() {
+ this.cancelled = true;
+ let operation = cal.wrapInstance(this.operation, Components.interfaces.calIOperation);
+ if (operation && operation.isPending) {
+ operation.cancel();
+ this.operation = null;
+ }
+ },
+
+ execute: function() {
+ if (!(aStart || aEnd || aCalendar)) {
+ this.agendaListbox.removeListItems();
+ }
+
+ if (!aCalendar) {
+ aCalendar = this.agendaListbox.calendar;
+ }
+ if (!aStart) {
+ aStart = this.agendaListbox.getStart();
+ }
+ if (!aEnd) {
+ aEnd = this.agendaListbox.getEnd();
+ }
+ if (!(aStart || aEnd || aCalendar)) {
+ return;
+ }
+
+ if (aCalendar.type == "composite") {
+ // we're refreshing from the composite calendar, so we can cancel
+ // all other pending refresh jobs.
+ this.calId = "composite";
+ for (let job of this.agendaListbox.mPendingRefreshJobs.values()) {
+ job.cancel();
+ }
+ this.agendaListbox.mPendingRefreshJobs.clear();
+ } else {
+ this.calId = aCalendar.id;
+ if (this.agendaListbox.mPendingRefreshJobs.has(this.calId)) {
+ this.agendaListbox.mPendingRefreshJobs.get(this.calId).cancel();
+ this.agendaListbox.mPendingRefreshJobs.delete(this.calId);
+ }
+ }
+ this.calendar = aCalendar;
+
+ let filter = this.calendar.ITEM_FILTER_CLASS_OCCURRENCES |
+ this.calendar.ITEM_FILTER_TYPE_EVENT;
+ let operation = this.calendar.getItems(filter, 0, aStart, aEnd, this);
+ operation = cal.wrapInstance(operation, Components.interfaces.calIOperation);
+ if (operation && operation.isPending) {
+ this.operation = operation;
+ this.agendaListbox.mPendingRefreshJobs.set(this.calId, this);
+ }
+ }
+ };
+
+ refreshJob.execute();
+};
+
+/**
+ * Sets up the calendar for the agenda listbox.
+ */
+agendaListbox.setupCalendar = function() {
+ this.init();
+ if (this.calendar == null) {
+ this.calendar = getCompositeCalendar();
+ }
+ if (this.calendar) {
+ // XXX This always gets called, does that happen on purpose?
+ this.calendar.removeObserver(this.calendarObserver);
+ }
+ this.calendar.addObserver(this.calendarObserver);
+ if (this.mListener) {
+ this.mListener.updatePeriod();
+ }
+};
+
+/**
+ * Refreshes the period dates, especially when a period is showing "today".
+ * Usually called at midnight to update the agenda pane. Also retrieves the
+ * items from the calendar.
+ *
+ * @see #refreshCalendarQuery
+ * @param newDate The first date to show if the agenda pane doesn't show
+ * today.
+ */
+agendaListbox.refreshPeriodDates = function(newDate) {
+ this.kDefaultTimezone = calendarDefaultTimezone();
+ // Today: now until midnight of tonight
+ let oldshowstoday = this.showstoday;
+ this.showstoday = this.showsToday(newDate);
+ if ((this.showstoday) && (!oldshowstoday)) {
+ this.addPeriodListItem(this.tomorrow, "tomorrow-header");
+ this.addPeriodListItem(this.soon, "nextweek-header");
+ } else if (!this.showstoday) {
+ this.removePeriodListItem(this.tomorrow);
+ this.removePeriodListItem(this.soon);
+ }
+ newDate.isDate = true;
+ for (let i = 0; i < this.periods.length; i++) {
+ let curPeriod = this.periods[i];
+ newDate.hour = newDate.minute = newDate.second = 0;
+ if (i == 0 && this.showstoday) {
+ curPeriod.start = now();
+ } else {
+ curPeriod.start = newDate.clone();
+ }
+ newDate.day += curPeriod.duration;
+ curPeriod.end = newDate.clone();
+ curPeriod.listItem.setItem(curPeriod, this.showstoday);
+ }
+ this.refreshCalendarQuery();
+};
+
+/**
+ * Adds a listener to this agenda listbox.
+ *
+ * @param aListener The listener to add.
+ */
+agendaListbox.addListener = function(aListener) {
+ this.mListener = aListener;
+};
+
+/**
+ * Checks if the agenda listbox is showing "today". Without arguments, this
+ * function assumes the today attribute of the agenda listbox.
+ *
+ * @param aStartDate (optional) The day to check if its "today".
+ * @return Returns true if today is shown.
+ */
+agendaListbox.showsToday = function(aStartDate) {
+ let lstart = aStartDate;
+ if (!lstart) {
+ lstart = this.today.start;
+ }
+ let lshowsToday = sameDay(now(), lstart);
+ if (lshowsToday) {
+ this.periods = [this.today, this.tomorrow, this.soon];
+ } else {
+ this.periods = [this.today];
+ }
+ return lshowsToday;
+};
+
+/**
+ * Moves the selection. Moves down unless the next item is a period item, in
+ * which case the selection moves up.
+ */
+agendaListbox.moveSelection = function() {
+ if (this.isEventListItem(this.agendaListboxControl.selectedItem.nextSibling)) {
+ this.agendaListboxControl.goDown();
+ } else {
+ this.agendaListboxControl.goUp();
+ }
+};
+
+/**
+ * Gets an array of selected items. If a period node is selected, it is not
+ * included.
+ *
+ * @return An array with all selected items.
+ */
+agendaListbox.getSelectedItems = function() {
+ let items = [];
+ if (this.isEventListItem(this.agendaListboxControl.selectedItem)) {
+ // If at some point we support selecting multiple items, this array can
+ // be expanded.
+ items = [this.agendaListboxControl.selectedItem.occurrence];
+ }
+ return items;
+};
+
+/**
+ * Checks if the passed node in the listbox is an Event item (not a
+ * period item).
+ *
+ * @param aListItem The node to check for.
+ * @return True, if the node is not a period item.
+ */
+agendaListbox.isEventListItem = function(aListItem) {
+ let isListItem = (aListItem != null);
+ if (isListItem) {
+ let localName = aListItem.localName;
+ isListItem = (localName == "agenda-richlist-item" ||
+ localName == "agenda-allday-richlist-item");
+ }
+ return isListItem;
+};
+
+/**
+ * Removes all Event items, keeping the period items intact.
+ */
+agendaListbox.removeListItems = function() {
+ let listItem = this.agendaListboxControl.lastChild;
+ if (listItem) {
+ let leaveloop = false;
+ do {
+ let newlistItem = null;
+ if (listItem) {
+ newlistItem = listItem.previousSibling;
+ } else {
+ leaveloop = true;
+ }
+ if (this.isEventListItem(listItem)) {
+ if (listItem == this.agendaListboxControl.firstChild) {
+ leaveloop = true;
+ } else {
+ listItem.remove();
+ }
+ }
+ listItem = newlistItem;
+ } while (!leaveloop);
+ }
+};
+
+/**
+ * Gets the list item node by its associated event's hashId.
+ *
+ * @return The XUL node if successful, otherwise null.
+ */
+agendaListbox.getListItemByHashId = function(ahashId) {
+ let listItem = this.agendaListboxControl.firstChild;
+ let leaveloop = false;
+ do {
+ if (this.isEventListItem(listItem)) {
+ if (listItem.occurrence.hashId == ahashId) {
+ return listItem;
+ }
+ }
+ listItem = listItem.nextSibling;
+ leaveloop = (listItem == null);
+ } while (!leaveloop);
+ return null;
+};
+
+/**
+ * The operation listener used for calendar queries.
+ * Implements calIOperationListener.
+ */
+agendaListbox.calendarOpListener = { agendaListbox: agendaListbox };
+
+/**
+ * Calendar and composite observer, used to keep agenda listbox up to date.
+ * @see calIObserver
+ * @see calICompositeObserver
+ */
+agendaListbox.calendarObserver = { agendaListbox: agendaListbox };
+
+agendaListbox.calendarObserver.QueryInterface = function(aIID) {
+ if (!aIID.equals(Components.interfaces.calIObserver) &&
+ !aIID.equals(Components.interfaces.calICompositeObserver) &&
+ !aIID.equals(Components.interfaces.nsISupports)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+};
+
+// calIObserver:
+agendaListbox.calendarObserver.onStartBatch = function() {
+};
+
+agendaListbox.calendarObserver.onEndBatch = function() {
+};
+
+agendaListbox.calendarObserver.onLoad = function() {
+ this.agendaListbox.refreshCalendarQuery();
+};
+
+agendaListbox.calendarObserver.onAddItem = function(item) {
+ if (!isEvent(item)) {
+ return;
+ }
+ // get all sub items if it is a recurring item
+ let occs = this.getOccurrencesBetween(item);
+ occs.forEach(this.agendaListbox.addItem, this.agendaListbox);
+ setCurrentEvent();
+};
+
+agendaListbox.calendarObserver.getOccurrencesBetween = function(aItem) {
+ let occs = [];
+ let start = this.agendaListbox.getStart();
+ let end = this.agendaListbox.getEnd();
+ if (start && end) {
+ occs = aItem.getOccurrencesBetween(start, end, {});
+ }
+ return occs;
+};
+
+agendaListbox.calendarObserver.onDeleteItem = function(item, rebuildFlag) {
+ this.onLocalDeleteItem(item, true);
+};
+
+agendaListbox.calendarObserver.onLocalDeleteItem = function(item, moveSelection) {
+ if (!isEvent(item)) {
+ return false;
+ }
+ let selectedItemHashId = -1;
+ // get all sub items if it is a recurring item
+ let occs = this.getOccurrencesBetween(item);
+ for (let i = 0; i < occs.length; i++) {
+ let isSelected = this.agendaListbox.deleteItem(occs[i], moveSelection);
+ if (isSelected) {
+ selectedItemHashId = occs[i].hashId;
+ }
+ }
+ return selectedItemHashId;
+};
+
+agendaListbox.calendarObserver.onModifyItem = function(newItem, oldItem) {
+ let selectedItemHashId = this.onLocalDeleteItem(oldItem, false);
+ if (!isEvent(newItem)) {
+ return;
+ }
+ this.onAddItem(newItem);
+ if (selectedItemHashId != -1) {
+ let listItem = agendaListbox.getListItemByHashId(selectedItemHashId);
+ if (listItem) {
+ agendaListbox.agendaListboxControl.clearSelection();
+ agendaListbox.agendaListboxControl.ensureElementIsVisible(listItem);
+ agendaListbox.agendaListboxControl.selectedItem = listItem;
+ }
+ }
+ setCurrentEvent();
+};
+
+agendaListbox.calendarObserver.onError = function(cal, errno, msg) {};
+
+agendaListbox.calendarObserver.onPropertyChanged = function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "disabled":
+ this.agendaListbox.refreshCalendarQuery();
+ break;
+ case "color":
+ for (let node = agendaListbox.agendaListboxControl.firstChild;
+ node;
+ node = node.nextSibling) {
+ // Change color on all nodes that don't do so themselves, which
+ // is currently only he agenda-richlist-item
+ if (node.localName != "agenda-richlist-item") {
+ continue;
+ }
+ node.refreshColor();
+ }
+ break;
+ }
+};
+
+agendaListbox.calendarObserver.onPropertyDeleting = function(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, null, null);
+};
+
+
+agendaListbox.calendarObserver.onCalendarRemoved = function(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.agendaListbox.deleteItemsFromCalendar(aCalendar);
+ }
+};
+
+agendaListbox.calendarObserver.onCalendarAdded = function(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.agendaListbox.refreshCalendarQuery(null, null, aCalendar);
+ }
+};
+
+agendaListbox.calendarObserver.onDefaultCalendarChanged = function(aCalendar) {
+};
+
+/**
+ * Updates the "Upcoming" section of today pane when preference soondays changes
+ **/
+agendaListbox.updateSoonSection = function() {
+ this.soon.duration = this.soonDays;
+ this.soon.open = true;
+ let soonHeader = document.getElementById("nextweek-header");
+ if (soonHeader) {
+ soonHeader.setItem(this.soon, true);
+ agendaListbox.refreshPeriodDates(now());
+ }
+};
+
+/**
+ * Updates the event considered "current". This goes through all "today" items
+ * and sets the "current" attribute on all list items that are currently
+ * occurring.
+ *
+ * @see scheduleNextCurrentEventUpdate
+ */
+function setCurrentEvent() {
+ if (!agendaListbox.showsToday() || !agendaListbox.today.open) {
+ return;
+ }
+
+ let msScheduleTime = -1;
+ let complistItem = agendaListbox.tomorrow.listItem.previousSibling;
+ let removelist = [];
+ let anow = now();
+ let msuntillend = 0;
+ let msuntillstart = 0;
+ let leaveloop;
+ do {
+ leaveloop = !agendaListbox.isEventListItem(complistItem);
+ if (!leaveloop) {
+ msuntillstart = complistItem.occurrence.startDate
+ .getInTimezone(agendaListbox.kDefaultTimezone)
+ .subtractDate(anow).inSeconds;
+ if (msuntillstart <= 0) {
+ msuntillend = complistItem.occurrence.endDate
+ .getInTimezone(agendaListbox.kDefaultTimezone)
+ .subtractDate(anow).inSeconds;
+ if (msuntillend > 0) {
+ complistItem.setAttribute("current", "true");
+ if (msuntillend < msScheduleTime || msScheduleTime == -1) {
+ msScheduleTime = msuntillend;
+ }
+ } else {
+ removelist.push(complistItem);
+ }
+ } else {
+ complistItem.removeAttribute("current");
+ }
+ if (msScheduleTime == -1 || msuntillstart < msScheduleTime) {
+ if (msuntillstart > 0) {
+ msScheduleTime = msuntillstart;
+ }
+ }
+ }
+ if (!leaveloop) {
+ complistItem = complistItem.previousSibling;
+ }
+ } while (!leaveloop);
+
+ if (msScheduleTime > -1) {
+ scheduleNextCurrentEventUpdate(setCurrentEvent, msScheduleTime * 1000);
+ }
+
+ if (removelist) {
+ if (removelist.length > 0) {
+ for (let i = 0; i < removelist.length; i++) {
+ removelist[i].remove();
+ }
+ }
+ }
+}
+
+var gEventTimer;
+
+/**
+ * Creates a timer that will fire after the next event is current.
+ * Pass in a function as aRefreshCallback that should be called at that time.
+ *
+ * @param aRefreshCallback The function to call when the next event is
+ * current.
+ * @param aMsUntil The number of milliseconds until the next event
+ * is current.
+ */
+function scheduleNextCurrentEventUpdate(aRefreshCallback, aMsUntil) {
+ // Is an nsITimer/callback extreme overkill here? Yes, but it's necessary to
+ // workaround bug 291386. If we don't, we stand a decent chance of getting
+ // stuck in an infinite loop.
+ let udCallback = {
+ notify: function(timer) {
+ aRefreshCallback();
+ }
+ };
+
+ if (gEventTimer) {
+ gEventTimer.cancel();
+ } else {
+ // Observer for wake after sleep/hibernate/standby to create new timers and refresh UI
+ let wakeObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "wake_notification") {
+ aRefreshCallback();
+ }
+ }
+ };
+ // Add observer
+ Services.obs.addObserver(wakeObserver, "wake_notification", false);
+
+ // Remove observer on unload
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(wakeObserver, "wake_notification");
+ }, false);
+
+ gEventTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ }
+ gEventTimer.initWithCallback(udCallback, aMsUntil, gEventTimer.TYPE_ONE_SHOT);
+}
+
+/**
+ * Gets a right value for calendar.agendaListbox.soondays preference, avoid
+ * erroneus values edited in the lightning.js preference file
+ **/
+function getSoondaysPreference() {
+ let prefName = "calendar.agendaListbox.soondays";
+ let soonpref = Preferences.get(prefName, 5);
+
+ if (soonpref > 0 && soonpref <= 28) {
+ if (soonpref % 7 != 0) {
+ let intSoonpref = Math.floor(soonpref / 7) * 7;
+ soonpref = (intSoonpref == 0 ? soonpref : intSoonpref);
+ Preferences.set(prefName, soonpref, "INT");
+ }
+ } else {
+ soonpref = soonpref > 28 ? 28 : 1;
+ Preferences.set(prefName, soonpref, "INT");
+ }
+ return soonpref;
+}
diff --git a/calendar/base/content/agenda-listbox.xml b/calendar/base/content/agenda-listbox.xml
new file mode 100644
index 000000000..bc205b7a0
--- /dev/null
+++ b/calendar/base/content/agenda-listbox.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="agenda-list-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="agenda-base-richlist-item"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <implementation>
+ <field name="mOccurrence">null</field>;
+ <property name="occurrence" readonly="true">
+ <getter><![CDATA[
+ return this.mOccurrence;
+ ]]></getter>
+ </property>
+ </implementation>
+
+ <handlers>
+ <handler event="click" phase="capturing"><![CDATA[
+ if (event.detail == 1) {
+ agendaListbox.onSelect(this);
+ } else if (event.button == 0) {
+ // We only care about button 0 doubleclick events
+ document.getElementById("agenda_edit_event_command").doCommand();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ ]]></handler>
+ <handler event="mouseover"><![CDATA[
+ event.stopPropagation();
+ onMouseOverItem(event);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="agenda-checkbox-richlist-item"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:treenode-checkbox class="agenda-checkbox" anonid="agenda-checkbox-widget"
+ flex="1"
+ xbl:inherits="selected,label,hidden,disabled"/>
+ </content>
+ <implementation>
+ <field name="kCheckbox">null</field>;
+ <constructor><![CDATA[
+ this.kCheckbox = document.getAnonymousElementByAttribute(this, "anonid", "agenda-checkbox-widget");
+ ]]></constructor>
+
+ <method name="getItem">
+ <body><![CDATA[
+ return this.mItem;
+ ]]></body>
+ </method>
+
+ <method name="setItem">
+ <parameter name="synthetic"/>
+ <parameter name="showsToday"/>
+ <body><![CDATA[
+ this.mItem = synthetic;
+ let duration = synthetic.duration;
+ if (showsToday) {
+ this.kCheckbox.label = this.getAttribute("title");
+ if (this.id == "nextweek-header") {
+ if (duration > 7) {
+ this.kCheckbox.label += " (" + unitPluralForm(duration / 7, "weeks") + ")";
+ } else {
+ this.kCheckbox.label += " (" + unitPluralForm(duration, "days") + ")";
+ }
+ }
+ } else if (synthetic.duration == 1) {
+ this.kCheckbox.label = getDateFormatter().formatDate(synthetic.start);
+ } else {
+ this.kCheckbox.label = getDateFormatter().formatInterval(synthetic.start, synthetic.end);
+ }
+ ]]></body>
+ </method>
+
+ <method name="getCheckbox">
+ <body><![CDATA[
+ return this.kCheckbox;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="agenda-allday-richlist-item"
+ extends="chrome://calendar/content/agenda-listbox.xml#agenda-base-richlist-item">
+ <content tooltip="itemTooltip">
+ <xul:hbox anonid="agenda-container-box"
+ class="agenda-allday-container-box"
+ xbl:inherits="selected,disabled"
+ flex="1">
+ <xul:vbox pack="center" flex="1">
+ <xul:label anonid="agenda-allDayEvent-date" class="agenda-event-start"
+ crop="end" xbl:inherits="selected" hidden="true"/>
+ <xul:hbox flex="1" align="start">
+ <xul:image anonid="agenda-multiDayEvent-image" class="agenda-multiDayEvent-image"/>
+ <xul:calendar-month-day-box-item anonid="allday-item"
+ flex="1"
+ flat="true"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <field name="mAllDayItem">null</field>;
+
+ <constructor><![CDATA[
+ this.mAllDayItem = document.getAnonymousElementByAttribute(this, "anonid", "allday-item");
+ ]]></constructor>
+
+ <method name="setOccurrence">
+ <parameter name="aOccurrence"/>
+ <parameter name="aPeriod"/>
+ <body><![CDATA[
+ this.mOccurrence = aOccurrence;
+ this.mAllDayItem.occurrence = aOccurrence;
+ let dateFormatter = cal.getDateFormatter();
+ let periodStartDate = aPeriod.start.clone();
+ periodStartDate.isDate = true;
+ let periodEndDate = aPeriod.end;
+ let startDate = this.mOccurrence[calGetStartDateProp(this.mOccurrence)]
+ .getInTimezone(calendarDefaultTimezone());
+ let endDate = this.mOccurrence[calGetEndDateProp(this.mOccurrence)]
+ .getInTimezone(calendarDefaultTimezone());
+ let endPreviousDay = endDate.clone();
+ endPreviousDay.day--;
+ // Show items's date for long periods but also for "Upcoming"
+ // period with one day duration.
+ let showDate = aPeriod.multiday || aPeriod.duration > 1;
+
+ let date = "";
+ let iconType = "";
+ let allDayDateLabel = document.getAnonymousElementByAttribute(this, "anonid", "agenda-allDayEvent-date");
+ setBooleanAttribute(allDayDateLabel, "hidden", !showDate);
+ if (startDate.compare(endPreviousDay) == 0) {
+ // All day event one day duration.
+ date = dateFormatter.formatDate(startDate);
+ } else if (startDate.compare(periodStartDate) >= 0 &&
+ startDate.compare(periodEndDate) <= 0) {
+ // All day event spanning multiple days.
+ iconType = "start";
+ date = dateFormatter.formatDate(startDate);
+ } else if (endDate.compare(periodStartDate) >= 0 &&
+ endDate.compare(periodEndDate) <= 0) {
+ iconType = "end";
+ date = dateFormatter.formatDate(endPreviousDay);
+ } else {
+ iconType = "continue";
+ hideElement(allDayDateLabel);
+ }
+
+ let multiDayImage = document.getAnonymousElementByAttribute(this, "anonid", "agenda-multiDayEvent-image");
+ multiDayImage.setAttribute("type", iconType);
+ // class wrap causes allday items to wrap its text in today-pane
+ let addWrap = document.getAnonymousElementByAttribute(this.mAllDayItem, "anonid", "eventbox");
+ addWrap.classList.add("wrap");
+ addWrap = document.getAnonymousElementByAttribute(this.mAllDayItem, "anonid", "event-detail-box");
+ addWrap.classList.add("wrap");
+ allDayDateLabel.value = date;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="dragstart" phase="capturing"><![CDATA[
+ invokeEventDragSession(this.mAllDayItem.occurrence.clone(), this);
+ event.stopPropagation();
+ event.preventDefault();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="agenda-richlist-item"
+ extends="chrome://calendar/content/agenda-listbox.xml#agenda-base-richlist-item">
+ <content tooltip="itemTooltip">
+ <xul:hbox anonid="agenda-container-box" class="agenda-container-box" xbl:inherits="selected,disabled,current" flex="1">
+ <xul:hbox>
+ <xul:vbox>
+ <xul:image anonid="agenda-calendar-image" class="agenda-calendar-image"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ <xul:vbox anonid="agenda-description" flex="1">
+ <xul:hbox align="start">
+ <xul:image anonid="agenda-multiDayEvent-image" class="agenda-multiDayEvent-image"/>
+ <xul:label anonid="agenda-event-start" class="agenda-event-start" crop="end" flex="1" xbl:inherits="selected"/>
+ </xul:hbox>
+ <xul:label anonid="agenda-event-title" class="agenda-event-title" crop="end" xbl:inherits="selected"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="setOccurrence">
+ <parameter name="aItem"/>
+ <parameter name="aPeriod"/>
+ <body><![CDATA[
+ this.mOccurrence = aItem;
+ this.setAttribute("status", aItem.status);
+ let dateFormatter = Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+
+ let periodStartDate = aPeriod.start.clone();
+ periodStartDate.isDate = true;
+ let periodEndDate = aPeriod.end.clone();
+ periodEndDate.day--;
+ let start = this.mOccurrence[calGetStartDateProp(this.mOccurrence)]
+ .getInTimezone(calendarDefaultTimezone());
+ let end = this.mOccurrence[calGetEndDateProp(this.mOccurrence)]
+ .getInTimezone(calendarDefaultTimezone());
+ let startDate = start.clone();
+ startDate.isDate = true;
+ let endDate = end.clone();
+ endDate.isDate = true;
+ let endAtMidnight = (end.hour == 0 && end.minute == 0);
+ if (endAtMidnight) {
+ endDate.day--;
+ }
+ // Show items's date for long periods but also for "Upcoming"
+ // period with one day duration.
+ let longFormat = aPeriod.multiday || aPeriod.duration > 1;
+
+ let duration = "";
+ let iconType = "";
+ if (startDate.compare(endDate) == 0) {
+ // event that starts and ends in the same day, midnight included
+ duration = longFormat ? dateFormatter.formatDateTime(start)
+ : dateFormatter.formatTime(start);
+ } else if (startDate.compare(periodStartDate) >= 0 &&
+ startDate.compare(periodEndDate) <= 0) {
+ // event spanning multiple days, start date within period
+ iconType = "start";
+ duration = longFormat ? dateFormatter.formatDateTime(start)
+ : dateFormatter.formatTime(start);
+ } else if (endDate.compare(periodStartDate) >= 0 &&
+ endDate.compare(periodEndDate) <= 0) {
+ // event spanning multiple days, end date within period
+ iconType = "end";
+ if (endAtMidnight) {
+ duration = dateFormatter.formatDate(endDate) + " ";
+ duration = longFormat ? duration + calGetString("dateFormat", "midnight")
+ : calGetString("dateFormat", "midnight");
+ } else {
+ duration = longFormat ? dateFormatter.formatDateTime(end)
+ : dateFormatter.formatTime(end);
+ }
+ } else {
+ iconType = "continue";
+ }
+ let multiDayImage = document.getAnonymousElementByAttribute(this, "anonid", "agenda-multiDayEvent-image");
+ multiDayImage.setAttribute("type", iconType);
+ let durationbox = document.getAnonymousElementByAttribute(this, "anonid", "agenda-event-start");
+ durationbox.textContent = duration;
+
+ // show items with time only (today & tomorrow) as one line.
+ if (longFormat) {
+ let titlebox = document.getAnonymousElementByAttribute(this, "anonid", "agenda-event-title");
+ titlebox.textContent = aItem.title;
+ } else {
+ durationbox.textContent += " " + aItem.title;
+ }
+ this.refreshColor();
+ ]]></body>
+ </method>
+
+ <method name="refreshColor">
+ <body><![CDATA[
+ let calcolor = (this.mOccurrence &&
+ this.mOccurrence.calendar.getProperty("color")) ||
+ "#a8c2e1";
+
+ let imagebox = document.getAnonymousElementByAttribute(this, "anonid", "agenda-calendar-image");
+ imagebox.setAttribute("style", "background-color: " + calcolor + ";");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="dragstart"><![CDATA[
+ invokeEventDragSession(this.mOccurrence.clone(), this);
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-base-view.xml b/calendar/base/content/calendar-base-view.xml
new file mode 100644
index 000000000..bd7e72d0d
--- /dev/null
+++ b/calendar/base/content/calendar-base-view.xml
@@ -0,0 +1,924 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="calendar-multiday-view-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="calendar-base-view">
+ <resources>
+ <stylesheet src="chrome://calendar-common/skin/calendar-alarms.css"/>
+ </resources>
+ <implementation>
+ <field name="mWeekStartOffset">0</field>
+ <field name="mRangeStartDate">null</field>;
+ <field name="mRangeEndDate">null</field>;
+ <field name="mWorkdaysOnly">false</field>
+ <field name="mPendingRefreshJobs">null</field>
+ <field name="mCalendar">null</field>
+ <field name="mController">null</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mTasksInView">false</field>
+ <field name="mShowCompleted">false</field>
+ <field name="mDisplayDaysOff">true</field>
+ <field name="mDaysOffArray">[0, 6]</field>
+ <field name="mTimezone">null</field>
+ <field name="mFlashingEvents">null</field>
+ <field name="mSelectedItems">[]</field>
+ <field name="mLongWeekdayTotalPixels">-1</field>
+ <field name="mResizeHandler">null</field>
+ <field name="mDropShadowsLength">null</field>
+ <field name="mShadowOffset">null</field>
+ <field name="mDropShadows">null</field>
+ <field name="mMagnifyAmount">0</field>
+ <field name="mPixelScrollDelta">0</field>
+ <field name="mViewStart">null</field>
+ <field name="mViewEnd">null</field>
+ <field name="mToggleStatus">0</field>
+ <field name="mLog">null</field>
+ <field name="mToggleStatusFlag"><![CDATA[
+ ({
+ WorkdaysOnly: 1,
+ TasksInView: 2,
+ ShowCompleted: 4,
+ })
+ ]]></field>
+
+ <field name="mPrefObserver"><![CDATA[
+ ({
+ calView: this,
+ observe: function(subj, topic, pref) {
+ this.calView.handlePreference(subj, topic, pref);
+ return;
+ }
+ })
+ ]]></field>
+
+ <field name="mObserver"><![CDATA[
+ // the calIObserver, calICompositeObserver, and calIAlarmServiceObserver
+ ({
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calIObserver,
+ Components.interfaces.calIAlarmServiceObserver,
+ Components.interfaces.calICompositeObserver
+ ]),
+
+ calView: this,
+
+ onStartBatch: function() {
+ },
+ onEndBatch: function() {
+ },
+
+ onLoad: function() {
+ this.calView.refresh();
+ },
+
+ onAddItem: function(aItem) {
+ if (cal.isToDo(aItem)) {
+ if (!aItem.entryDate && !aItem.dueDate) {
+ return;
+ }
+ if (!this.calView.mTasksInView) {
+ return;
+ }
+ if (aItem.isCompleted && !this.calView.mShowCompleted) {
+ return;
+ }
+ }
+
+ let occs = aItem.getOccurrencesBetween(this.calView.startDate,
+ this.calView.queryEndDate,
+ {});
+ for (let occ of occs) {
+ this.calView.doAddItem(occ);
+ }
+ return;
+ },
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (cal.isToDo(aNewItem) && cal.isToDo(aOldItem) &&
+ !this.calView.mTasksInView) {
+ return;
+ }
+ let occs;
+
+ if (!cal.isToDo(aOldItem) || aOldItem.entryDate || aOldItem.dueDate) {
+ occs = aOldItem.getOccurrencesBetween(this.calView.startDate,
+ this.calView.queryEndDate,
+ {});
+ for (let occ of occs) {
+ this.calView.doDeleteItem(occ);
+ }
+ }
+ if (cal.isToDo(aNewItem)) {
+ if ((!aNewItem.entryDate && !aNewItem.dueDate) || !this.calView.mTasksInView) {
+ return;
+ }
+ if (aNewItem.isCompleted && !this.calView.mShowCompleted) {
+ return;
+ }
+ }
+
+ occs = aNewItem.getOccurrencesBetween(this.calView.startDate,
+ this.calView.queryEndDate,
+ {});
+ for (let occ of occs) {
+ this.calView.doAddItem(occ);
+ }
+ },
+
+ onDeleteItem: function(aItem) {
+ if (cal.isToDo(aItem)) {
+ if (!this.calView.mTasksInView) {
+ return;
+ }
+ if (!aItem.entryDate && !aItem.dueDate) {
+ return;
+ }
+ if (aItem.isCompleted && !this.calView.mShowCompleted) {
+ return;
+ }
+ }
+
+ let occs = aItem.getOccurrencesBetween(this.calView.startDate,
+ this.calView.queryEndDate,
+ {});
+ for (let occ of occs) {
+ this.calView.doDeleteItem(occ);
+ }
+ },
+
+ onError: function(aCalendar, aErrNo, aMessage) { },
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "suppressAlarms":
+ if (!Preferences.get("calendar.alarms.indicator.show", true) ||
+ aCalendar.getProperty("capabilities.alarms.popup.supported") === false) {
+ break;
+ }
+ // else fall through
+ case "readOnly":
+ case "disabled":
+ // XXXvv we can be smarter about how we handle this stuff
+ this.calView.refresh();
+ break;
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ // Values are not important here yet.
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ },
+
+ //
+ // calIAlarmServiceObserver stuff
+ //
+ onAlarm: function(aAlarmItem) {
+ this.calView.flashAlarm(aAlarmItem, false);
+ },
+
+ onRemoveAlarmsByItem: function(aItem) {
+ // Stop the flashing for the item.
+ this.calView.flashAlarm(aItem, true);
+ },
+
+ onRemoveAlarmsByCalendar: function(aCalendar) {
+ // Stop the flashing for all items of this calendar
+ for (let key in this.calView.mFlashingEvents) {
+ let item = this.calView.mFlashingEvents[key];
+ if (item.calendar.id == aCalendar.id) {
+ this.calView.flashAlarm(item, true);
+ }
+ }
+ },
+
+ onAlarmsLoaded: function(aCalendar) {},
+
+ //
+ // calICompositeObserver stuff
+ // XXXvv we can be smarter about how we handle this stuff
+ //
+ onCalendarAdded: function(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.calView.addItemsFromCalendar(aCalendar);
+ }
+ },
+
+ onCalendarRemoved: function(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.calView.deleteItemsFromCalendar(aCalendar);
+ }
+ },
+
+ onDefaultCalendarChanged: function(aNewDefaultCalendar) {
+ // don't care, for now
+ }
+ })
+ ]]></field>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+ Components.utils.import("resource:///modules/gloda/log4moz.js");
+
+ const kWorkdaysCommand = "calendar_toggle_workdays_only_command";
+ const kTasksInViewCommand = "calendar_toggle_tasks_in_view_command";
+ const kShowCompleted = "calendar_toggle_show_completed_in_view_command";
+ const kOrientation = "calendar_toggle_orientation_command";
+
+ this.workdaysOnly = (document.getElementById(kWorkdaysCommand)
+ .getAttribute("checked") == "true");
+ this.tasksInView = (document.getElementById(kTasksInViewCommand)
+ .getAttribute("checked") == "true");
+ this.rotated = (document.getElementById(kOrientation)
+ .getAttribute("checked") == "true");
+ this.showCompleted = (document.getElementById(kShowCompleted)
+ .getAttribute("checked") == "true");
+
+ this.mTimezone = calendarDefaultTimezone();
+ let alarmService = Components.classes["@mozilla.org/calendar/alarm-service;1"]
+ .getService(Components.interfaces.calIAlarmService);
+ alarmService.addObserver(this.mObserver);
+ this.setAttribute("type", this.type);
+ this.mResizeHandler = () => {
+ this.onResize(this);
+ };
+ this.viewBroadcaster.addEventListener(this.getAttribute("type") + "viewresized", this.mResizeHandler, true);
+ // add a preference observer to monitor changes
+ Services.prefs.addObserver("calendar.", this.mPrefObserver, false);
+ this.weekStartOffset = Preferences.get("calendar.week.start", 0);
+ this.updateDaysOffPrefs();
+ this.mPendingRefreshJobs = new Map();
+ this.mLog = Log4Moz.getConfiguredLogger("calBaseView");
+ this.mFlashingEvents = {};
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ if (this.mCalendar) {
+ this.mCalendar.removeObserver(this.mObserver);
+ }
+ let alarmService = Components.classes["@mozilla.org/calendar/alarm-service;1"]
+ .getService(Components.interfaces.calIAlarmService);
+ alarmService.removeObserver(this.mObserver);
+ this.viewBroadcaster.removeEventListener(this.getAttribute("type") + "viewresized", this.mResizeHandler, true);
+ Services.prefs.removeObserver("calendar.", this.mPrefObserver);
+ ]]></destructor>
+
+ <property name="type" readonly="true">
+ <getter><![CDATA[
+ let typelist = this.id.split("-");
+ return typelist[0];
+ ]]></getter>
+ </property>
+
+ <property name="viewBroadcaster" readonly="true"
+ onget="return document.getElementById('calendarviewBroadcaster')"/>
+
+ <property name="labeldaybox" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'labeldaybox')"/>
+
+ <property name="rotated"
+ onget="return (this.orient == 'horizontal')"
+ onset="return (this.orient = (val ? 'horizontal' : 'vertical'))"/>
+ <property name="supportsRotation" readonly="true"
+ onget="return false"/>
+ <property name="displayDaysOff"
+ onget="return this.mDisplayDaysOff;"
+ onset="return (this.mDisplayDaysOff = val);"/>
+ <property name="controller"
+ onget="return this.mController;"
+ onset="return (this.mController = val);"/>
+ <property name="daysOffArray"
+ onget="return this.mDaysOffArray;"
+ onset="return (this.mDaysOffArray = val);"/>
+ <property name="tasksInView"
+ onget="return this.mTasksInView;"
+ onset="return (this.mTasksInView = val);"/>
+ <property name="showCompleted"
+ onget="return this.mShowCompleted;"
+ onset="return (this.mShowCompleted = val);"/>
+ <property name="timezone"
+ onget="return this.mTimezone;"
+ onset="return (this.mTimezone = val);"/>
+ <property name="workdaysOnly"
+ onget="return this.mWorkdaysOnly;"
+ onset="return (this.mWorkdaysOnly = val);"/>
+ <property name="supportsWorkdaysOnly" readonly="true"
+ onget="return true;"/>
+ <property name="supportsZoom" readonly="true"
+ onget="return false;"/>
+ <property name="selectionObserver" readonly="true"
+ onget="return this.mSelectionObserver;"/>
+ <property name="startDay" readonly="true"
+ onget="return this.startDate;"/>
+ <property name="endDay" readonly="true"
+ onget="return this.endDate;"/>
+ <property name="supportDisjointDates" readonly="true"
+ onget="return false;"/>
+ <property name="hasDisjointDates" readonly="true"
+ onget="return false;"/>
+ <property name="rangeStartDate"
+ onget="return this.mRangeStartDate;"
+ onset="return (this.mRangeStartDate = val);"/>
+ <property name="rangeEndDate"
+ onget="return this.mRangeEndDate;"
+ onset="return (this.mRangeEndDate = val);"/>
+ <property name="observerID" readonly="true"
+ onget="return 'base-view-observer';"/>
+
+ <property name="weekStartOffset">
+ <getter><![CDATA[
+ return this.mWeekStartOffset;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mWeekStartOffset = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="displayCalendar">
+ <getter><![CDATA[
+ return this.mCalendar;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.mCalendar) {
+ this.mCalendar.removeObserver(this.mObserver);
+ }
+ this.mCalendar = val;
+ this.mCalendar.addObserver(this.mObserver);
+ this.refresh();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="initialized">
+ <getter><![CDATA[
+ let retval;
+
+ // Some views throw when accessing an uninitialized startDay
+ try {
+ retval = this.displayCalendar && this.startDay &&
+ this.endDay;
+ } catch (ex) {
+ return false;
+ }
+ return retval;
+ ]]></getter>
+ </property>
+
+ <method name="goToDay">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this.showDate(aDate);
+ ]]></body>
+ </method>
+
+ <method name="getRangeDescription">
+ <body><![CDATA[
+ return getDateFormatter().formatInterval(this.rangeStartDate, this.rangeEndDate);
+ ]]></body>
+ </method>
+
+ <!-- This function guarantees that the
+ labels are clipped in the instance that the overflow occurrs,
+ avoiding horizontal scrollbars from showing up for a short period. -->
+ <method name="adjustWeekdayLength">
+ <parameter name="forceShortName"/>
+ <body><![CDATA[
+ let useShortNames = false;
+ let labeldayboxkids = this.labeldaybox.childNodes;
+ if (!labeldayboxkids || !labeldayboxkids[0]) {
+ useShortNames = true;
+ } else if (forceShortName && forceShortName === true) {
+ useShortNames = true;
+ } else {
+ let clientWidth = document.getAnonymousElementByAttribute(this, "anonid", "mainbox").clientWidth;
+ let timespacer = document.getAnonymousElementByAttribute(this, "anonid", "headertimespacer");
+ if (timespacer) {
+ clientWidth -= timespacer.clientWidth;
+ }
+ if (this.getLongWeekdayTotalPixels() > 0.95 * clientWidth) {
+ useShortNames = true;
+ }
+ }
+ for (let i = 0; i < labeldayboxkids.length; i++) {
+ labeldayboxkids[i].shortWeekNames = useShortNames;
+ }
+ ]]></body>
+ </method>
+
+ <method name="today">
+ <body><![CDATA[
+ let date = cal.jsDateToDateTime(new Date()).getInTimezone(this.mTimezone);
+ date.isDate = true;
+ return date;
+ ]]></body>
+ </method>
+
+ <method name="isVisible">
+ <body><![CDATA[
+ return (this.nodeName == currentView().nodeName);
+ ]]></body>
+ </method>
+
+ <method name="refresh">
+ <body><![CDATA[
+ if (this.isVisible()) {
+ this.addItemsFromCalendar(this.mCalendar);
+ }
+ ]]></body>
+ </method>
+
+ <!-- force refresh visible and invisible views.
+ This method is added because when only a preference is toggled, the start
+ and end date of views are unchanged, therefore those views behind the
+ "scene" might stay the same upon switch to them. -->
+ <method name="forceRefresh">
+ <body><![CDATA[
+ this.addItemsFromCalendar(this.mCalendar);
+ ]]></body>
+ </method>
+
+ <method name="addItemsFromCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let refreshJob = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ calView: this,
+ calendar: null,
+ calId: null,
+ operation: null,
+ cancelled: false,
+
+ onOperationComplete: function(aOpCalendar, aStatus, aOperationType, aId, aDateTime) {
+ this.calView.mLog.info("Refresh complete of calendar " + this.calId);
+ if (this.calView.mPendingRefreshJobs.has(this.calId)) {
+ this.calView.mPendingRefreshJobs.delete(this.calId);
+ }
+
+ if (!this.cancelled) {
+ // Fire event
+ this.calView.fireEvent("viewloaded", aOperationType);
+ }
+ },
+
+ onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (this.cancelled || !Components.isSuccessCode(aStatus)) {
+ return;
+ }
+
+ for (let item of aItems) {
+ if (!cal.isToDo(item) || item.entryDate || item.dueDate) {
+ this.calView.doAddItem(item);
+ }
+ }
+ },
+
+ cancel: function() {
+ this.calView.mLog.info("Refresh cancelled for calendar " + this.calId);
+ this.cancelled = true;
+ let operation = cal.wrapInstance(this.operation, Components.interfaces.calIOperation);
+ if (operation && operation.isPending) {
+ operation.cancel();
+ this.operation = null;
+ }
+ },
+
+ execute: function() {
+ if (!this.calView.startDate || !this.calView.endDate || !aCalendar) {
+ return;
+ }
+
+ if (aCalendar.type == "composite") {
+ // we're refreshing from the composite calendar, so we can cancel
+ // all other pending refresh jobs.
+ this.calView.mLog.info("Refreshing composite calendar, cancelling all pending refreshes");
+ this.calId = "composite";
+ for (let job of this.calView.mPendingRefreshJobs.values()) {
+ job.cancel();
+ }
+ this.calView.mPendingRefreshJobs.clear();
+ this.calView.relayout();
+ } else {
+ this.calId = aCalendar.id;
+ if (this.calView.mPendingRefreshJobs.has(this.calId)) {
+ this.calView.mPendingRefreshJobs.get(this.calId).cancel();
+ this.calView.mPendingRefreshJobs.delete(this.calId);
+ }
+ }
+ this.calendar = aCalendar;
+
+ // start our items query; for a disjoint date range
+ // we get all the items, and just filter out the ones we don't
+ // care about in addItem
+ let filter = this.calendar.ITEM_FILTER_CLASS_OCCURRENCES;
+ if (this.calView.mShowCompleted) {
+ filter |= this.calendar.ITEM_FILTER_COMPLETED_ALL;
+ } else {
+ filter |= this.calendar.ITEM_FILTER_COMPLETED_NO;
+ }
+
+ if (this.calView.mTasksInView) {
+ filter |= this.calendar.ITEM_FILTER_TYPE_ALL;
+ } else {
+ filter |= this.calendar.ITEM_FILTER_TYPE_EVENT;
+ }
+
+ let operation = this.calendar.getItems(filter,
+ 0,
+ this.calView.startDate,
+ this.calView.queryEndDate,
+ this);
+ operation = cal.wrapInstance(operation, Components.interfaces.calIOperation);
+ if (operation && operation.isPending) {
+ this.operation = operation;
+ this.calView.mPendingRefreshJobs.set(this.calId, this);
+ }
+ }
+ };
+
+ refreshJob.execute();
+ ]]></body>
+ </method>
+
+ <method name="deleteItemsFromCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ /* This method must be implemented in subclasses. */
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ ]]></body>
+ </method>
+
+ <!-- the end date that should be used for getItems and similar queries -->
+ <property name="queryEndDate" readonly="true">
+ <getter><![CDATA[
+ let end = this.endDate;
+ if (!end) {
+ return null;
+ }
+ end = end.clone();
+ end.day += 1;
+ end.isDate = true;
+ return end;
+ ]]></getter>
+ </property>
+
+ <method name="fireEvent">
+ <parameter name="aEventName"/>
+ <parameter name="aEventDetail"/>
+ <body><![CDATA[
+ let event = document.createEvent("Events");
+ event.initEvent(aEventName, true, false);
+ event.detail = aEventDetail;
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="removeDropShadows">
+ <body><![CDATA[
+ let dropbox = document.getAnonymousElementByAttribute(this, "dropbox", "true");
+ if (dropbox && dropbox !== undefined) {
+ dropbox.setAttribute("dropbox", "false");
+ }
+ ]]></body>
+ </method>
+
+ <method name="getLongWeekdayTotalPixels">
+ <body><![CDATA[
+ if (this.mLongWeekdayTotalPixels <= 0) {
+ let maxDayWidth = 0;
+ for (let label of this.labeldaybox.childNodes) {
+ if (label.localName && label.localName == "calendar-day-label") {
+ label.shortWeekNames = false;
+ let curPixelLength = label.getLongWeekdayPixels();
+ maxDayWidth = Math.max(maxDayWidth, curPixelLength);
+ }
+ }
+ if (maxDayWidth > 0) {
+ this.mLongWeekdayTotalPixels = (maxDayWidth * this.labeldaybox.childNodes.length) + 10;
+ }
+ }
+ return this.mLongWeekdayTotalPixels;
+ ]]></body>
+ </method>
+
+ <!-- A preference handler which is called by the preference observer.
+ Can be overwritten in derived bindings. -->
+ <method name="handleCommonPreference">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aPreference"/>
+ <body><![CDATA[
+ // refresh view if categories seem to have changed
+ if (aPreference.startsWith("calendar.category.color")) {
+ this.refreshView();
+ return;
+ }
+ switch (aPreference) {
+ case "calendar.week.d0sundaysoff":
+ case "calendar.week.d1mondaysoff":
+ case "calendar.week.d2tuesdaysoff":
+ case "calendar.week.d3wednesdaysoff":
+ case "calendar.week.d4thursdaysoff":
+ case "calendar.week.d5fridaysoff":
+ case "calendar.week.d6saturdaysoff":
+ this.updateDaysOffPrefs();
+ break;
+ case "calendar.timezone.local":
+ this.timezone = calendarDefaultTimezone();
+ this.refreshView();
+ break;
+ case "calendar.alarms.indicator.show":
+ // Break here to ensure the view is refreshed
+ break;
+ case "calendar.week.start":
+ this.weekStartOffset = aSubject.getIntPref(aPreference);
+ break;
+ case "calendar.date.format":
+ this.refreshView();
+ break;
+ default:
+ return;
+ }
+ this.refreshView();
+ ]]></body>
+ </method>
+
+ <method name="updateDaysOffPrefs">
+ <body><![CDATA[
+ const weekPrefix = "calendar.week.";
+ const prefNames = ["d0sundaysoff", "d1mondaysoff", "d2tuesdaysoff",
+ "d3wednesdaysoff", "d4thursdaysoff",
+ "d5fridaysoff", "d6saturdaysoff"];
+ const defaults = ["true", "false", "false", "false",
+ "false", "false", "true"];
+ let daysOff = [];
+ for (let i in prefNames) {
+ if (Preferences.get(weekPrefix + prefNames[i], defaults[i])) {
+ daysOff.push(Number(i));
+ }
+ }
+ this.daysOffArray = daysOff;
+ ]]></body>
+ </method>
+
+ <method name="refreshView">
+ <body><![CDATA[
+ if (!this.startDay || !this.endDay) {
+ // don't refresh if we're not initialized
+ return;
+ }
+ // Just refresh, the goToDay function will notice
+ this.goToDay(this.selectedDay);
+ this.forceRefresh();
+ ]]></body>
+ </method>
+
+ <!-- Default implementations follow, these make things easier for
+ extensions that don't need certain features. -->
+ <method name="handlePreference">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aPref"/>
+ <body><![CDATA[
+ // Do nothing by default
+ ]]></body>
+ </method>
+ <method name="setDateRange">
+ <parameter name="aStartDate"/>
+ <parameter name="aEndDate"/>
+ <body><![CDATA[
+ cal.navigationBar.setDateRange(aStartDate, aEndDate);
+ ]]></body>
+ </method>
+
+ <property name="selectedDay"
+ onget="return this.startDate"
+ onset="return this.startDate"/>
+
+ <method name="getSelectedItems">
+ <parameter name="aCount"/>
+ <body><![CDATA[
+ aCount.value = this.mSelectedItems.length;
+ return this.mSelectedItems;
+ ]]></body>
+ </method>
+ <method name="setSelectedItems">
+ <parameter name="aCount"/>
+ <parameter name="aItems"/>
+ <body><![CDATA[
+ this.mSelectedItems = aItems.concat([]);
+ return this.mSelectedItems;
+ ]]></body>
+ </method>
+
+ <method name="getDateList">
+ <parameter name="aCount"/>
+ <parameter name="aDates"/>
+ <body><![CDATA[
+ let start = this.startDate.clone();
+ while (start.compare(this.endDate) <= 0) {
+ dates.push(start);
+ start.day++;
+ }
+ aCount.value = dates.length;
+ return dates;
+ ]]></body>
+ </method>
+
+ <method name="flashAlarm">
+ <parameter name="aAlarmItem"/>
+ <parameter name="aStop"/>
+ <body><![CDATA[
+ // Do nothing by default
+ ]]></body>
+ </method>
+
+ <method name="zoomIn">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+ <method name="zoomOut">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+ <method name="zoomReset">
+ <body><![CDATA[
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="move"><![CDATA[
+ this.moveView(event.detail);
+ ]]></handler>
+ <handler event="keypress"><![CDATA[
+ const kKE = Components.interfaces.nsIDOMKeyEvent;
+ switch (event.keyCode) {
+ case kKE.DOM_VK_PAGE_UP:
+ this.moveView(-1);
+ break;
+ case kKE.DOM_VK_PAGE_DOWN:
+ this.moveView(1);
+ break;
+ }
+ ]]></handler>
+ <handler event="wheel"><![CDATA[
+ const pixelThreshold = 150;
+ if (event.shiftKey && Preferences.get("calendar.view.mousescroll", true)) {
+ if (event.deltaMode == event.DOM_DELTA_LINE) {
+ if (event.deltaY != 0) {
+ deltaView = event.deltaY < 0 ? -1 : 1;
+ }
+ } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
+ this.mPixelScrollDelta += event.deltaY;
+ if (this.mPixelScrollDelta > pixelThreshold) {
+ deltaView = 1;
+ this.mPixelScrollDelta = 0;
+ } else if (this.mPixelScrollDelta < -pixelThreshold) {
+ deltaView = -1;
+ this.mPixelScrollDelta = 0;
+ }
+ }
+
+ if (deltaView != 0) {
+ this.moveView(deltaView);
+ }
+ }
+
+ // Prevent default scroll handling
+ event.preventDefault();
+ ]]></handler>
+ <handler event="MozRotateGesture"><![CDATA[
+ // Threshold for the minimum and maximum angle we should accept
+ // rotation for. 90 degrees minimum is most logical, but 45 degrees
+ // allows you to rotate with one hand.
+ const MIN_ROTATE_ANGLE = 45;
+ const MAX_ROTATE_ANGLE = 180;
+
+ let absval = Math.abs(event.delta);
+ if (this.supportsRotation &&
+ absval >= MIN_ROTATE_ANGLE &&
+ absval < MAX_ROTATE_ANGLE) {
+ toggleOrientation();
+ event.preventDefault();
+ }
+ ]]></handler>
+ <handler event="MozMagnifyGestureStart"><![CDATA[
+ this.mMagnifyAmount = 0;
+ ]]></handler>
+ <handler event="MozMagnifyGestureUpdate"><![CDATA[
+ // Threshold as to how much magnification causes the zoom to happen
+ const THRESHOLD = 30;
+
+ if (this.supportsZoom) {
+ this.mMagnifyAmount += event.delta;
+
+ if (this.mMagnifyAmount > THRESHOLD) {
+ this.zoomOut();
+ this.mMagnifyAmount = 0;
+ } else if (this.mMagnifyAmount < -THRESHOLD) {
+ this.zoomIn();
+ this.mMagnifyAmount = 0;
+ }
+ event.preventDefault();
+ }
+ ]]></handler>
+ <handler event="MozSwipeGesture"><![CDATA[
+ if ((event.direction == SimpleGestureEvent.DIRECTION_UP && !this.rotated) ||
+ (event.direction == SimpleGestureEvent.DIRECTION_LEFT && this.rotated)) {
+ this.moveView(-1);
+ } else if ((event.direction == SimpleGestureEvent.DIRECTION_DOWN && !this.rotated) ||
+ (event.direction == SimpleGestureEvent.DIRECTION_RIGHT && this.rotated)) {
+ this.moveView(1);
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-day-label" extends="xul:box">
+ <content flex="1" pack="center">
+ <xul:label anonid="longWeekdayName" class="calendar-day-label-name" xbl:inherits="selected,relation"/>
+ <xul:label anonid="shortWeekdayName" class="calendar-day-label-name" hidden="true" xbl:inherits="selected,relation"/>
+ </content>
+ <implementation>
+ <field name="mWeekday">-1</field>
+ <field name="longWeekdayPixels">0</field>
+ <field name="mDate">null</field>
+ <property name="longName" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'longWeekdayName');"/>
+ <property name="shortName" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'shortWeekdayName');"/>
+ <property name="weekDay">
+ <getter>return this.mWeekday;</getter>
+ <setter><![CDATA[
+ this.mWeekday = val % 7;
+ this.longName.value = getDateFormatter().dayName(val);
+ this.shortName.value = getDateFormatter().shortDayName(val);
+ return this.mWeekday;
+ ]]></setter>
+ </property>
+
+ <property name="date">
+ <getter><![CDATA[
+ return this.mDate;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mDate = val;
+ let dateFormatter = cal.getDateFormatter();
+ let label = cal.calGetString("calendar", "dayHeaderLabel",
+ [dateFormatter.shortDayName(val.weekday),
+ dateFormatter.formatDateWithoutYear(val)]);
+ this.shortName.setAttribute("value", label);
+ label = cal.calGetString("calendar", "dayHeaderLabel",
+ [dateFormatter.dayName(val.weekday),
+ dateFormatter.formatDateWithoutYear(val)]);
+ this.longName.setAttribute("value", label);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="shortWeekNames">
+ <getter><![CDATA[
+ ]]></getter>
+ <setter><![CDATA[
+ // cache before change, in case we are switching to short
+ this.getLongWeekdayPixels();
+ setBooleanAttribute(this.longName, "hidden", val);
+ setBooleanAttribute(this.shortName, "hidden", !val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="getLongWeekdayPixels">
+ <body><![CDATA[
+ // Only do this if the long weekdays are visible and we haven't already cached.
+ let longNameWidth = this.longName.boxObject.width;
+ if (longNameWidth > 0) {
+ this.longWeekdayPixels = longNameWidth +
+ getSummarizedStyleValues(this.longName, ["margin-left", "margin-right"]);
+ this.longWeekdayPixels += getSummarizedStyleValues(this, ["border-left-width",
+ "padding-left", "padding-right"]);
+ return this.longWeekdayPixels;
+ } else {
+ // in this case the weekdaypixels have not yet been layouted;
+ // by definition 0 is returned
+ return 0;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-bindings.css b/calendar/base/content/calendar-bindings.css
new file mode 100644
index 000000000..e34071c94
--- /dev/null
+++ b/calendar/base/content/calendar-bindings.css
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+
+calendar-day-view {
+ -moz-binding: url(chrome://calendar/content/calendar-views.xml#calendar-day-view);
+}
+
+calendar-week-view {
+ -moz-binding: url(chrome://calendar/content/calendar-views.xml#calendar-week-view);
+}
+
+calendar-multiweek-view {
+ -moz-binding: url(chrome://calendar/content/calendar-views.xml#calendar-multiweek-view);
+}
+
+calendar-month-view {
+ -moz-binding: url(chrome://calendar/content/calendar-views.xml#calendar-month-view);
+}
+
+calendar-task-tree {
+ -moz-binding: url(chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree);
+}
+
+menupopup[type="task-progress"] > arrowscrollbox {
+ -moz-binding: url(chrome://calendar/content/calendar-menus.xml#task-progress-menupopup);
+}
+
+menupopup[type="task-priority"] > arrowscrollbox {
+ -moz-binding: url(chrome://calendar/content/calendar-menus.xml#task-priority-menupopup);
+}
+
+task-menupopup {
+ -moz-binding: url(chrome://calendar/content/calendar-menus.xml#task-menupopup);
+}
+
+calendar-caption {
+ -moz-binding: url("chrome://calendar/content/calendar-item-bindings.xml#calendar-caption");
+}
+
+.item-date-row {
+ -moz-binding: url(chrome://calendar/content/calendar-item-bindings.xml#item-date-row);
+}
diff --git a/calendar/base/content/calendar-calendars-list.xul b/calendar/base/content/calendar-calendars-list.xul
new file mode 100644
index 000000000..fcca18e9c
--- /dev/null
+++ b/calendar/base/content/calendar-calendars-list.xul
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://calendar/skin/calendar-management.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<overlay id="calendar-list-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <popupset id="calendar-popupset">
+ <menupopup id="list-calendars-context-menu"
+ onpopupshowing="return calendarListSetupContextMenu(event);">
+ <menuitem id="list-calendars-context-togglevisible"
+ accesskeyshow="&calendar.context.showcalendar.accesskey;"
+ accesskeyhide="&calendar.context.hidecalendar.accesskey;"
+ oncommand="toggleCalendarVisible(document.getElementById('list-calendars-context-menu').contextCalendar);"/>
+ <menuitem id="list-calendars-context-showonly"
+ accesskey="&calendar.context.showonly.accesskey;"
+ oncommand="showOnlyCalendar(document.getElementById('list-calendars-context-menu').contextCalendar);"/>
+ <menuitem id="list-calendars-context-showall"
+ label="&calendar.context.showall.label;"
+ accesskey="&calendar.context.showall.accesskey;"
+ oncommand="showAllCalendars();"/>
+ <menuseparator id="list-calendars-context-showops-menuseparator"/>
+ <menuitem id="list-calendars-context-new"
+ label="&calendar.context.newserver.label;"
+ accesskey="&calendar.context.newserver.accesskey;"
+ observes="calendar_new_calendar_command"/>
+ <menuitem id="list-calendars-context-find"
+ label="&calendar.context.findcalendar.label;"
+ accesskey="&calendar.context.findcalendar.accesskey;"
+ oncommand="openCalendarSubscriptionsDialog();"/>
+ <menuitem id="list-calendars-context-delete"
+ labeldelete="&calendar.context.deleteserver2.label;"
+ labelremove="&calendar.context.removeserver.label;"
+ labelunsubscribe="&calendar.context.unsubscribeserver.label;"
+ accesskeydelete="&calendar.context.deleteserver2.accesskey;"
+ accesskeyremove="&calendar.context.removeserver.accesskey;"
+ accesskeyunsubscribe="&calendar.context.unsubscribeserver.accesskey;"
+ observes="calendar_delete_calendar_command"/>
+ <menuseparator id="list-calendars-context-itemops-menuseparator"/>
+ <menuitem id="list-calendars-context-export"
+ label="&calendar.context.export.label;"
+ accesskey="&calendar.context.export.accesskey;"
+ oncommand="exportEntireCalendar(document.getElementById('list-calendars-context-menu').contextCalendar);"/>
+ <menuitem id="list-calendars-context-publish"
+ label="&calendar.context.publish.label;"
+ accesskey="&calendar.context.publish.accesskey;"
+ observes="calendar_publish_selected_calendar_command"/>
+ <menuseparator id="list-calendars-context-export-menuseparator"/>
+ <menuitem id="list-calendars-context-reload"
+ label="&calendar.context.synccalendars.label;"
+ accesskey="&calendar.context.synccalendars.accesskey;"
+ observes="calendar_reload_remote_calendars"/>
+ <menuseparator id="list-calendars-context-reload-menuseparator"/>
+ <menuitem id="list-calendars-context-edit"
+ label="&calendar.context.properties.label;"
+ accesskey="&calendar.context.properties.accesskey;"
+ observes="calendar_edit_calendar_command"/>
+ </menupopup>
+ <tooltip id="calendar-list-tooltip"
+ onpopupshowing="return calendarListTooltipShowing(event)"/>
+ </popupset>
+
+ <calendar-list-tree id="calendar-list-tree-widget"
+ type="full"
+ writable="true"
+ allowdrag="true"
+ onSortOrderChanged="updateSortOrderPref(event)"
+ onselect="document.commandDispatcher.updateCommands('calendar_commands')"
+ childtooltip="calendar-list-tooltip"
+ childcontext="list-calendars-context-menu"/>
+</overlay>
diff --git a/calendar/base/content/calendar-chrome-startup.js b/calendar/base/content/calendar-chrome-startup.js
new file mode 100644
index 000000000..e63688776
--- /dev/null
+++ b/calendar/base/content/calendar-chrome-startup.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/iteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/* exported commonInitCalendar, commonFinishCalendar */
+
+/**
+ * Common initialization steps for calendar chrome windows.
+ */
+function commonInitCalendar() {
+ // Move around toolbarbuttons and whatever is needed in the UI.
+ migrateCalendarUI();
+
+ // Load the Calendar Manager
+ loadCalendarManager();
+
+ // Restore the last shown calendar view
+ switchCalendarView(getLastCalendarView(), false);
+
+ // set up the unifinder
+ prepareCalendarToDoUnifinder();
+
+ // Make sure we update ourselves if the program stays open over midnight
+ scheduleMidnightUpdate(refreshUIBits);
+
+ // Set up the command controller from calendar-common-sets.js
+ injectCalendarCommandController();
+
+ // Set up item and day selection listeners
+ getViewDeck().addEventListener("dayselect", observeViewDaySelect, false);
+ getViewDeck().addEventListener("itemselect", calendarController.onSelectionChanged, true);
+
+ // Start alarm service
+ Components.classes["@mozilla.org/calendar/alarm-service;1"]
+ .getService(Components.interfaces.calIAlarmService)
+ .startup();
+ document.getElementById("calsidebar_splitter").addEventListener("command", onCalendarViewResize, false);
+ window.addEventListener("resize", onCalendarViewResize, true);
+
+ // Set up the category colors
+ categoryManagement.initCategories();
+
+ // Set up window pref observers
+ calendarWindowPrefs.init();
+
+ /* Ensure the new items commands state can be setup properly even when no
+ * calendar support refreshes (i.e. the "onLoad" notification) or when none
+ * are active. In specific cases such as for file-based ICS calendars can
+ * happen, the initial "onLoad" will already have been triggered at this
+ * point (see bug 714431 comment 29). We thus inconditionnally invoke
+ * calendarUpdateNewItemsCommand until somebody writes code that enables the
+ * checking of the calendar readiness (getProperty("ready") ?).
+ */
+ calendarUpdateNewItemsCommand();
+}
+
+/**
+ * Common unload steps for calendar chrome windows.
+ */
+function commonFinishCalendar() {
+ // Unload the calendar manager
+ unloadCalendarManager();
+
+ // clean up the unifinder
+ finishCalendarToDoUnifinder();
+
+ // Remove the command controller
+ removeCalendarCommandController();
+
+ document.getElementById("calsidebar_splitter").removeEventListener("command", onCalendarViewResize, false);
+ window.removeEventListener("resize", onCalendarViewResize, true);
+
+ // Clean up the category colors
+ categoryManagement.cleanupCategories();
+
+ // Clean up window pref observers
+ calendarWindowPrefs.cleanup();
+}
+
+/**
+ * Handler function to create |viewtype + "viewresized"| events that are
+ * dispatched through the calendarviewBroadcaster.
+ *
+ * XXX this has nothing to do with startup, needs to go somewhere else.
+ */
+function onCalendarViewResize(aEvent) {
+ let event = document.createEvent("Events");
+ event.initEvent(currentView().type + "viewresized", true, false);
+ document.getElementById("calendarviewBroadcaster").dispatchEvent(event);
+}
+
+/**
+ * TODO: The systemcolors pref observer really only needs to be set up once, so
+ * ideally this code should go into a component. This should be taken care of when
+ * there are more prefs that need to be observed on a global basis that don't fit
+ * into the calendar manager.
+ */
+var calendarWindowPrefs = {
+
+ /** nsISupports QueryInterface */
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
+
+ /** Initialize the preference observers */
+ init: function() {
+ Services.prefs.addObserver("calendar.view.useSystemColors", this, false);
+ Services.ww.registerNotification(this);
+
+ // Trigger setting pref on all open windows
+ this.observe(null, "nsPref:changed", "calendar.view.useSystemColors");
+ },
+
+ /** Cleanup the preference observers */
+ cleanup: function() {
+ Services.prefs.removeObserver("calendar.view.useSystemColors", this);
+ Services.ww.unregisterNotification(this);
+ },
+
+ /**
+ * Observer function called when a pref has changed
+ *
+ * @see nsIObserver
+ */
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed") {
+ switch (aData) {
+ case "calendar.view.useSystemColors": {
+ let attributeValue = Preferences.get("calendar.view.useSystemColors", false) && "true";
+ for (let win in fixIterator(Services.ww.getWindowEnumerator())) {
+ setElementValue(win.document.documentElement, attributeValue, "systemcolors");
+ }
+ break;
+ }
+ }
+ } else if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("load", () => {
+ let attributeValue = Preferences.get("calendar.view.useSystemColors", false) && "true";
+ setElementValue(win.document.documentElement, attributeValue, "systemcolors");
+ }, false);
+ }
+ }
+};
+
+/**
+ * Migrate calendar UI. This function is called at each startup and can be used
+ * to change UI items that require js code intervention
+ */
+function migrateCalendarUI() {
+ const UI_VERSION = 3;
+ let currentUIVersion = Preferences.get("calendar.ui.version");
+ if (currentUIVersion >= UI_VERSION) {
+ return;
+ }
+
+ try {
+ Preferences.set("calendar.ui.version", UI_VERSION);
+ } catch (e) {
+ cal.ERROR("Error upgrading UI from " + currentUIVersion + " to " +
+ UI_VERSION + ": " + e);
+ }
+}
diff --git a/calendar/base/content/calendar-clipboard.js b/calendar/base/content/calendar-clipboard.js
new file mode 100644
index 000000000..20011ab9a
--- /dev/null
+++ b/calendar/base/content/calendar-clipboard.js
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/* exported cutToClipboard, pasteFromClipboard */
+
+/**
+ * Test if a writable calendar is selected, and if the clipboard has items that
+ * can be pasted into Calendar. The data must be of type "text/calendar" or
+ * "text/unicode".
+ *
+ * @return If true, pasting is currently possible.
+ */
+function canPaste() {
+ let selectedCal = getSelectedCalendar();
+ if (!selectedCal || !cal.isCalendarWritable(selectedCal)) {
+ return false;
+ }
+
+ const flavors = ["text/calendar", "text/unicode"];
+ return Services.clipboard.hasDataMatchingFlavors(flavors,
+ flavors.length,
+ Components.interfaces.nsIClipboard.kGlobalClipboard);
+}
+
+/**
+ * Copy the ics data of the current view's selected events to the clipboard and
+ * deletes the events on success
+ */
+function cutToClipboard() {
+ if (copyToClipboard()) {
+ deleteSelectedItems();
+ }
+}
+
+/**
+ * Copy the ics data of the items in calendarItemArray to the clipboard. Fills
+ * both text/unicode and text/calendar mime types.
+ *
+ * @param calendarItemArray (optional) an array of items to copy. If not
+ * passed, the current view's selected items will
+ * be used.
+ * @return A boolean indicating if the operation succeeded.
+ */
+function copyToClipboard(calendarItemArray) {
+ if (!calendarItemArray) {
+ calendarItemArray = getSelectedItems();
+ }
+
+ if (!calendarItemArray.length) {
+ cal.LOG("[calendar-clipboard] No items to copy.");
+ return false;
+ }
+
+ let icsSerializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
+ .createInstance(Components.interfaces.calIIcsSerializer);
+ icsSerializer.addItems(calendarItemArray, calendarItemArray.length);
+ let icsString = icsSerializer.serializeToString();
+
+ let clipboard = Services.clipboard;
+ let trans = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+
+ if (trans && clipboard) {
+ // Register supported data flavors
+ trans.init(null);
+ trans.addDataFlavor("text/calendar");
+ trans.addDataFlavor("text/unicode");
+
+ // Create the data objects
+ let icsWrapper = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Components.interfaces.nsISupportsString);
+ icsWrapper.data = icsString;
+
+ // Add data objects to transferable
+ // Both Outlook 2000 client and Lotus Organizer use text/unicode
+ // when pasting iCalendar data.
+ trans.setTransferData("text/calendar",
+ icsWrapper,
+ icsWrapper.data.length * 2); // double byte data
+ trans.setTransferData("text/unicode",
+ icsWrapper,
+ icsWrapper.data.length * 2);
+
+ clipboard.setData(trans,
+ null,
+ Components.interfaces.nsIClipboard.kGlobalClipboard);
+
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Reads ics data from the clipboard, parses it into items and inserts the items
+ * into the currently selected calendar.
+ */
+function pasteFromClipboard() {
+ if (!canPaste()) {
+ return;
+ }
+
+ let clipboard = Services.clipboard;
+ let trans = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+
+ if (!trans || !clipboard) {
+ return;
+ }
+
+ // Register the wanted data flavors (highest fidelity first!)
+ trans.init(null);
+ trans.addDataFlavor("text/calendar");
+ trans.addDataFlavor("text/unicode");
+
+ // Get transferable from clipboard
+ clipboard.getData(trans, Components.interfaces.nsIClipboard.kGlobalClipboard);
+
+ // Ask transferable for the best flavor.
+ let flavor = {};
+ let data = {};
+ trans.getAnyTransferData(flavor, data, {});
+ data = data.value.QueryInterface(Components.interfaces.nsISupportsString).data;
+ switch (flavor.value) {
+ case "text/calendar":
+ case "text/unicode": {
+ let destCal = getSelectedCalendar();
+ if (!destCal) {
+ return;
+ }
+
+ let icsParser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ try {
+ icsParser.parseString(data);
+ } catch (e) {
+ // Ignore parser errors from the clipboard data, if it fails
+ // there will just be 0 items.
+ }
+
+ let items = icsParser.getItems({});
+ if (items.length == 0) {
+ return;
+ }
+
+ // If there are multiple items on the clipboard, the earliest
+ // should be set to the selected day and the rest adjusted.
+ let earliestDate = null;
+ for (let item of items) {
+ let date = null;
+ if (item.startDate) {
+ date = item.startDate.clone();
+ } else if (item.entryDate) {
+ date = item.entryDate.clone();
+ } else if (item.dueDate) {
+ date = item.dueDate.clone();
+ }
+
+ if (!date) {
+ continue;
+ }
+
+ if (!earliestDate || date.compare(earliestDate) < 0) {
+ earliestDate = date;
+ }
+ }
+ let firstDate = currentView().selectedDay;
+
+ let offset = null;
+ if (earliestDate) {
+ // Timezones and DT/DST time may differ between the earliest item
+ // and the selected day. Determine the offset between the
+ // earliestDate in local time and the selected day in whole days.
+ earliestDate = earliestDate.getInTimezone(calendarDefaultTimezone());
+ earliestDate.isDate = true;
+ offset = firstDate.subtractDate(earliestDate);
+ let deltaDST = firstDate.timezoneOffset - earliestDate.timezoneOffset;
+ offset.inSeconds += deltaDST;
+ }
+
+ startBatchTransaction();
+ for (let item of items) {
+ let newItem = item.clone();
+ // Set new UID to allow multiple paste actions of the same
+ // clipboard content.
+ newItem.id = cal.getUUID();
+ if (offset) {
+ cal.shiftItem(newItem, offset);
+ }
+ doTransaction("add", newItem, destCal, null, null);
+ }
+ endBatchTransaction();
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/calendar/base/content/calendar-common-sets.js b/calendar/base/content/calendar-common-sets.js
new file mode 100644
index 000000000..55736ca7a
--- /dev/null
+++ b/calendar/base/content/calendar-common-sets.js
@@ -0,0 +1,950 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/* exported injectCalendarCommandController, removeCalendarCommandController,
+ * setupContextItemType, minimonthPick, getSelectedItems,
+ * deleteSelectedItems, calendarUpdateNewItemsCommand
+ */
+
+var CalendarDeleteCommandEnabled = false;
+var CalendarNewEventsCommandEnabled = false;
+var CalendarNewTasksCommandEnabled = false;
+
+/**
+ * Command controller to execute calendar specific commands
+ * @see nsICommandController
+ */
+var calendarController = {
+ defaultController: null,
+
+ commands: {
+ // Common commands
+ "calendar_new_event_command": true,
+ "calendar_new_event_context_command": true,
+ "calendar_modify_event_command": true,
+ "calendar_delete_event_command": true,
+
+ "calendar_modify_focused_item_command": true,
+ "calendar_delete_focused_item_command": true,
+
+ "calendar_new_todo_command": true,
+ "calendar_new_todo_context_command": true,
+ "calendar_new_todo_todaypane_command": true,
+ "calendar_modify_todo_command": true,
+ "calendar_modify_todo_todaypane_command": true,
+ "calendar_delete_todo_command": true,
+
+ "calendar_new_calendar_command": true,
+ "calendar_edit_calendar_command": true,
+ "calendar_delete_calendar_command": true,
+
+ "calendar_import_command": true,
+ "calendar_export_command": true,
+ "calendar_export_selection_command": true,
+
+ "calendar_publish_selected_calendar_command": true,
+ "calendar_publish_calendar_command": true,
+ "calendar_publish_selected_events_command": true,
+
+ "calendar_view_next_command": true,
+ "calendar_view_prev_command": true,
+
+ "calendar_toggle_orientation_command": true,
+ "calendar_toggle_workdays_only_command": true,
+
+ "calendar_day-view_command": true,
+ "calendar_week-view_command": true,
+ "calendar_multiweek-view_command": true,
+ "calendar_month-view_command": true,
+
+ "calendar_task_filter_command": true,
+ "calendar_task_filter_todaypane_command": true,
+ "calendar_reload_remote_calendars": true,
+ "calendar_show_unifinder_command": true,
+ "calendar_toggle_completed_command": true,
+ "calendar_percentComplete-0_command": true,
+ "calendar_percentComplete-25_command": true,
+ "calendar_percentComplete-50_command": true,
+ "calendar_percentComplete-75_command": true,
+ "calendar_percentComplete-100_command": true,
+ "calendar_priority-0_command": true,
+ "calendar_priority-9_command": true,
+ "calendar_priority-5_command": true,
+ "calendar_priority-1_command": true,
+ "calendar_general-priority_command": true,
+ "calendar_general-progress_command": true,
+ "calendar_general-postpone_command": true,
+ "calendar_postpone-1hour_command": true,
+ "calendar_postpone-1day_command": true,
+ "calendar_postpone-1week_command": true,
+ "calendar_task_category_command": true,
+
+ "calendar_attendance_command": true,
+
+ // for events/tasks in a tab
+ "cmd_save": true,
+ "cmd_accept": true,
+
+ // Pseudo commands
+ "calendar_in_foreground": true,
+ "calendar_in_background": true,
+ "calendar_mode_calendar": true,
+ "calendar_mode_task": true,
+
+ "cmd_selectAll": true
+ },
+
+ updateCommands: function() {
+ for (let command in this.commands) {
+ goUpdateCommand(command);
+ }
+ },
+
+ supportsCommand: function(aCommand) {
+ if (aCommand in this.commands) {
+ return true;
+ }
+ if (this.defaultContoller) {
+ return this.defaultContoller.supportsCommand(aCommand);
+ }
+ return false;
+ },
+
+ isCommandEnabled: function(aCommand) {
+ switch (aCommand) {
+ case "calendar_new_event_command":
+ case "calendar_new_event_context_command":
+ return CalendarNewEventsCommandEnabled;
+ case "calendar_modify_focused_item_command":
+ return this.item_selected;
+ case "calendar_modify_event_command":
+ return this.item_selected;
+ case "calendar_delete_focused_item_command":
+ return CalendarDeleteCommandEnabled && this.selected_items_writable;
+ case "calendar_delete_event_command":
+ return CalendarDeleteCommandEnabled && this.selected_items_writable;
+ case "calendar_new_todo_command":
+ case "calendar_new_todo_context_command":
+ case "calendar_new_todo_todaypane_command":
+ return CalendarNewTasksCommandEnabled;
+ case "calendar_modify_todo_command":
+ case "calendar_modify_todo_todaypane_command":
+ return this.todo_items_selected;
+ // This code is temporarily commented out due to
+ // bug 469684 Unifinder-todo: raising of the context menu fires blur-event
+ // this.todo_tasktree_focused;
+ case "calendar_edit_calendar_command":
+ return this.isCalendarInForeground();
+ case "calendar_task_filter_command":
+ return true;
+ case "calendar_delete_todo_command":
+ if (!CalendarDeleteCommandEnabled) {
+ return false;
+ }
+ // falls through otherwise
+ case "calendar_toggle_completed_command":
+ case "calendar_percentComplete-0_command":
+ case "calendar_percentComplete-25_command":
+ case "calendar_percentComplete-50_command":
+ case "calendar_percentComplete-75_command":
+ case "calendar_percentComplete-100_command":
+ case "calendar_priority-0_command":
+ case "calendar_priority-9_command":
+ case "calendar_priority-5_command":
+ case "calendar_priority-1_command":
+ case "calendar_task_category_command":
+ case "calendar_general-progress_command":
+ case "calendar_general-priority_command":
+ case "calendar_general-postpone_command":
+ case "calendar_postpone-1hour_command":
+ case "calendar_postpone-1day_command":
+ case "calendar_postpone-1week_command":
+ return ((this.isCalendarInForeground() || this.todo_tasktree_focused) &&
+ this.writable &&
+ this.todo_items_selected &&
+ this.todo_items_writable) ||
+ document.getElementById("tabmail").currentTabInfo.mode.type == "calendarTask";
+ case "calendar_delete_calendar_command":
+ return this.isCalendarInForeground() && !this.last_calendar;
+ case "calendar_import_command":
+ return this.writable;
+ case "calendar_export_selection_command":
+ return this.item_selected;
+ case "calendar_toggle_orientation_command":
+ return this.isInMode("calendar") &&
+ currentView().supportsRotation;
+ case "calendar_toggle_workdays_only_command":
+ return this.isInMode("calendar") &&
+ currentView().supportsWorkdaysOnly;
+ case "calendar_publish_selected_events_command":
+ return this.item_selected;
+
+ case "calendar_reload_remote_calendar":
+ return !this.no_network_calendars && !this.offline;
+ case "calendar_attendance_command": {
+ let attendSel = false;
+ if (this.todo_tasktree_focused) {
+ attendSel = this.writable &&
+ this.todo_items_invitation &&
+ this.todo_items_selected &&
+ this.todo_items_writable;
+ } else {
+ attendSel = this.item_selected &&
+ this.selected_events_invitation &&
+ this.selected_items_writable;
+ }
+
+ // Small hack, we want to hide instead of disable.
+ setBooleanAttribute("calendar_attendance_command", "hidden", !attendSel);
+ return attendSel;
+ }
+
+ // The following commands all just need the calendar in foreground,
+ // make sure you take care when changing things here.
+ case "calendar_view_next_command":
+ case "calendar_view_prev_command":
+ case "calendar_in_foreground":
+ return this.isCalendarInForeground();
+ case "calendar_in_background":
+ return !this.isCalendarInForeground();
+
+ // The following commands need calendar mode, be careful when
+ // changing things.
+ case "calendar_day-view_command":
+ case "calendar_week-view_command":
+ case "calendar_multiweek-view_command":
+ case "calendar_month-view_command":
+ case "calendar_show_unifinder_command":
+ case "calendar_mode_calendar":
+ return this.isInMode("calendar");
+
+ case "calendar_mode_task":
+ return this.isInMode("task");
+
+ case "cmd_selectAll":
+ if (this.todo_tasktree_focused || this.isInMode("calendar")) {
+ return true;
+ } else if (this.defaultController.supportsCommand(aCommand)) {
+ return this.defaultController.isCommandEnabled(aCommand);
+ }
+ break;
+
+ // for events/tasks in a tab
+ case "cmd_save":
+ // falls through
+ case "cmd_accept": {
+ let tabType = document.getElementById("tabmail").currentTabInfo.mode.type;
+ return tabType == "calendarTask" || tabType == "calendarEvent";
+ }
+
+ default:
+ if (this.defaultController && !this.isCalendarInForeground()) {
+ // The delete-button demands a special handling in mail-mode
+ // as it is supposed to delete an element of the focused pane
+ if (aCommand == "cmd_delete" || aCommand == "button_delete") {
+ let focusedElement = document.commandDispatcher.focusedElement;
+ if (focusedElement) {
+ if (focusedElement.getAttribute("id") == "agenda-listbox") {
+ return agendaListbox.isEventSelected();
+ } else if (focusedElement.className == "calendar-task-tree") {
+ return this.writable &&
+ this.todo_items_selected &&
+ this.todo_items_writable;
+ }
+ }
+ }
+
+ if (this.defaultController.supportsCommand(aCommand)) {
+ return this.defaultController.isCommandEnabled(aCommand);
+ }
+ }
+ if (aCommand in this.commands) {
+ // All other commands we support should be enabled by default
+ return true;
+ }
+ }
+ return false;
+ },
+
+ doCommand: function(aCommand) {
+ switch (aCommand) {
+ // Common Commands
+ case "calendar_new_event_command":
+ createEventWithDialog(getSelectedCalendar(),
+ getDefaultStartDate(currentView().selectedDay));
+ break;
+ case "calendar_new_event_context_command": {
+ let newStart = currentView().selectedDateTime;
+ if (!newStart) {
+ newStart = getDefaultStartDate(currentView().selectedDay);
+ }
+ createEventWithDialog(getSelectedCalendar(), newStart,
+ null, null, null,
+ newStart.isDate == true);
+ break;
+ }
+ case "calendar_modify_event_command":
+ editSelectedEvents();
+ break;
+ case "calendar_modify_focused_item_command": {
+ let focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement && this.defaultController && !this.isCalendarInForeground()) {
+ this.defaultController.doCommand(aCommand);
+ } else {
+ let focusedRichListbox = getParentNodeOrThis(focusedElement, "richlistbox");
+ if (focusedRichListbox && focusedRichListbox.id == "agenda-listbox") {
+ agendaListbox.editSelectedItem();
+ } else if (focusedElement && focusedElement.className == "calendar-task-tree") {
+ modifyTaskFromContext();
+ } else if (this.isInMode("calendar")) {
+ editSelectedEvents();
+ }
+ }
+ break;
+ }
+ case "calendar_delete_event_command":
+ deleteSelectedEvents();
+ break;
+ case "calendar_delete_focused_item_command": {
+ let focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement && this.defaultController && !this.isCalendarInForeground()) {
+ this.defaultController.doCommand(aCommand);
+ } else {
+ let focusedRichListbox = getParentNodeOrThis(focusedElement, "richlistbox");
+ if (focusedRichListbox && focusedRichListbox.id == "agenda-listbox") {
+ agendaListbox.deleteSelectedItem(false);
+ } else if (focusedElement && focusedElement.className == "calendar-task-tree") {
+ deleteToDoCommand(null, false);
+ } else if (this.isInMode("calendar")) {
+ deleteSelectedEvents();
+ }
+ }
+ break;
+ }
+ case "calendar_new_todo_command":
+ createTodoWithDialog(getSelectedCalendar(),
+ null, null, null,
+ getDefaultStartDate(currentView().selectedDay));
+ break;
+ case "calendar_new_todo_context_command": {
+ let initialDate = currentView().selectedDateTime;
+ if (!initialDate || initialDate.isDate) {
+ initialDate = getDefaultStartDate(currentView().selectedDay);
+ }
+ createTodoWithDialog(getSelectedCalendar(),
+ null, null, null,
+ initialDate);
+ break;
+ }
+ case "calendar_new_todo_todaypane_command":
+ createTodoWithDialog(getSelectedCalendar(),
+ null, null, null,
+ getDefaultStartDate(agendaListbox.today.start));
+ break;
+ case "calendar_delete_todo_command":
+ deleteToDoCommand();
+ break;
+ case "calendar_modify_todo_command":
+ modifyTaskFromContext(null, getDefaultStartDate(currentView().selectedDay));
+ break;
+ case "calendar_modify_todo_todaypane_command":
+ modifyTaskFromContext(null, getDefaultStartDate(agendaListbox.today.start));
+ break;
+
+ case "calendar_new_calendar_command":
+ openCalendarWizard();
+ break;
+ case "calendar_edit_calendar_command":
+ openCalendarProperties(getSelectedCalendar());
+ break;
+ case "calendar_delete_calendar_command":
+ promptDeleteCalendar(getSelectedCalendar());
+ break;
+
+ case "calendar_import_command":
+ loadEventsFromFile();
+ break;
+ case "calendar_export_command":
+ exportEntireCalendar();
+ break;
+ case "calendar_export_selection_command":
+ saveEventsToFile(currentView().getSelectedItems({}));
+ break;
+
+ case "calendar_publish_selected_calendar_command":
+ publishEntireCalendar(getSelectedCalendar());
+ break;
+ case "calendar_publish_calendar_command":
+ publishEntireCalendar();
+ break;
+ case "calendar_publish_selected_events_command":
+ publishCalendarData();
+ break;
+
+ case "calendar_reload_remote_calendars":
+ getCompositeCalendar().refresh();
+ break;
+ case "calendar_show_unifinder_command":
+ toggleUnifinder();
+ break;
+ case "calendar_view_next_command":
+ currentView().moveView(1);
+ break;
+ case "calendar_view_prev_command":
+ currentView().moveView(-1);
+ break;
+ case "calendar_toggle_orientation_command":
+ toggleOrientation();
+ break;
+ case "calendar_toggle_workdays_only_command":
+ toggleWorkdaysOnly();
+ break;
+
+ case "calendar_day-view_command":
+ switchCalendarView("day", true);
+ break;
+ case "calendar_week-view_command":
+ switchCalendarView("week", true);
+ break;
+ case "calendar_multiweek-view_command":
+ switchCalendarView("multiweek", true);
+ break;
+ case "calendar_month-view_command":
+ switchCalendarView("month", true);
+ break;
+ case "calendar_attendance_command":
+ // This command is actually handled inline, since it takes a value
+ break;
+
+ case "cmd_selectAll":
+ if (!this.todo_tasktree_focused &&
+ this.defaultController && !this.isCalendarInForeground()) {
+ // Unless a task tree is focused, make the default controller
+ // take care.
+ this.defaultController.doCommand(aCommand);
+ } else {
+ selectAllItems();
+ }
+ break;
+
+ default:
+ if (this.defaultController && !this.isCalendarInForeground()) {
+ // If calendar is not in foreground, let the default controller take
+ // care. If we don't have a default controller, just continue.
+ this.defaultController.doCommand(aCommand);
+ return;
+ }
+
+ }
+ return;
+ },
+
+ onEvent: function(aEvent) {
+ },
+
+ isCalendarInForeground: function() {
+ return gCurrentMode && gCurrentMode != "mail";
+ },
+
+ isInMode: function(mode) {
+ switch (mode) {
+ case "mail":
+ return !isCalendarInForeground();
+ case "calendar":
+ return gCurrentMode && gCurrentMode == "calendar";
+ case "task":
+ return gCurrentMode && gCurrentMode == "task";
+ }
+ return false;
+ },
+
+ onSelectionChanged: function(aEvent) {
+ let selectedItems = aEvent.detail;
+
+ calendarUpdateDeleteCommand(selectedItems);
+ calendarController.item_selected = selectedItems && (selectedItems.length > 0);
+
+ let selLength = (selectedItems === undefined ? 0 : selectedItems.length);
+ let selected_events_readonly = 0;
+ let selected_events_requires_network = 0;
+ let selected_events_invitation = 0;
+
+ if (selLength > 0) {
+ for (let item of selectedItems) {
+ if (item.calendar.readOnly) {
+ selected_events_readonly++;
+ }
+ if (item.calendar.getProperty("requiresNetwork") &&
+ !item.calendar.getProperty("cache.enabled") &&
+ !item.calendar.getProperty("cache.always")) {
+ selected_events_requires_network++;
+ }
+
+ if (cal.isInvitation(item)) {
+ selected_events_invitation++;
+ } else if (item.organizer) {
+ // If we are the organizer and there are attendees, then
+ // this is likely also an invitation.
+ let calOrgId = item.calendar.getProperty("organizerId");
+ if (item.organizer.id == calOrgId && item.getAttendees({}).length) {
+ selected_events_invitation++;
+ }
+ }
+ }
+ }
+
+ calendarController.selected_events_readonly =
+ (selected_events_readonly == selLength);
+
+ calendarController.selected_events_requires_network =
+ (selected_events_requires_network == selLength);
+ calendarController.selected_events_invitation =
+ (selected_events_invitation == selLength);
+
+ calendarController.updateCommands();
+ calendarController2.updateCommands();
+ document.commandDispatcher.updateCommands("mail-toolbar");
+ },
+
+ /**
+ * Condition Helpers
+ */
+
+ // These attributes will be set up manually.
+ item_selected: false,
+ selected_events_readonly: false,
+ selected_events_requires_network: false,
+ selected_events_invitation: false,
+
+ /**
+ * Returns a boolean indicating if its possible to write items to any
+ * calendar.
+ */
+ get writable() {
+ return cal.getCalendarManager().getCalendars({}).some(cal.isCalendarWritable);
+ },
+
+ /**
+ * Returns a boolean indicating if the application is currently in offline
+ * mode.
+ */
+ get offline() {
+ return Services.io.offline;
+ },
+
+ /**
+ * Returns a boolean indicating if all calendars are readonly.
+ */
+ get all_readonly() {
+ let calMgr = getCalendarManager();
+ return (calMgr.readOnlyCalendarCount == calMgr.calendarCount);
+ },
+
+ /**
+ * Returns a boolean indicating if all calendars are local
+ */
+ get no_network_calendars() {
+ return (getCalendarManager().networkCalendarCount == 0);
+ },
+
+ /**
+ * Returns a boolean indicating if there are calendars that don't require
+ * network access.
+ */
+ get has_local_calendars() {
+ let calMgr = getCalendarManager();
+ return (calMgr.networkCalendarCount < calMgr.calendarCount);
+ },
+
+ /**
+ * Returns a boolean indicating if there are cached calendars and thus that don't require
+ * network access.
+ */
+ get has_cached_calendars() {
+ let calMgr = getCalendarManager();
+ let calendars = calMgr.getCalendars({});
+ for (let calendar of calendars) {
+ if (calendar.getProperty("cache.enabled") || calendar.getProperty("cache.always")) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Returns a boolean indicating that there is only one calendar left.
+ */
+ get last_calendar() {
+ return (getCalendarManager().calendarCount < 2);
+ },
+
+ /**
+ * Returns a boolean indicating that all local calendars are readonly
+ */
+ get all_local_calendars_readonly() {
+ // We might want to speed this part up by keeping track of this in the
+ // calendar manager.
+ let calendars = getCalendarManager().getCalendars({});
+ let count = calendars.length;
+ for (let calendar of calendars) {
+ if (!isCalendarWritable(calendar)) {
+ count--;
+ }
+ }
+ return (count == 0);
+ },
+
+ /**
+ * Returns a boolean indicating that at least one of the items selected
+ * in the current view has a writable calendar.
+ */
+ get selected_items_writable() {
+ return this.writable &&
+ this.item_selected &&
+ !this.selected_events_readonly &&
+ (!this.offline || !this.selected_events_requires_network);
+ },
+
+ /**
+ * Returns a boolean indicating that tasks are selected.
+ */
+ get todo_items_selected() {
+ let selectedTasks = getSelectedTasks();
+ return (selectedTasks.length > 0);
+ },
+
+
+ get todo_items_invitation() {
+ let selectedTasks = getSelectedTasks();
+ let selected_tasks_invitation = 0;
+
+ for (let item of selectedTasks) {
+ if (cal.isInvitation(item)) {
+ selected_tasks_invitation++;
+ } else if (item.organizer) {
+ // If we are the organizer and there are attendees, then
+ // this is likely also an invitation.
+ let calOrgId = item.calendar.getProperty("organizerId");
+ if (item.organizer.id == calOrgId && item.getAttendees({}).length) {
+ selected_tasks_invitation++;
+ }
+ }
+ }
+
+ return (selectedTasks.length == selected_tasks_invitation);
+ },
+
+ /**
+ * Returns a boolean indicating that at least one task in the selection is
+ * on a calendar that is writable.
+ */
+ get todo_items_writable() {
+ let selectedTasks = getSelectedTasks();
+ for (let task of selectedTasks) {
+ if (isCalendarWritable(task.calendar)) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+/**
+ * XXX This is a temporary hack so we can release 1.0b2. This will soon be
+ * superceeded by a new command controller architecture.
+ */
+var calendarController2 = {
+ defaultController: null,
+
+ commands: {
+ cmd_cut: true,
+ cmd_copy: true,
+ cmd_paste: true,
+ cmd_undo: true,
+ cmd_redo: true,
+ cmd_print: true,
+ cmd_pageSetup: true,
+
+ cmd_printpreview: true,
+ button_print: true,
+ button_delete: true,
+ cmd_delete: true,
+ cmd_properties: true,
+ cmd_goForward: true,
+ cmd_goBack: true,
+ cmd_fullZoomReduce: true,
+ cmd_fullZoomEnlarge: true,
+ cmd_fullZoomReset: true,
+ cmd_showQuickFilterBar: true
+ },
+
+ // These functions can use the same from the calendar controller for now.
+ updateCommands: calendarController.updateCommands,
+ supportsCommand: calendarController.supportsCommand,
+ onEvent: calendarController.onEvent,
+
+ isCommandEnabled: function(aCommand) {
+ switch (aCommand) {
+ // Thunderbird Commands
+ case "cmd_cut":
+ return calendarController.selected_items_writable;
+ case "cmd_copy":
+ return calendarController.item_selected;
+ case "cmd_paste":
+ return canPaste();
+ case "cmd_undo":
+ goSetMenuValue(aCommand, "valueDefault");
+ return canUndo();
+ case "cmd_redo":
+ goSetMenuValue(aCommand, "valueDefault");
+ return canRedo();
+ case "button_delete":
+ case "cmd_delete":
+ return calendarController.isCommandEnabled("calendar_delete_focused_item_command");
+ case "cmd_fullZoomReduce":
+ case "cmd_fullZoomEnlarge":
+ case "cmd_fullZoomReset":
+ return calendarController.isInMode("calendar") &&
+ currentView().supportsZoom;
+ case "cmd_properties":
+ case "cmd_printpreview":
+ return false;
+ case "cmd_showQuickFilterBar":
+ return calendarController.isInMode("task");
+ default:
+ return true;
+ }
+ },
+
+ doCommand: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_cut":
+ cutToClipboard();
+ break;
+ case "cmd_copy":
+ copyToClipboard();
+ break;
+ case "cmd_paste":
+ pasteFromClipboard();
+ break;
+ case "cmd_undo":
+ undo();
+ break;
+ case "cmd_redo":
+ redo();
+ break;
+ case "cmd_pageSetup":
+ PrintUtils.showPageSetup();
+ break;
+ case "button_print":
+ case "cmd_print":
+ calPrint();
+ break;
+
+ // Thunderbird commands
+ case "cmd_goForward":
+ currentView().moveView(1);
+ break;
+ case "cmd_goBack":
+ currentView().moveView(-1);
+ break;
+ case "cmd_fullZoomReduce":
+ currentView().zoomIn();
+ break;
+ case "cmd_fullZoomEnlarge":
+ currentView().zoomOut();
+ break;
+ case "cmd_fullZoomReset":
+ currentView().zoomReset();
+ break;
+ case "cmd_showQuickFilterBar":
+ document.getElementById("task-text-filter-field").select();
+ break;
+
+ case "button_delete":
+ case "cmd_delete":
+ calendarController.doCommand("calendar_delete_focused_item_command");
+ break;
+ }
+ }
+};
+
+/**
+ * Inserts the command controller into the document. On Lightning, also make
+ * sure that it is inserted before the conflicting thunderbird command
+ * controller.
+ */
+function injectCalendarCommandController() {
+ // We need to put our new command controller *before* the one that
+ // gets installed by thunderbird. Since we get called pretty early
+ // during startup we need to install the function below as a callback
+ // that periodically checks when the original thunderbird controller
+ // gets alive. Please note that setTimeout with a value of 0 means that
+ // we leave the current thread in order to re-enter the message loop.
+
+ let tbController = top.controllers.getControllerForCommand("cmd_runJunkControls");
+ if (tbController) {
+ calendarController.defaultController = tbController;
+ top.controllers.insertControllerAt(0, calendarController);
+ document.commandDispatcher.updateCommands("calendar_commands");
+ } else {
+ setTimeout(injectCalendarCommandController, 0);
+ }
+}
+
+/**
+ * Remove the calendar command controller from the document.
+ */
+function removeCalendarCommandController() {
+ top.controllers.removeController(calendarController);
+}
+
+/**
+ * Handler function to set up the item context menu, depending on the given
+ * items. Changes the delete menuitem to fit the passed items.
+ *
+ * @param event The DOM popupshowing event that is triggered by opening
+ * the context menu.
+ * @param items An array of items (usually the selected items) to adapt
+ * the context menu for.
+ * @return True, to show the popup menu.
+ */
+function setupContextItemType(event, items) {
+ function adaptModificationMenuItem(aMenuItemId, aItemType) {
+ let menuItem = document.getElementById(aMenuItemId);
+ if (menuItem) {
+ menuItem.setAttribute("label", calGetString("calendar", "delete" + aItemType + "Label"));
+ menuItem.setAttribute("accesskey", calGetString("calendar", "delete" + aItemType + "Accesskey"));
+ }
+ }
+ if (items.some(isEvent) && items.some(isToDo)) {
+ event.target.setAttribute("type", "mixed");
+ adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Item");
+ } else if (items.length && isEvent(items[0])) {
+ event.target.setAttribute("type", "event");
+ adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Event");
+ } else if (items.length && isToDo(items[0])) {
+ event.target.setAttribute("type", "todo");
+ adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Task");
+ } else {
+ event.target.removeAttribute("type");
+ adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Item");
+ }
+
+ let menu = document.getElementById("calendar-item-context-menu-attendance-menu");
+ setupAttendanceMenu(menu, items);
+
+ return true;
+}
+
+/**
+ * Shows the given date in the current view, if in calendar mode.
+ *
+ * XXX This function is misplaced, should go to calendar-views.js or a minimonth
+ * specific js file.
+ *
+ * @param aNewDate The new date as a JSDate.
+ */
+function minimonthPick(aNewDate) {
+ if (gCurrentMode == "calendar" || gCurrentMode == "task") {
+ let cdt = cal.jsDateToDateTime(aNewDate, currentView().timezone);
+ cdt.isDate = true;
+ currentView().goToDay(cdt);
+
+ // update date filter for task tree
+ let tree = document.getElementById("calendar-task-tree");
+ tree.updateFilter();
+ }
+}
+
+/**
+ * Selects all items, based on which mode we are currently in and what task tree is focused
+ */
+function selectAllItems() {
+ if (calendarController.todo_tasktree_focused) {
+ getTaskTree().selectAll();
+ } else if (calendarController.isInMode("calendar")) {
+ selectAllEvents();
+ }
+}
+
+/**
+ * Returns the selected items, based on which mode we are currently in and what task tree is focused
+ */
+function getSelectedItems() {
+ if (calendarController.todo_tasktree_focused) {
+ return getSelectedTasks();
+ }
+
+ return currentView().getSelectedItems({});
+}
+
+/**
+ * Deletes the selected items, based on which mode we are currently in and what task tree is focused
+ */
+function deleteSelectedItems() {
+ if (calendarController.todo_tasktree_focused) {
+ deleteToDoCommand();
+ } else if (calendarController.isInMode("calendar")) {
+ deleteSelectedEvents();
+ }
+}
+
+function calendarUpdateNewItemsCommand() {
+ // keep current current status
+ let oldEventValue = CalendarNewEventsCommandEnabled;
+ let oldTaskValue = CalendarNewTasksCommandEnabled;
+
+ // define command set to update
+ let eventCommands = ["calendar_new_event_command",
+ "calendar_new_event_context_command"];
+ let taskCommands = ["calendar_new_todo_command",
+ "calendar_new_todo_context_command",
+ "calendar_new_todo_todaypane_command"];
+
+ // re-calculate command status
+ CalendarNewEventsCommandEnabled = false;
+ CalendarNewTasksCommandEnabled = false;
+ let calendars = cal.getCalendarManager().getCalendars({}).filter(cal.isCalendarWritable).filter(userCanAddItemsToCalendar);
+ if (calendars.some(cal.isEventCalendar)) {
+ CalendarNewEventsCommandEnabled = true;
+ }
+ if (calendars.some(cal.isTaskCalendar)) {
+ CalendarNewTasksCommandEnabled = true;
+ }
+
+ // update command status if required
+ if (CalendarNewEventsCommandEnabled != oldEventValue) {
+ eventCommands.forEach(goUpdateCommand);
+ }
+ if (CalendarNewTasksCommandEnabled != oldTaskValue) {
+ taskCommands.forEach(goUpdateCommand);
+ }
+}
+
+function calendarUpdateDeleteCommand(selectedItems) {
+ let oldValue = CalendarDeleteCommandEnabled;
+ CalendarDeleteCommandEnabled = (selectedItems.length > 0);
+
+ /* we must disable "delete" when at least one item cannot be deleted */
+ for (let item of selectedItems) {
+ if (!userCanDeleteItemsFromCalendar(item.calendar)) {
+ CalendarDeleteCommandEnabled = false;
+ break;
+ }
+ }
+
+ if (CalendarDeleteCommandEnabled != oldValue) {
+ let commands = ["calendar_delete_event_command",
+ "calendar_delete_todo_command",
+ "calendar_delete_focused_item_command",
+ "button_delete",
+ "cmd_delete"];
+ for (let command of commands) {
+ goUpdateCommand(command);
+ }
+ }
+}
diff --git a/calendar/base/content/calendar-common-sets.xul b/calendar/base/content/calendar-common-sets.xul
new file mode 100644
index 000000000..aa7b1135c
--- /dev/null
+++ b/calendar/base/content/calendar-common-sets.xul
@@ -0,0 +1,577 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" > %calendarDTD;
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %eventDialogDTD;
+ <!ENTITY % menuOverlayDTD SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %menuOverlayDTD;
+]>
+
+<overlay id="calendar-common-sets-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <stringbundleset id="calendar_stringbundles">
+ <stringbundle id="bundle_branding" src="chrome://branding/locale/brand.properties"/>
+ </stringbundleset>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-common-sets.js"/>
+
+ <broadcasterset id="calendar_broadcasters">
+ <broadcaster id="modeBroadcaster" mode="calendar"/>
+ <broadcaster id="calendarviewBroadcaster"/>
+ <broadcaster id="unifinder-todo-filter-broadcaster"
+ persist="value"
+ value="throughcurrent"/>
+ </broadcasterset>
+
+ <commandset id="calendar_commands"
+ commandupdater="true"
+ events="calendar_commands"
+ oncommandupdate="calendarController.updateCommands()">
+ <command id="calendar_new_event_command" oncommand="goDoCommand('calendar_new_event_command')"/>
+ <command id="calendar_new_event_context_command" oncommand="goDoCommand('calendar_new_event_context_command')"/>
+ <command id="calendar_modify_event_command" oncommand="goDoCommand('calendar_modify_event_command')"/>
+ <command id="calendar_delete_event_command" oncommand="goDoCommand('calendar_delete_event_command')"/>
+
+ <command id="calendar_new_todo_command" oncommand="goDoCommand('calendar_new_todo_command')"/>
+ <command id="calendar_new_todo_context_command" oncommand="goDoCommand('calendar_new_todo_context_command')"/>
+ <command id="calendar_new_todo_todaypane_command" oncommand="goDoCommand('calendar_new_todo_todaypane_command')"/>
+ <command id="calendar_modify_todo_command" oncommand="goDoCommand('calendar_modify_todo_command')"/>
+ <command id="calendar_modify_todo_todaypane_command" oncommand="goDoCommand('calendar_modify_todo_todaypane_command')"/>
+ <command id="calendar_delete_todo_command" oncommand="goDoCommand('calendar_delete_todo_command')"/>
+
+ <command id="calendar_modify_focused_item_command" oncommand="goDoCommand('calendar_modify_focused_item_command')"/>
+ <command id="calendar_delete_focused_item_command" oncommand="goDoCommand('calendar_delete_focused_item_command')"/>
+
+ <command id="calendar_new_calendar_command" oncommand="goDoCommand('calendar_new_calendar_command')"/>
+ <command id="calendar_edit_calendar_command" oncommand="goDoCommand('calendar_edit_calendar_command')"/>
+ <command id="calendar_delete_calendar_command" oncommand="goDoCommand('calendar_delete_calendar_command')"/>
+
+ <command id="calendar_import_command" oncommand="goDoCommand('calendar_import_command')"/>
+ <command id="calendar_export_command" oncommand="goDoCommand('calendar_export_command')"/>
+ <command id="calendar_export_selection_command" oncommand="goDoCommand('calendar_export_selection_command')"/>
+
+ <command id="calendar_publish_selected_calendar_command" oncommand="goDoCommand('calendar_publish_selected_calendar_command')"/>
+ <command id="calendar_publish_calendar_command" oncommand="goDoCommand('calendar_publish_calendar_command')"/>
+ <command id="calendar_publish_selected_events_command" oncommand="goDoCommand('calendar_publish_selected_events_command')"/>
+
+ <command id="calendar_reload_remote_calendars" oncommand="goDoCommand('calendar_reload_remote_calendars')"/>
+
+ <command id="calendar_show_unifinder_command" oncommand="goDoCommand('calendar_show_unifinder_command')"/>
+ <!-- The dash instead of the underscore is intended. the 'xxx-view' part should be the id of the view in the deck -->
+ <command id="calendar_day-view_command" oncommand="goDoCommand('calendar_day-view_command')"/>
+ <command id="calendar_week-view_command" oncommand="goDoCommand('calendar_week-view_command')"/>
+ <command id="calendar_multiweek-view_command" oncommand="goDoCommand('calendar_multiweek-view_command')"/>
+ <command id="calendar_month-view_command" oncommand="goDoCommand('calendar_month-view_command')"/>
+ <command id="calendar_task_category_command"/>
+ <command id="calendar_toggle_completed_command" oncommand="toggleCompleted(event)"/>
+ <command id="calendar_percentComplete-0_command" oncommand="contextChangeTaskProgress(event, 0)"/>
+ <command id="calendar_percentComplete-25_command" oncommand="contextChangeTaskProgress(event, 25)"/>
+ <command id="calendar_percentComplete-50_command" oncommand="contextChangeTaskProgress(event, 50)"/>
+ <command id="calendar_percentComplete-75_command" oncommand="contextChangeTaskProgress(event, 75)"/>
+ <command id="calendar_percentComplete-100_command" oncommand="contextChangeTaskProgress(event, 100)"/>
+ <command id="calendar_priority-0_command" oncommand="contextChangeTaskPriority(event, 0)"/>
+ <command id="calendar_priority-9_command" oncommand="contextChangeTaskPriority(event, 9)"/>
+ <command id="calendar_priority-5_command" oncommand="contextChangeTaskPriority(event, 5)"/>
+ <command id="calendar_priority-1_command" oncommand="contextChangeTaskPriority(event, 1)"/>
+ <command id="calendar_general-priority_command" oncommand="goDoCommand('calendar_general-priority_command')"/>
+ <command id="calendar_general-progress_command" oncommand="goDoCommand('calendar_general-progress_command')"/>
+ <command id="calendar_general-postpone_command"/>
+ <command id="calendar_postpone-1hour_command" oncommand="contextPostponeTask(event, 'PT1H')"/>
+ <command id="calendar_postpone-1day_command" oncommand="contextPostponeTask(event, 'P1D')"/>
+ <command id="calendar_postpone-1week_command" oncommand="contextPostponeTask(event, 'P1W')"/>
+ <command id="calendar_toggle_orientation_command" persist="checked" oncommand="goDoCommand('calendar_toggle_orientation_command')"/>
+ <command id="calendar_toggle_workdays_only_command" persist="checked" oncommand="goDoCommand('calendar_toggle_workdays_only_command')"/>
+ <command id="calendar_toggle_tasks_in_view_command" persist="checked" oncommand="toggleTasksInView()"/>
+ <command id="calendar_toggle_show_completed_in_view_command" persist="checked" oncommand="toggleShowCompletedInView()"/>
+ <command id="calendar_toggle_calendarsidebar_command" oncommand="togglePaneSplitter('calsidebar_splitter')"/>
+ <command id="calendar_toggle_minimonthpane_command" oncommand="document.getElementById('minimonth-pane').togglePane(event)"/>
+ <command id="calendar_toggle_calendarlist_command" oncommand="document.getElementById('calendar-list-pane').togglePane(event)"/>
+ <command id="calendar_task_filter_command" oncommand="taskViewUpdate(event.explicitOriginalTarget.getAttribute('value'))"/>
+ <command id="calendar_task_filter_todaypane_command" oncommand="updateCalendarToDoUnifinder(event.explicitOriginalTarget.getAttribute('value'))"/>
+ <command id="calendar_toggle_filter_command" oncommand="document.getElementById('task-filter-pane').togglePane(event)"/>
+ <command id="calendar_view_next_command" oncommand="goDoCommand('calendar_view_next_command')"/>
+ <command id="calendar_view_today_command" oncommand="currentView().moveView()"/>
+ <command id="calendar_view_prev_command" oncommand="goDoCommand('calendar_view_prev_command')"/>
+
+ <!-- this is a pseudo-command that is disabled when in calendar mode -->
+ <command id="calendar_in_foreground"/>
+ <!-- this is a pseudo-command that is disabled when not in calendar mode -->
+ <command id="calendar_in_background"/>
+
+ <!-- These commands are enabled when in calendar or task mode, respectively -->
+ <command id="calendar_mode_calendar"/>
+ <command id="calendar_mode_task"/>
+
+ <command id="calendar_attendance_command"/>
+ </commandset>
+
+ <keyset id="calendar-keys">
+
+
+// For linux tab switching reservers alt+number, where on windows that's ctrl.
+// Use the available modifiers for each platform.
+// Can't use the OPTION key on OSX, so we will use SHIFT+OPTION on the Mac.
+#ifdef XP_UNIX
+// Linux
+#define CAL_VIEW_MODIFIERS accel
+#else
+// Windows
+#define CAL_VIEW_MODIFIERS alt
+#endif
+ <key id="calendar-day-view-key" key="1"
+ observes="calendar_day-view_command"
+#expand modifiers="__CAL_VIEW_MODIFIERS__"/>
+ <key id="calendar-week-view-key" key="2"
+ observes="calendar_week-view_command"
+#expand modifiers="__CAL_VIEW_MODIFIERS__"/>
+ <key id="calendar-multiweek-view-key" key="3"
+ observes="calendar_multiweek-view_command"
+#expand modifiers="__CAL_VIEW_MODIFIERS__"/>
+ <key id="calendar-month-view-key" key="4"
+ observes="calendar_month-view_command"
+#expand modifiers="__CAL_VIEW_MODIFIERS__"/>
+ <key id="calendar-go-to-today-key" keycode="VK_END" observes="calendar_go_to_today_command" modifiers="alt"/>
+ <key id="calendar-delete-item-key" keycode="VK_DELETE" observes="calendar_delete_event_command"/>
+ <key id="calendar-delete-todo-key" keycode="VK_DELETE" observes="calendar_delete_todo_command"/>
+ </keyset>
+
+ <popupset id="calendar-popupset">
+ <!-- Tooltips -->
+ <tooltip id="eventTreeTooltip"
+ onpopupshowing="return showToolTip(this, unifinderTreeView.getItemFromEvent(event))"
+ noautohide="true"/>
+
+ <tooltip id="taskTreeTooltip"
+ onpopupshowing="return showToolTip(this, getTaskTree().getTaskFromEvent(event))"
+ noautohide="true"/>
+
+ <tooltip id="itemTooltip"
+ noautohide="true"/>
+
+ <!-- CALENDAR ITEM CONTEXT MENU -->
+ <menupopup id="calendar-item-context-menu" onpopupshowing="return setupContextItemType(event, currentView().getSelectedItems({}));">
+ <menuitem id="calendar-item-context-menu-modify-menuitem"
+ label="&calendar.context.modifyorviewitem.label;"
+ accesskey="&calendar.context.modifyorviewitem.accesskey;"
+ observes="calendar_modify_event_command"/>
+ <menuitem id="calendar-item-context-menu-newevent-menutitem"
+ label="&calendar.context.newevent.label;"
+ accesskey="&calendar.context.newevent.accesskey;"
+ key="calendar-new-event-key"
+ observes="calendar_new_event_context_command"/>
+ <menuitem id="calendar-item-context-menu-newtodo-menuitem"
+ label="&calendar.context.newtodo.label;"
+ accesskey="&calendar.context.newtodo.accesskey;"
+ key="calendar-new-todo-key"
+ observes="calendar_new_todo_context_command"/>
+ <menuseparator id="calendar-item-context-menuseparator-adddeletemodify"/>
+ <menuitem id="calendar-item-context-menu-cut-menuitem"
+ label="&calendar.context.cutevent.label;"
+ accesskey="&calendar.context.cutevent.accesskey;"
+ key="key_cut"
+ observes="cmd_cut"
+ command="cmd_cut"/>
+ <menuitem id="calendar-item-context-menu-copy-menuitem"
+ label="&calendar.context.copyevent.label;"
+ accesskey="&calendar.context.copyevent.accesskey;"
+ key="key_copy"
+ observes="cmd_copy"
+ command="cmd_copy"/>
+ <menuitem id="calendar-item-context-menu-paste-menuitem"
+ label="&calendar.context.pasteevent.label;"
+ accesskey="&calendar.context.pasteevent.accesskey;"
+ key="key_paste"
+ observes="cmd_paste"
+ command="cmd_paste"/>
+ <menuseparator id="calendar-item-context-separator-cutcopypaste"/>
+ <menu id="calendar-item-context-menu-convert-menu"
+ label="&calendar.context.convertmenu.label;"
+ accesskey="&calendar.context.convertmenu.accesskey.calendar;">
+ <menupopup id="calendar-item-context-menu-convert-menupopup">
+ <menuitem id="calendar-view-context-menu-convert-message-menuitem"
+ label="&calendar.context.convertmenu.message.label;"
+ accesskey="&calendar.context.convertmenu.message.accesskey;"
+ oncommand="calendarMailButtonDNDObserver.onDropItems(currentView().getSelectedItems({}))"/>
+ <menuitem id="calendar-item-context-menu-convert-event-menuitem"
+ class="todo-only"
+ label="&calendar.context.convertmenu.event.label;"
+ accesskey="&calendar.context.convertmenu.event.accesskey;"
+ oncommand="calendarCalendarButtonDNDObserver.onDropItems(currentView().getSelectedItems({}))"/>
+ <menuitem id="calendar-item-context-menu-convert-task-menuitem"
+ class="event-only"
+ label="&calendar.context.convertmenu.task.label;"
+ accesskey="&calendar.context.convertmenu.task.accesskey;"
+ oncommand="calendarTaskButtonDNDObserver.onDropItems(currentView().getSelectedItems({}))"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="calendar-menuseparator-before-delete"/>
+ <!-- the label and accesskey of the following menuitem is set during runtime,
+ and depends on wether the item is a task or an event-->
+ <menuitem id="calendar-item-context-menu-delete-menuitem"
+ key="calendar-delete-item-key"
+ observes="calendar_delete_event_command"/>
+ <menu id="calendar-item-context-menu-attendance-menu"
+ class="attendance-menu"
+ label="&calendar.context.attendance.menu.label;"
+ accesskey="&calendar.context.attendance.menu.accesskey;"
+ oncommand="setContextPartstat(event.target.value, event.target.getAttribute('scope'), currentView().getSelectedItems({}))"
+ observes="calendar_attendance_command">
+ <menupopup id="calendar-item-context-menu-attendance-menupopup">
+ <label id="calendar-item-context-attendance-thisoccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.occurrence.label;"/>
+ <menuitem id="calendar-item-context-menu-attend-accept-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-item-context-attendance"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="calendar-item-context-menu-attend-tentative-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-item-context-attendance"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="calendar-item-context-menu-attend-declined-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-item-context-attendance"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="calendar-item-context-menu-attend-needsaction-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-item-context-attendance"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ <label id="calendar-item-context-attendance-alloccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.all.label;"/>
+ <menuitem id="calendar-item-context-menu-attend-accept-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-item-context-attendance-all"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="calendar-item-context-menu-attend-tentative-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-item-context-attendance-all"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="calendar-item-context-menu-attend-declined-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-item-context-attendance-all"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="calendar-item-context-menu-attend-needsaction-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-item-context-attendance-all"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <!-- CALENDAR VIEW CONTEXT MENU -->
+ <menupopup id="calendar-view-context-menu">
+ <menuitem id="calendar-view-context-menu-newevent"
+ label="&calendar.context.newevent.label;"
+ observes="calendar_new_event_context_command"
+ accesskey="&calendar.context.newevent.accesskey;"
+ key="calendar-new-event-key"/>
+ <menuitem id="calendar-view-context-menu-newtodo"
+ label="&calendar.context.newtodo.label;"
+ observes="calendar_new_todo_context_command"
+ accesskey="&calendar.context.newtodo.accesskey;"
+ key="calendar-new-todo-key"/>
+ <!-- These labels are set dynamically, based on the current view -->
+ <menuitem id="calendar-view-context-menu-previous"
+ label=""
+ accesskey=""
+ observes="calendar_view_prev_command"
+ label-day="&calendar.prevday.label;"
+ label-week="&calendar.prevweek.label;"
+ label-multiweek="&calendar.prevweek.label;"
+ label-month="&calendar.prevmonth.label;"
+ accesskey-day="&calendar.prevday.accesskey;"
+ accesskey-week="&calendar.prevweek.accesskey;"
+ accesskey-multiweek="&calendar.prevweek.accesskey;"
+ accesskey-month="&calendar.prevmonth.accesskey;"/>
+ <menuitem id="calendar-view-context-menu-next"
+ label=""
+ observes="calendar_view_next_command"
+ label-day="&calendar.nextday.label;"
+ label-week="&calendar.nextweek.label;"
+ label-multiweek="&calendar.nextweek.label;"
+ label-month="&calendar.nextmonth.label;"
+ accesskey-day="&calendar.nextday.accesskey;"
+ accesskey-week="&calendar.nextweek.accesskey;"
+ accesskey-multiweek="&calendar.nextweek.accesskey;"
+ accesskey-month="&calendar.nextmonth.accesskey;"/>
+ <menuseparator id="calendar-item-context-separator-cutcopypaste"/>
+ <!-- Cut and copy doesn't make sense in the views, but only showing paste
+ makes it look like something is missing. Disable by default. -->
+ <menuitem id="calendar-view-context-menu-cut-menuitem"
+ label="&calendar.context.cutevent.label;"
+ accesskey="&calendar.context.cutevent.accesskey;"
+ key="key_cut"
+ disabled="true"/>
+ <menuitem id="calendar-view-context-menu-copy-menuitem"
+ label="&calendar.context.copyevent.label;"
+ accesskey="&calendar.context.copyevent.accesskey;"
+ key="key_copy"
+ disabled="true"/>
+ <menuitem id="calendar-view-context-menu-paste-menuitem"
+ label="&calendar.context.pasteevent.label;"
+ accesskey="&calendar.context.pasteevent.accesskey;"
+ key="key_paste"
+ observes="cmd_paste"
+ command="cmd_paste"/>
+ </menupopup>
+
+ <!-- TASK ITEM CONTEXT MENU -->
+ <menupopup id="taskitem-context-menu"
+ onpopupshowing="changeContextMenuForTask(event);"
+ onpopuphiding="handleTaskContextMenuStateChange(event);">
+ <menuitem id="task-context-menu-modify"
+ label="&calendar.context.modifyorviewtask.label;"
+ accesskey="&calendar.context.modifyorviewtask.accesskey;"
+ command="calendar_modify_todo_command"
+ observes="calendar_modify_todo_command"/>
+ <menuitem id="task-context-menu-modify-todaypane"
+ label="&calendar.context.modifyorviewtask.label;"
+ accesskey="&calendar.context.modifyorviewtask.accesskey;"
+ command="calendar_modify_todo_todaypane_command"
+ observes="calendar_modify_todo_todaypane_command"/>
+ <menuitem id="task-context-menu-new"
+ label="&calendar.context.newtodo.label;"
+ accesskey="&calendar.context.newtodo.accesskey;"
+ key="calendar-new-todo-key"
+ command="calendar_new_todo_command"
+ observes="calendar_new_todo_command"/>
+ <menuitem id="task-context-menu-new-todaypane"
+ label="&calendar.context.newtodo.label;"
+ accesskey="&calendar.context.newtodo.accesskey;"
+ key="calendar-new-todo-key"
+ command="calendar_new_todo_todaypane_command"
+ observes="calendar_new_todo_todaypane_command"/>
+ <menuseparator id="task-context-menuseparator-cutcopypaste"/>
+ <menuitem id="task-context-menu-cut-menuitem"
+ label="&calendar.context.cutevent.label;"
+ accesskey="&calendar.context.cutevent.accesskey;"
+ key="key_cut"
+ observes="cmd_cut"
+ command="cmd_cut"/>
+ <menuitem id="task-context-menu-copy-menuitem"
+ label="&calendar.context.copyevent.label;"
+ accesskey="&calendar.context.copyevent.accesskey;"
+ key="key_copy"
+ observes="cmd_copy"
+ command="cmd_copy"/>
+ <menuitem id="task-context-menu-paste-menuitem"
+ label="&calendar.context.pasteevent.label;"
+ accesskey="&calendar.context.pasteevent.accesskey;"
+ key="key_paste"
+ observes="cmd_paste"
+ command="cmd_paste"/>
+ <menuseparator id="calendar-menuseparator-beforemarkcompleted"/>
+ <menuitem id="calendar-context-markcompleted"
+ type="checkbox"
+ autocheck="false"
+ label="&calendar.context.markcompleted.label;"
+ accesskey="&calendar.context.markcompleted.accesskey;"
+ observes="calendar_toggle_completed_command"
+ command="calendar_toggle_completed_command"/>
+ <menu id="task-context-menu-progress"
+ label="&calendar.context.progress.label;"
+ accesskey="&calendar.context.progress.accesskey;"
+ command="calendar_general-progress_command"
+ observes="calendar_general-progress_command">
+ <menupopup id="progress-menupopup" type="task-progress"/>
+ </menu>
+ <menu id="task-context-menu-priority"
+ label="&calendar.context.priority.label;"
+ accesskey="&calendar.context.priority.accesskey;"
+ command="calendar_general-priority_command"
+ observes="calendar_general-priority_command">
+ <menupopup id="priority-menupopup" type="task-priority"/>
+ </menu>
+ <menu id="task-context-menu-postpone"
+ label="&calendar.context.postpone.label;"
+ accesskey="&calendar.context.postpone.accesskey;"
+ command="calendar_general-postpone_command"
+ observes="calendar_general-postpone_command">
+ <menupopup id="task-context-postpone-menupopup">
+ <menuitem id="task-context-postpone-1hour"
+ label="&calendar.context.postpone.1hour.label;"
+ accesskey="&calendar.context.postpone.1hour.accesskey;"
+ observes="calendar_postpone-1hour_command"/>
+ <menuitem id="task-context-postpone-1day"
+ label="&calendar.context.postpone.1day.label;"
+ accesskey="&calendar.context.postpone.1day.accesskey;"
+ observes="calendar_postpone-1day_command"/>
+ <menuitem id="task-context-postpone-1week"
+ label="&calendar.context.postpone.1week.label;"
+ accesskey="&calendar.context.postpone.1week.accesskey;"
+ observes="calendar_postpone-1week_command"/>
+ </menupopup>
+ </menu>
+ <menu id="calendar-context-calendar-menu"
+ label="&calendar.calendar.label;"
+ accesskey="&calendar.calendar.accesskey;">
+ <menupopup id="calendar-context-calendar-menupopup"
+ onpopupshowing="addCalendarNames(event);"/>
+ </menu>
+ <menuseparator id="task-context-menu-separator-conversion"/>
+ <menu id="task-context-menu-convert"
+ label="&calendar.context.convertmenu.label;"
+ accesskey="&calendar.context.convertmenu.accesskey.calendar;">
+ <menupopup id="task-context-convert-menupopup">
+ <menuitem id="calendar-context-converttomessage"
+ label="&calendar.context.convertmenu.message.label;"
+ accesskey="&calendar.context.convertmenu.message.accesskey;"
+ oncommand="tasksToMail(event)"/>
+ <menuitem id="calendar-context-converttoevent"
+ label="&calendar.context.convertmenu.event.label;"
+ accesskey="&calendar.context.convertmenu.event.accesskey;"
+ oncommand="tasksToEvents(event)"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem id="task-context-menu-delete"
+ label="&calendar.context.deletetask.label;"
+ accesskey="&calendar.context.deletetask.accesskey;"
+ command="calendar_delete_todo_command"
+ observes="calendar_delete_todo_command"/>
+ <menu id="task-context-menu-attendance-menu"
+ class="attendance-menu"
+ label="&calendar.context.attendance.menu.label;"
+ accesskey="&calendar.context.attendance.menu.accesskey;"
+ oncommand="setContextPartstat(event.target.value, event.target.getAttribute('scope'), getSelectedTasks())"
+ observes="calendar_attendance_command">
+ <menupopup id="task-context-menu-attendance-menupopup">
+ <label id="task-context-attendance-thisoccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.occurrence.label;"/>
+ <menuitem id="task-context-menu-attend-accept-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="task-context-attendance"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="task-context-menu-attend-tentative-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="task-context-attendance"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="task-context-menu-attend-declined-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="task-context-attendance"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="task-context-menu-attend-needsaction-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="task-context-attendance"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ <label id="task-context-attendance-alloccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.all.label;"/>
+ <menuitem id="task-context-menu-attend-accept-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="task-context-attendance-all"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="task-context-menu-attend-tentative-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="task-context-attendance-all"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="task-context-menu-attend-declined-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="task-context-attendance-all"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="task-context-menu-attend-needsaction-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="task-context-attendance-all"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="task-context-menu-separator-filter"/>
+ <menu id="task-context-menu-filter-todaypane"
+ label="&calendar.tasks.view.filtertasks.label;"
+ accesskey="&calendar.tasks.view.filtertasks.accesskey;">
+ <menupopup id="task-context-menu-filter-todaypane-popup">
+ <observes element="unifinder-todo-filter-broadcaster"
+ attribute="value"
+ onbroadcast="checkRadioControl(this.parentNode, document.getElementById(this.getAttribute('element')).getAttribute('value'));"/>
+ <menuitem id="task-context-menu-filter-todaypane-current"
+ name="filtergrouptodaypane"
+ value="throughcurrent"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.current.label;"
+ accesskey="&calendar.task.filter.current.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-today"
+ name="filtergrouptodaypane"
+ value="throughtoday"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.today.label;"
+ accesskey="&calendar.task.filter.today.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-next7days"
+ name="filtergrouptodaypane"
+ value="throughsevendays"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.next7days.label;"
+ accesskey="&calendar.task.filter.next7days.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-notstarted"
+ name="filtergrouptodaypane"
+ value="notstarted"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.notstarted.label;"
+ accesskey="&calendar.task.filter.notstarted.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-overdue"
+ name="filtergrouptodaypane"
+ value="overdue"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.overdue.label;"
+ accesskey="&calendar.task.filter.overdue.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-completed"
+ name="filtergrouptodaypane"
+ type="radio"
+ value="completed"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.completed.label;"
+ accesskey="&calendar.task.filter.completed.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-open"
+ name="filtergrouptodaypane"
+ type="radio"
+ value="open"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.open.label;"
+ accesskey="&calendar.task.filter.open.accesskey;"/>
+ <menuitem id="task-context-menu-filter-todaypane-all"
+ name="filtergrouptodaypane"
+ value="all"
+ type="radio"
+ command="calendar_task_filter_todaypane_command"
+ label="&calendar.task.filter.all.label;"
+ accesskey="&calendar.task.filter.all.accesskey;"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <!-- TASKVIEW LINK CONTEXT MENU -->
+ <menupopup id="taskview-link-context-menu">
+ <menuitem id="taskview-link-context-menu-copy"
+ label="&calendar.copylink.label;"
+ accesskey="&calendar.copylink.accesskey;"
+ oncommand="taskViewCopyLink(document.popupNode)"/>
+ </menupopup>
+ </popupset>
+</overlay>
diff --git a/calendar/base/content/calendar-daypicker.xml b/calendar/base/content/calendar-daypicker.xml
new file mode 100644
index 000000000..17f217b18
--- /dev/null
+++ b/calendar/base/content/calendar-daypicker.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!--
+ ########################################################################
+ ## daypicker
+ ########################################################################
+ -->
+
+ <binding id="daypicker" display="xul:button"
+ extends="chrome://global/content/bindings/button.xml#button-base">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/calendar-daypicker.css"/>
+ </resources>
+ <content>
+ <xul:hbox anonid="daypickerId" class="daypickerclass" align="center" flex="1">
+ <xul:label anonid="daytext"
+ class="toolbarbutton-text"
+ flex="1"
+ xbl:inherits="value=label"/>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <method name="onmodified">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.attrName == "checked") {
+ let event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.calendar.dispatchEvent(event);
+ }
+ ]]></body>
+ </method>
+ <constructor><![CDATA[
+ this.setAttribute("autoCheck", "true");
+ this.setAttribute("type", "checkbox");
+ this.addEventListener("DOMAttrModified", this.onmodified, false);
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <!--
+ ########################################################################
+ ## daypicker-weekday
+ ########################################################################
+ -->
+
+ <binding id="daypicker-weekday" extends="xul:box">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/calendar-daypicker.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox anonid="mainbox" flex="1">
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" xbl:inherits="disabled,mode=id"/>
+ <xul:daypicker bottom="true" right="true" xbl:inherits="disabled,mode=id"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <!--
+ The weekday-picker manages an array of selected days of the week and
+ the 'days' property is the interface to this array. the expected argument is
+ an array containing integer elements, where each element represents a selected
+ day of the week, starting with SUNDAY=1.
+ -->
+ <property name="days">
+ <setter><![CDATA[
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numChilds = mainbox.childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let child = mainbox.childNodes[i];
+ child.removeAttribute("checked");
+ }
+ for (let i = 0; i < val.length; i++) {
+ let index = val[i] - 1 - this.weekStartOffset;
+ if (index < 0) {
+ index += 7;
+ }
+ mainbox.childNodes[index].setAttribute("checked", "true");
+ }
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numChilds = mainbox.childNodes.length;
+ let days = [];
+ for (let i = 0; i < numChilds; i++) {
+ let child = mainbox.childNodes[i];
+ if (child.getAttribute("checked") == "true") {
+ let index = i + this.weekStartOffset;
+ if (index >= 7) {
+ index -= 7;
+ }
+ days.push(index + 1);
+ }
+ }
+ return days;
+ ]]></getter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.weekStartOffset = Preferences.get("calendar.week.start", 0);
+
+ let props =
+ Services.strings.createBundle(
+ "chrome://calendar/locale/dateFormat.properties");
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numChilds = mainbox.childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let child = mainbox.childNodes[i];
+ let dow = i + this.weekStartOffset;
+ if (dow >= 7) {
+ dow -= 7;
+ }
+ let day = props.GetStringFromName("day." + (dow + 1) + ".Mmm");
+ child.label = day;
+ child.calendar = this;
+ }
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <!--
+ ########################################################################
+ ## daypicker-monthday
+ ########################################################################
+ -->
+
+ <binding id="daypicker-monthday" extends="xul:box">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/calendar-daypicker.css"/>
+ </resources>
+
+ <content>
+ <xul:vbox anonid="mainbox" class="daypicker-monthday-mainbox" flex="1" >
+ <xul:hbox class="daypicker-row" flex="1">
+ <daypicker label="1" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="2" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="3" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="4" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="5" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="6" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="7" right="true" xbl:inherits="disabled, mode=id"/>
+ </xul:hbox>
+ <xul:hbox class="daypicker-row" flex="1">
+ <daypicker label="8" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="9" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="10" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="11" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="12" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="13" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="14" right="true" xbl:inherits="disabled, mode=id"/>
+ </xul:hbox>
+ <xul:hbox class="daypicker-row" flex="1">
+ <daypicker label="15" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="16" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="17" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="18" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="19" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="20" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="21" right="true" xbl:inherits="disabled, mode=id"/>
+ </xul:hbox>
+ <xul:hbox class="daypicker-row" flex="1">
+ <daypicker label="22" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="23" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="24" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="25" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="26" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="27" xbl:inherits="disabled, mode=id"/>
+ <daypicker label="28" right="true" xbl:inherits="disabled, mode=id"/>
+ </xul:hbox>
+ <xul:hbox class="daypicker-row" flex="1">
+ <daypicker bottom="true" label="29" xbl:inherits="disabled, mode=id"/>
+ <daypicker bottom="true" label="30" xbl:inherits="disabled, mode=id"/>
+ <daypicker bottom="true" label="31" xbl:inherits="disabled, mode=id"/>
+ <daypicker bottom="true" right="true" label="" xbl:inherits="disabled, mode=id"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <property name="days">
+ <setter><![CDATA[
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numRows = mainbox.childNodes.length;
+ let days = [];
+ for (let i = 0; i < numRows; i++) {
+ let row = mainbox.childNodes[i];
+ let numChilds = row.childNodes.length;
+ for (let j = 0; j < numChilds; j++) {
+ let child = row.childNodes[j];
+ child.removeAttribute("checked");
+ days.push(child);
+ }
+ }
+ for (let i = 0; i < val.length; i++) {
+ let lastDayOffset = val[i] == -1 ? 0 : -1;
+ let index = (val[i] < 0 ? val[i] + days.length + lastDayOffset
+ : val[i] - 1);
+ days[index].setAttribute("checked", "true");
+ }
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numRows = mainbox.childNodes.length;
+ let days = [];
+ for (let i = 0; i < numRows; i++) {
+ let row = mainbox.childNodes[i];
+ let numChilds = row.childNodes.length;
+ for (let j = 0; j < numChilds; j++) {
+ let child = row.childNodes[j];
+ if (child.getAttribute("checked") == "true") {
+ days.push(Number(child.label) ? Number(child.label) : -1);
+ }
+ }
+ }
+ return days;
+ ]]></getter>
+ </property>
+
+ <constructor><![CDATA[
+ let mainbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+ let numRows = mainbox.childNodes.length;
+ let child = null;
+ for (let i = 0; i < numRows; i++) {
+ let row = mainbox.childNodes[i];
+ let numChilds = row.childNodes.length;
+ for (let j = 0; j < numChilds; j++) {
+ child = row.childNodes[j];
+ child.calendar = this;
+ }
+ }
+ let labelLastDay = calGetString("calendar-event-dialog", "eventRecurrenceMonthlyLastDayLabel");
+ child.setAttribute("label", labelLastDay);
+ ]]></constructor>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-dnd-listener.js b/calendar/base/content/calendar-dnd-listener.js
new file mode 100644
index 000000000..9901a0332
--- /dev/null
+++ b/calendar/base/content/calendar-dnd-listener.js
@@ -0,0 +1,596 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var itemConversion = {
+
+ /**
+ * Converts an email message to a calendar item.
+ *
+ * @param aItem The target calIItemBase.
+ * @param aMessage The nsIMsgHdr to convert from.
+ */
+ calendarItemFromMessage: function iC_calendarItemFromMessage(aItem, aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ aItem.calendar = getSelectedCalendar();
+ aItem.title = aMsgHdr.mime2DecodedSubject;
+
+ cal.setDefaultStartEndHour(aItem);
+ cal.alarms.setDefaultValues(aItem);
+
+ let messenger = Components.classes["@mozilla.org/messenger;1"]
+ .createInstance(Components.interfaces.nsIMessenger);
+ let streamListener = Components.classes["@mozilla.org/network/sync-stream-listener;1"]
+ .createInstance(Components.interfaces.nsISyncStreamListener);
+ messenger.messageServiceFromURI(msgUri).streamMessage(msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false);
+
+ let plainTextMessage = "";
+ plainTextMessage = msgFolder.getMsgTextFromStream(streamListener.inputStream,
+ aMsgHdr.Charset,
+ 65536,
+ 32768,
+ false,
+ true,
+ {});
+ aItem.setProperty("DESCRIPTION", plainTextMessage);
+ },
+
+ /**
+ * Copy base item properties from aItem to aTarget. This includes properties
+ * like title, location, description, priority, transparency,
+ * attendees, categories, calendar, recurrence and possibly more.
+ *
+ * @param aItem The item to copy from.
+ * @param aTarget the item to copy to.
+ */
+ copyItemBase: function iC_copyItemBase(aItem, aTarget) {
+ const copyProps = ["SUMMARY", "LOCATION", "DESCRIPTION",
+ "URL", "CLASS", "PRIORITY"];
+
+ for (var prop of copyProps) {
+ aTarget.setProperty(prop, aItem.getProperty(prop));
+ }
+
+ // Attendees
+ var attendees = aItem.getAttendees({});
+ for (var attendee of attendees) {
+ aTarget.addAttendee(attendee.clone());
+ }
+
+ // Categories
+ var categories = aItem.getCategories({});
+ aTarget.setCategories(categories.length, categories);
+
+ // Organizer
+ aTarget.organizer = (aItem.organizer ? aItem.organizer.clone() : null);
+
+ // Calendar
+ aTarget.calendar = getSelectedCalendar();
+
+ // Recurrence
+ if (aItem.recurrenceInfo) {
+ aTarget.recurrenceInfo = aItem.recurrenceInfo.clone();
+ aTarget.recurrenceInfo.item = aTarget;
+ }
+ },
+
+ /**
+ * Creates a task from the passed event. This function copies the base item
+ * and a few event specific properties (dates, alarms, ...).
+ *
+ * @param aEvent The event to copy from.
+ * @return The resulting task.
+ */
+ taskFromEvent: function iC_taskFromEvent(aEvent) {
+ let item = cal.createTodo();
+
+ this.copyItemBase(aEvent, item);
+
+ // Dates and alarms
+ if (!aEvent.startDate.isDate && !aEvent.endDate.isDate) {
+ // Dates
+ item.entryDate = aEvent.startDate.clone();
+ item.dueDate = aEvent.endDate.clone();
+
+ // Alarms
+ for (let alarm of aEvent.getAlarms({})) {
+ item.addAlarm(alarm.clone());
+ }
+ item.alarmLastAck = (aEvent.alarmLastAck ?
+ aEvent.alarmLastAck.clone() :
+ null);
+ }
+
+ // Map Status values
+ let statusMap = {
+ "TENTATIVE": "NEEDS-ACTION",
+ "CONFIRMED": "IN-PROCESS",
+ "CANCELLED": "CANCELLED"
+ };
+ if (aEvent.getProperty("STATUS") in statusMap) {
+ item.setProperty("STATUS", statusMap[aEvent.getProperty("STATUS")]);
+ }
+ return item;
+ },
+
+ /**
+ * Creates an event from the passed task. This function copies the base item
+ * and a few task specific properties (dates, alarms, ...). If the task has
+ * no due date, the default event length is used.
+ *
+ * @param aTask The task to copy from.
+ * @return The resulting event.
+ */
+ eventFromTask: function iC_eventFromTask(aTask) {
+ let item = cal.createEvent();
+
+ this.copyItemBase(aTask, item);
+
+ // Dates and alarms
+ item.startDate = aTask.entryDate;
+ if (!item.startDate) {
+ if (aTask.dueDate) {
+ item.startDate = aTask.dueDate.clone();
+ item.startDate.minute -= Preferences.get("calendar.event.defaultlength", 60);
+ } else {
+ item.startDate = cal.getDefaultStartDate();
+ }
+ }
+
+ item.endDate = aTask.dueDate;
+ if (!item.endDate) {
+ // Make the event be the default event length if no due date was
+ // specified.
+ item.endDate = item.startDate.clone();
+ item.endDate.minute += Preferences.get("calendar.event.defaultlength", 60);
+ }
+
+ // Alarms
+ for (let alarm of aTask.getAlarms({})) {
+ item.addAlarm(alarm.clone());
+ }
+ item.alarmLastAck = (aTask.alarmLastAck ?
+ aTask.alarmLastAck.clone() :
+ null);
+
+ // Map Status values
+ let statusMap = {
+ "NEEDS-ACTION": "TENTATIVE",
+ "COMPLETED": "CONFIRMED",
+ "IN-PROCESS": "CONFIRMED",
+ "CANCELLED": "CANCELLED"
+ };
+ if (aTask.getProperty("STATUS") in statusMap) {
+ item.setProperty("STATUS", statusMap[aTask.getProperty("STATUS")]);
+ }
+ return item;
+ }
+};
+
+/**
+ * A base class for drag and drop observers
+ * @class calDNDBaseObserver
+ */
+function calDNDBaseObserver() {
+ ASSERT(false, "Inheriting objects call calDNDBaseObserver!");
+}
+
+calDNDBaseObserver.prototype = {
+ // initialize this class's members
+ initBase: function calDNDInitBase() {
+ },
+
+ getSupportedFlavours: function calDNDGetFlavors() {
+ var flavourSet = new FlavourSet();
+ flavourSet.appendFlavour("text/calendar");
+ flavourSet.appendFlavour("text/x-moz-url");
+ flavourSet.appendFlavour("text/x-moz-message");
+ flavourSet.appendFlavour("text/unicode");
+ flavourSet.appendFlavour("application/x-moz-file");
+ return flavourSet;
+ },
+
+ /**
+ * Action to take when dropping the event.
+ */
+
+ onDrop: function calDNDDrop(aEvent, aTransferData, aDragSession) {
+ var transferable = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+ transferable.init(null);
+ transferable.addDataFlavor("text/calendar");
+ transferable.addDataFlavor("text/x-moz-url");
+ transferable.addDataFlavor("text/x-moz-message");
+ transferable.addDataFlavor("text/unicode");
+ transferable.addDataFlavor("application/x-moz-file");
+
+ aDragSession.getData(transferable, 0);
+
+ var data = new Object();
+ var bestFlavor = new Object();
+ var length = new Object();
+ transferable.getAnyTransferData(bestFlavor, data, length);
+
+ try {
+ data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
+ } catch (exc) {
+ // we currently only supports strings:
+ return;
+ }
+
+ // Treat unicode data with VEVENT in it as text/calendar
+ if (bestFlavor.value == "text/unicode" && data.toString().includes("VEVENT")) {
+ bestFlavor.value = "text/calendar";
+ }
+
+ var destCal = getSelectedCalendar();
+ switch (bestFlavor.value) {
+ case "text/calendar":
+ var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ parser.parseString(data);
+ this.onDropItems(parser.getItems({}).concat(parser.getParentlessItems({})));
+ break;
+ case "text/unicode":
+ var droppedUrl = this.retrieveURLFromData(data, bestFlavor.value);
+ if (!droppedUrl)
+ return;
+
+ var url = makeURL(droppedUrl);
+
+ var localFileInstance = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ localFileInstance.initWithPath(url.path);
+
+ var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ inputStream.init(localFileInstance,
+ MODE_RDONLY,
+ parseInt("0444", 8),
+ {});
+
+ try {
+ //XXX support csv
+ var importer = Components.classes["@mozilla.org/calendar/import;1?type=ics"]
+ .getService(Components.interfaces.calIImporter);
+ var items = importer.importFromStream(inputStream, {});
+ this.onDropItems(items);
+ }
+ finally {
+ inputStream.close();
+ }
+
+ break;
+ case "application/x-moz-file-promise":
+ case "text/x-moz-url":
+ var uri = Services.io.newURI(data.toString(), null, null);
+ var loader = Components.classes["@mozilla.org/network/unichar-stream-loader;1"]
+ .createInstance(Components.interfaces.nsIUnicharStreamLoader);
+ var channel = Services.io.newChannelFromURI2(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_NORMAL,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+ channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
+
+ var self = this;
+
+ var listener = {
+
+ // nsIUnicharStreamLoaderObserver:
+ onDetermineCharset: function(loader, context, firstSegment, length) {
+ var charset = null;
+ if (loader && loader.channel) {
+ charset = channel.contentCharset;
+ }
+ if (!charset || charset.length == 0) {
+ charset = "UTF-8";
+ }
+ return charset;
+ },
+
+ onStreamComplete: function(loader, context, status, unicharString) {
+ var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ parser.parseString(unicharString);
+ self.onDropItems(parser.getItems({}).concat(parser.getParentlessItems({})));
+ }
+ };
+
+ try {
+ loader.init(listener, Components.interfaces.nsIUnicharStreamLoader.DEFAULT_SEGMENT_SIZE);
+ channel.asyncOpen(loader, null);
+ } catch(e) {
+ Components.utils.reportError(e)
+ }
+ break;
+ case "text/x-moz-message":
+ this.onDropMessage(messenger.msgHdrFromURI(data));
+ break;
+ default:
+ ASSERT(false, "unknown data flavour:" + bestFlavor.value+'\n');
+ break;
+ }
+ },
+
+ onDragStart: function calDNDStart(aEvent, aTransferData, aDragAction) {},
+ onDragOver: function calDNDOver(aEvent, aFlavor, aDragSession) {},
+ onDragExit: function calDNDExit(aEvent, aDragSession) {},
+
+ onDropItems: function calDNDDropItems(aItems) {},
+ onDropMessage: function calDNDDropMessage(aMessage) {},
+
+
+ retrieveURLFromData: function calDNDRetrieveURL(aData, aFlavor) {
+ var data;
+ switch (aFlavor) {
+ case "text/unicode":
+ data = aData.toString();
+ var separator = data.indexOf("\n");
+ if (separator != -1)
+ data = data.substr(0, separator);
+ return data;
+ case "application/x-moz-file":
+ return aData.URL;
+ default:
+ return null;
+ }
+ }
+};
+
+/**
+ * calViewDNDObserver::calViewDNDObserver
+ *
+ * Drag'n'drop handler for the calendar views. This handler is
+ * derived from the base handler and just implements specific actions.
+ */
+function calViewDNDObserver() {
+ this.wrappedJSObject = this;
+ this.initBase();
+}
+
+calViewDNDObserver.prototype = {
+ __proto__: calDNDBaseObserver.prototype,
+
+ /**
+ * calViewDNDObserver::onDropItems
+ *
+ * Gets called in case we're dropping an array of items
+ * on one of the calendar views. In this case we just
+ * try to add these items to the currently selected calendar.
+ */
+ onDropItems: function(aItems) {
+ var destCal = getSelectedCalendar();
+ startBatchTransaction();
+ try {
+ for (var item of aItems) {
+ doTransaction('add', item, destCal, null, null);
+ }
+ }
+ finally {
+ endBatchTransaction();
+ }
+ }
+};
+
+/**
+ * calMailButtonDNDObserver::calMailButtonDNDObserver
+ *
+ * Drag'n'drop handler for the 'mail mode'-button. This handler is
+ * derived from the base handler and just implements specific actions.
+ */
+function calMailButtonDNDObserver() {
+ this.wrappedJSObject = this;
+ this.initBase();
+}
+
+calMailButtonDNDObserver.prototype = {
+ __proto__: calDNDBaseObserver.prototype,
+
+ /**
+ * calMailButtonDNDObserver::onDropItems
+ *
+ * Gets called in case we're dropping an array of items
+ * on the 'mail mode'-button.
+ *
+ * @param aItems An array of items to handle.
+ */
+ onDropItems: function(aItems) {
+ if (aItems && aItems.length > 0) {
+ let item = aItems[0];
+ let recipients = cal.getRecipientList(item.getAttendees({}));
+ let identity = item.calendar.getProperty("imip.identity");
+ sendMailTo(recipients, item.title, item.getProperty("DESCRIPTION"), identity);
+ }
+ },
+
+ /**
+ * calMailButtonDNDObserver::onDropMessage
+ *
+ * Gets called in case we're dropping a message
+ * on the 'mail mode'-button.
+ *
+ * @param aMessage The message to handle.
+ */
+ onDropMessage: function(aMessage) {
+ }
+};
+
+/**
+ * calCalendarButtonDNDObserver::calCalendarButtonDNDObserver
+ *
+ * Drag'n'drop handler for the 'calendar mode'-button. This handler is
+ * derived from the base handler and just implements specific actions.
+ */
+function calCalendarButtonDNDObserver() {
+ this.wrappedJSObject = this;
+ this.initBase();
+}
+
+calCalendarButtonDNDObserver.prototype = {
+ __proto__: calDNDBaseObserver.prototype,
+
+ /**
+ * calCalendarButtonDNDObserver::onDropItems
+ *
+ * Gets called in case we're dropping an array of items
+ * on the 'calendar mode'-button.
+ *
+ * @param aItems An array of items to handle.
+ */
+ onDropItems: function(aItems) {
+ for (var item of aItems) {
+ var newItem = item;
+ if (isToDo(item)) {
+ newItem = itemConversion.eventFromTask(item);
+ }
+ createEventWithDialog(null, null, null, null, newItem);
+ }
+ },
+
+ /**
+ * calCalendarButtonDNDObserver::onDropMessage
+ *
+ * Gets called in case we're dropping a message on the
+ * 'calendar mode'-button. In this case we create a new
+ * event from the mail. We open the default event dialog
+ * and just use the subject of the message as the event title.
+ *
+ * @param aMessage The message to handle.
+ */
+ onDropMessage: function(aMessage) {
+ var newItem = createEvent();
+ itemConversion.calendarItemFromMessage(newItem, aMessage);
+ createEventWithDialog(null, null, null, null, newItem);
+ }
+};
+
+/**
+ * calTaskButtonDNDObserver::calTaskButtonDNDObserver
+ *
+ * Drag'n'drop handler for the 'task mode'-button. This handler is
+ * derived from the base handler and just implements specific actions.
+ */
+function calTaskButtonDNDObserver() {
+ this.wrappedJSObject = this;
+ this.initBase();
+}
+
+calTaskButtonDNDObserver.prototype = {
+ __proto__: calDNDBaseObserver.prototype,
+
+ /**
+ * calTaskButtonDNDObserver::onDropItems
+ *
+ * Gets called in case we're dropping an array of items
+ * on the 'task mode'-button.
+ *
+ * @param aItems An array of items to handle.
+ */
+ onDropItems: function(aItems) {
+ for (var item of aItems) {
+ var newItem = item;
+ if (isEvent(item)) {
+ newItem = itemConversion.taskFromEvent(item);
+ }
+ createTodoWithDialog(null, null, null, newItem);
+ }
+ },
+
+ /**
+ * calTaskButtonDNDObserver::onDropMessage
+ *
+ * Gets called in case we're dropping a message
+ * on the 'task mode'-button.
+ *
+ * @param aMessage The message to handle.
+ */
+ onDropMessage: function(aMessage) {
+ var todo = createTodo();
+ itemConversion.calendarItemFromMessage(todo, aMessage);
+ createTodoWithDialog(null, null, null, todo);
+ }
+};
+
+/**
+ * Invoke a drag session for the passed item. The passed box will be used as a
+ * source.
+ *
+ * @param aItem The item to drag.
+ * @param aXULBox The XUL box to invoke the drag session from.
+ */
+function invokeEventDragSession(aItem, aXULBox) {
+ let transfer = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+ transfer.init(null);
+ transfer.addDataFlavor("text/calendar");
+
+ let flavourProvider = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIFlavorDataProvider]),
+
+ item: aItem,
+ getFlavorData: function(aInTransferable, aInFlavor, aOutData, aOutDataLen) {
+ if ((aInFlavor == "application/vnd.x-moz-cal-event") ||
+ (aInFlavor == "application/vnd.x-moz-cal-task")) {
+ aOutData.value = aItem;
+ aOutDataLen.value = 1;
+ } else {
+ ASSERT(false, "error:" + aInFlavor);
+ }
+ }
+ };
+
+ if (isEvent(aItem)) {
+ transfer.addDataFlavor("application/vnd.x-moz-cal-event");
+ transfer.setTransferData("application/vnd.x-moz-cal-event", flavourProvider, 0);
+ } else if (isToDo(aItem)) {
+ transfer.addDataFlavor("application/vnd.x-moz-cal-task");
+ transfer.setTransferData("application/vnd.x-moz-cal-task", flavourProvider, 0);
+ }
+
+ // Also set some normal data-types, in case we drag into another app
+ let serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
+ .createInstance(Components.interfaces.calIIcsSerializer);
+ serializer.addItems([aItem], 1);
+
+ let supportsString = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Components.interfaces.nsISupportsString);
+ supportsString.data = serializer.serializeToString();
+ transfer.setTransferData("text/calendar", supportsString, supportsString.data.length * 2);
+ transfer.setTransferData("text/unicode", supportsString, supportsString.data.length * 2);
+
+ let action = Components.interfaces.nsIDragService.DRAGDROP_ACTION_MOVE;
+ let supArray = Components.classes["@mozilla.org/supports-array;1"]
+ .createInstance(Components.interfaces.nsISupportsArray);
+ supArray.AppendElement(transfer);
+ aXULBox.sourceObject = aItem;
+ try {
+ cal.getDragService().invokeDragSession(aXULBox, supArray, null, action);
+ } catch (error) {
+ // Nothing done here because we only have to catch an exception that occurs when dragging
+ // is cancelled with ESC. This is an odd behaviour of the nativeDragService which we have
+ // have to cover.
+ // Therefore the DND API for calendar should be changed to the new DOM driven DND-API
+ // sometime.
+ }
+}
+
+var calendarViewDNDObserver = new calViewDNDObserver();
+var calendarMailButtonDNDObserver = new calMailButtonDNDObserver();
+var calendarCalendarButtonDNDObserver = new calCalendarButtonDNDObserver();
+var calendarTaskButtonDNDObserver = new calTaskButtonDNDObserver();
diff --git a/calendar/base/content/calendar-extract.js b/calendar/base/content/calendar-extract.js
new file mode 100644
index 000000000..8ceb3be7e
--- /dev/null
+++ b/calendar/base/content/calendar-extract.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calExtract.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var calendarExtract = {
+ onShowLocaleMenu: function(target) {
+ let localeList = document.getElementById(target.id);
+ let langs = [];
+ let chrome = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIXULChromeRegistry);
+ chrome.QueryInterface(Components.interfaces.nsIToolkitChromeRegistry);
+ let locales = chrome.getLocalesForPackage("calendar");
+ let langRegex = /^(([^-]+)-*(.*))$/;
+
+ while (locales.hasMore()) {
+ let localeParts = langRegex.exec(locales.getNext());
+ let langName = localeParts[2];
+
+ try {
+ langName = cal.calGetString("languageNames", langName, null, "global");
+ } catch (ex) {
+ // If no language name is found that is ok, keep the technical term
+ }
+
+ let label = cal.calGetString("calendar", "extractUsing", [langName]);
+ if (localeParts[3] != "") {
+ label = cal.calGetString("calendar", "extractUsingRegion", [langName, localeParts[3]]);
+ }
+
+ langs.push([label, localeParts[1]]);
+ }
+
+ // sort
+ let pref = "calendar.patterns.last.used.languages";
+ let lastUsedLangs = Preferences.get(pref, "");
+
+ langs.sort((a, b) => {
+ let idx_a = lastUsedLangs.indexOf(a[1]);
+ let idx_b = lastUsedLangs.indexOf(b[1]);
+
+ if (idx_a == -1 && idx_b == -1) {
+ return a[0].localeCompare(b[0]);
+ } else if (idx_a != -1 && idx_b != -1) {
+ return idx_a - idx_b;
+ } else if (idx_a == -1) {
+ return 1;
+ } else {
+ return -1;
+ }
+ });
+ removeChildren(localeList);
+
+ for (let lang of langs) {
+ addMenuItem(localeList, lang[0], lang[1], null);
+ }
+ },
+
+ extractWithLocale: function(event, isEvent) {
+ event.stopPropagation();
+ let locale = event.target.value;
+ this.extractFromEmail(isEvent, true, locale);
+ },
+
+ extractFromEmail: function(isEvent, fixedLang, fixedLocale) {
+ // TODO would be nice to handle multiple selected messages,
+ // though old conversion functionality didn't
+ let message = gFolderDisplay.selectedMessage;
+ let messenger = Components.classes["@mozilla.org/messenger;1"]
+ .createInstance(Components.interfaces.nsIMessenger);
+ let listener = Components.classes["@mozilla.org/network/sync-stream-listener;1"]
+ .createInstance(Components.interfaces.nsISyncStreamListener);
+ let uri = message.folder.getUriForMsg(message);
+ messenger.messageServiceFromURI(uri)
+ .streamMessage(uri, listener, null, null, false, "");
+ let folder = message.folder;
+ let title = message.mime2DecodedSubject;
+ let content = folder.getMsgTextFromStream(listener.inputStream,
+ message.Charset,
+ 65536,
+ 32768,
+ false,
+ true,
+ { });
+ cal.LOG("[calExtract] Original email content: \n" + title + "\r\n" + content);
+ let date = new Date(message.date / 1000);
+ let time = (new Date()).getTime();
+
+ let locale = Preferences.get("general.useragent.locale", "en-US");
+ let dayStart = Preferences.get("calendar.view.daystarthour", 6);
+ let extractor;
+
+ if (fixedLang) {
+ extractor = new Extractor(fixedLocale, dayStart);
+ } else {
+ extractor = new Extractor(locale, dayStart, false);
+ }
+
+ let item;
+ item = isEvent ? cal.createEvent() : cal.createTodo();
+ item.title = message.mime2DecodedSubject;
+ item.calendar = getSelectedCalendar();
+ item.setProperty("DESCRIPTION", content);
+ cal.setDefaultStartEndHour(item);
+ cal.alarms.setDefaultValues(item);
+ let sel = GetMessagePaneFrame().getSelection();
+ // Thunderbird Conversations might be installed
+ if (sel === null) {
+ try {
+ sel = document.getElementById("multimessage")
+ .contentDocument.querySelector(".iframe-container iframe")
+ .contentDocument.getSelection();
+ } catch (ex) {
+ // If Thunderbird Conversations is not installed that is fine,
+ // we will just have a null selection.
+ }
+ }
+ let collected = extractor.extract(title, content, date, sel);
+
+ // if we only have email date then use default start and end
+ if (collected.length == 1) {
+ cal.LOG("[calExtract] Date and time information was not found in email/selection.");
+ createEventWithDialog(null, null, null, null, item);
+ } else {
+ let guessed = extractor.guessStart(!isEvent);
+ let endGuess = extractor.guessEnd(guessed, !isEvent);
+ let allDay = (guessed.hour == null || guessed.minute == null) && isEvent;
+
+ if (isEvent) {
+ if (guessed.year != null) {
+ item.startDate.year = guessed.year;
+ }
+ if (guessed.month != null) {
+ item.startDate.month = guessed.month - 1;
+ }
+ if (guessed.day != null) {
+ item.startDate.day = guessed.day;
+ }
+ if (guessed.hour != null) {
+ item.startDate.hour = guessed.hour;
+ }
+ if (guessed.minute != null) {
+ item.startDate.minute = guessed.minute;
+ }
+
+ item.endDate = item.startDate.clone();
+ item.endDate.minute += Preferences.get("calendar.event.defaultlength", 60);
+
+ if (endGuess.year != null) {
+ item.endDate.year = endGuess.year;
+ }
+ if (endGuess.month != null) {
+ item.endDate.month = endGuess.month - 1;
+ }
+ if (endGuess.day != null) {
+ item.endDate.day = endGuess.day;
+ if (allDay) {
+ item.endDate.day++;
+ }
+ }
+ if (endGuess.hour != null) {
+ item.endDate.hour = endGuess.hour;
+ }
+ if (endGuess.minute != null) {
+ item.endDate.minute = endGuess.minute;
+ }
+ } else {
+ let dtz = cal.calendarDefaultTimezone();
+ let dueDate = new Date();
+ // set default
+ dueDate.setHours(0);
+ dueDate.setMinutes(0);
+ dueDate.setSeconds(0);
+
+ if (endGuess.year != null) {
+ dueDate.setYear(endGuess.year);
+ }
+ if (endGuess.month != null) {
+ dueDate.setMonth(endGuess.month - 1);
+ }
+ if (endGuess.day != null) {
+ dueDate.setDate(endGuess.day);
+ }
+ if (endGuess.hour != null) {
+ dueDate.setHours(endGuess.hour);
+ }
+ if (endGuess.minute != null) {
+ dueDate.setMinutes(endGuess.minute);
+ }
+
+ setItemProperty(item, "entryDate", cal.jsDateToDateTime(date, dtz));
+ if (endGuess.year != null) {
+ setItemProperty(item, "dueDate", cal.jsDateToDateTime(dueDate, dtz));
+ }
+ }
+
+ // if time not guessed set allday for events
+ if (allDay) {
+ createEventWithDialog(null, null, null, null, item, true);
+ } else {
+ createEventWithDialog(null, null, null, null, item);
+ }
+ }
+
+ let timeSpent = (new Date()).getTime() - time;
+ cal.LOG("[calExtract] Total time spent for conversion (including loading of dictionaries): " + timeSpent + "ms");
+ },
+
+ addListeners: function() {
+ if (window.top.document.location == "chrome://messenger/content/messenger.xul") {
+ // covers initial load and folder change
+ let folderTree = document.getElementById("folderTree");
+ folderTree.addEventListener("select", this.setState, false);
+
+ // covers selection change in a folder
+ let msgTree = window.top.GetThreadTree();
+ msgTree.addEventListener("select", this.setState, false);
+
+ window.addEventListener("unload", () => {
+ folderTree.removeEventListener("select", this.setState, false);
+ msgTree.removeEventListener("select", this.setState, false);
+ }, false);
+ }
+ },
+
+ setState: function() {
+ let eventButton = document.getElementById("extractEventButton");
+ let taskButton = document.getElementById("extractTaskButton");
+ let hdrEventButton = document.getElementById("hdrExtractEventButton");
+ let hdrTaskButton = document.getElementById("hdrExtractTaskButton");
+ let contextMenu = document.getElementById("mailContext-calendar-convert-menu");
+ let contextMenuEvent = document.getElementById("mailContext-calendar-convert-event-menuitem");
+ let contextMenuTask = document.getElementById("mailContext-calendar-convert-task-menuitem");
+ let eventDisabled = (gFolderDisplay.selectedCount == 0);
+ let taskDisabled = (gFolderDisplay.selectedCount == 0);
+ let contextEventDisabled = false;
+ let contextTaskDisabled = false;
+ let newEvent = document.getElementById("calendar_new_event_command");
+ let newTask = document.getElementById("calendar_new_todo_command");
+
+ if (newEvent.getAttribute("disabled") == "true") {
+ eventDisabled = true;
+ contextEventDisabled = true;
+ }
+
+ if (newTask.getAttribute("disabled") == "true") {
+ taskDisabled = true;
+ contextTaskDisabled = true;
+ }
+
+ if (eventButton) {
+ eventButton.disabled = eventDisabled;
+ }
+ if (taskButton) {
+ taskButton.disabled = taskDisabled;
+ }
+ if (hdrEventButton) {
+ hdrEventButton.disabled = eventDisabled;
+ }
+ if (hdrTaskButton) {
+ hdrTaskButton.disabled = taskDisabled;
+ }
+
+ contextMenuEvent.disabled = contextEventDisabled;
+ contextMenuTask.disabled = contextTaskDisabled;
+
+ contextMenu.disabled = contextEventDisabled && contextTaskDisabled;
+ }
+};
+
+window.addEventListener("load", calendarExtract.addListeners.bind(calendarExtract), false);
diff --git a/calendar/base/content/calendar-invitations-manager.js b/calendar/base/content/calendar-invitations-manager.js
new file mode 100644
index 000000000..7c8a28ee8
--- /dev/null
+++ b/calendar/base/content/calendar-invitations-manager.js
@@ -0,0 +1,422 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calItipUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/* exported getInvitationsManager */
+
+/**
+ * This object contains functions to take care of manipulating requests.
+ */
+var gInvitationsRequestManager = {
+ mRequestStatusList: {},
+
+ /**
+ * Add a request to the request manager.
+ *
+ * @param calendar The calendar to add for.
+ * @param op The operation to add
+ */
+ addRequestStatus: function(calendar, operation) {
+ if (operation) {
+ this.mRequestStatusList[calendar.id] = operation;
+ }
+ },
+
+ /**
+ * Cancel all pending requests
+ */
+ cancelPendingRequests: function() {
+ for (let id in this.mRequestStatusList) {
+ let request = this.mRequestStatusList[id];
+ if (request && request.isPending) {
+ request.cancel(null);
+ }
+ }
+ this.mRequestStatusList = {};
+ }
+};
+
+var gInvitationsManager = null;
+
+/**
+ * Return a cached instance of the invitations manager
+ *
+ * @return The invitations manager instance.
+ */
+function getInvitationsManager() {
+ if (!gInvitationsManager) {
+ gInvitationsManager = new InvitationsManager();
+ }
+ return gInvitationsManager;
+}
+
+/**
+ * The invitations manager class constructor
+ *
+ * XXX do we really need this to be an instance?
+ *
+ * @constructor
+ */
+function InvitationsManager() {
+ this.mItemList = [];
+ this.mStartDate = null;
+ this.mJobsPending = 0;
+ this.mTimer = null;
+
+ window.addEventListener("unload", () => {
+ // Unload handlers get removed automatically
+ this.cancelInvitationsUpdate();
+ }, false);
+}
+
+InvitationsManager.prototype = {
+ mItemList: null,
+ mStartDate: null,
+ mJobsPending: 0,
+ mTimer: null,
+
+ /**
+ * Schedule an update for the invitations manager asynchronously.
+ *
+ * @param firstDelay The timeout before the operation should start.
+ * @param operationListener The calIGenericOperationListener to notify.
+ */
+ scheduleInvitationsUpdate: function(firstDelay, operationListener) {
+ this.cancelInvitationsUpdate();
+
+ this.mTimer = setTimeout(() => {
+ if (Preferences.get("calendar.invitations.autorefresh.enabled", true)) {
+ this.mTimer = setInterval(() => {
+ this.getInvitations(operationListener);
+ }, Preferences.get("calendar.invitations.autorefresh.timeout", 3) * 60000);
+ }
+ this.getInvitations(operationListener);
+ }, firstDelay);
+ },
+
+ /**
+ * Cancel pending any pending invitations update.
+ */
+ cancelInvitationsUpdate: function() {
+ clearTimeout(this.mTimer);
+ },
+
+ /**
+ * Retrieve invitations from all calendars. Notify all passed
+ * operation listeners.
+ *
+ * @param operationListener1 The first operation listener to notify.
+ * @param operationListener2 (optinal) The second operation listener to
+ * notify.
+ */
+ getInvitations: function(operationListener1, operationListener2) {
+ let listeners = [];
+ if (operationListener1) {
+ listeners.push(operationListener1);
+ }
+ if (operationListener2) {
+ listeners.push(operationListener2);
+ }
+
+ gInvitationsRequestManager.cancelPendingRequests();
+ this.updateStartDate();
+ this.deleteAllItems();
+
+ let cals = getCalendarManager().getCalendars({});
+
+ let opListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ mCount: cals.length,
+ mRequestManager: gInvitationsRequestManager,
+ mInvitationsManager: this,
+ mHandledItems: {},
+
+ // calIOperationListener
+ onOperationComplete: function(aCalendar,
+ aStatus,
+ aOperationType,
+ aId,
+ aDetail) {
+ if (--this.mCount == 0) {
+ this.mInvitationsManager.mItemList.sort((a, b) => {
+ return a.startDate.compare(b.startDate);
+ });
+ for (let listener of listeners) {
+ try {
+ if (this.mInvitationsManager.mItemList.length) {
+ // Only call if there are actually items
+ listener.onGetResult(null,
+ Components.results.NS_OK,
+ Components.interfaces.calIItemBase,
+ null,
+ this.mInvitationsManager.mItemList.length,
+ this.mInvitationsManager.mItemList);
+ }
+ listener.onOperationComplete(null,
+ Components.results.NS_OK,
+ Components.interfaces.calIOperationListener.GET,
+ null,
+ null);
+ } catch (exc) {
+ ERROR(exc);
+ }
+ }
+ }
+ },
+
+ onGetResult: function(aCalendar,
+ aStatus,
+ aItemType,
+ aDetail,
+ aCount,
+ aItems) {
+ if (Components.isSuccessCode(aStatus)) {
+ for (let item of aItems) {
+ // we need to retrieve by occurrence to properly filter exceptions,
+ // should be fixed with bug 416975
+ item = item.parentItem;
+ let hid = item.hashId;
+ if (!this.mHandledItems[hid]) {
+ this.mHandledItems[hid] = true;
+ this.mInvitationsManager.addItem(item);
+ }
+ }
+ }
+ }
+ };
+
+ for (let calendar of cals) {
+ if (!isCalendarWritable(calendar) || calendar.getProperty("disabled")) {
+ opListener.onOperationComplete();
+ continue;
+ }
+
+ // temporary hack unless calCachedCalendar supports REQUEST_NEEDS_ACTION filter:
+ calendar = calendar.getProperty("cache.uncachedCalendar");
+ if (!calendar) {
+ opListener.onOperationComplete();
+ continue;
+ }
+
+ try {
+ calendar = calendar.QueryInterface(Components.interfaces.calICalendar);
+ let endDate = this.mStartDate.clone();
+ endDate.year += 1;
+ let operation = calendar.getItems(Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION |
+ Components.interfaces.calICalendar.ITEM_FILTER_TYPE_ALL |
+ // we need to retrieve by occurrence to properly filter exceptions,
+ // should be fixed with bug 416975
+ Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES,
+ 0, this.mStartDate,
+ endDate /* we currently cannot pass null here, because of bug 416975 */,
+ opListener);
+ gInvitationsRequestManager.addRequestStatus(calendar, operation);
+ } catch (exc) {
+ opListener.onOperationComplete();
+ ERROR(exc);
+ }
+ }
+ },
+
+ /**
+ * Open the invitations dialog, non-modal.
+ *
+ * XXX Passing these listeners in instead of keeping them in the window
+ * sounds fishy to me. Maybe there is a more encapsulated solution.
+ *
+ * @param onLoadOpListener The operation listener to notify when
+ * getting invitations. Should be passed
+ * to this.getInvitations().
+ * @param finishedCallBack A callback function to call when the
+ * dialog has completed.
+ */
+ openInvitationsDialog: function(onLoadOpListener, finishedCallBack) {
+ let args = {};
+ args.onLoadOperationListener = onLoadOpListener;
+ args.queue = [];
+ args.finishedCallBack = finishedCallBack;
+ args.requestManager = gInvitationsRequestManager;
+ args.invitationsManager = this;
+ // the dialog will reset this to auto when it is done loading
+ window.setCursor("wait");
+ // open the dialog
+ window.openDialog(
+ "chrome://calendar/content/calendar-invitations-dialog.xul",
+ "_blank",
+ "chrome,titlebar,resizable",
+ args);
+ },
+
+ /**
+ * Process the passed job queue. A job is an object that consists of an
+ * action, a newItem and and oldItem. This processor only takes "modify"
+ * operations into account.
+ *
+ * @param queue The array of objects to process.
+ * @param jobQueueFinishedCallBack A callback function called when
+ * job has finished.
+ */
+ processJobQueue: function(queue, jobQueueFinishedCallBack) {
+ // TODO: undo/redo
+ function operationListener(mgr, queueCallback, oldItem_) {
+ this.mInvitationsManager = mgr;
+ this.mJobQueueFinishedCallBack = queueCallback;
+ this.mOldItem = oldItem_;
+ }
+ operationListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar,
+ aStatus,
+ aOperationType,
+ aId,
+ aDetail) {
+ if (Components.isSuccessCode(aStatus) &&
+ aOperationType == Components.interfaces.calIOperationListener.MODIFY) {
+ cal.itip.checkAndSend(aOperationType, aDetail, this.mOldItem);
+ this.mInvitationsManager.deleteItem(aDetail);
+ this.mInvitationsManager.addItem(aDetail);
+ }
+ this.mInvitationsManager.mJobsPending--;
+ if (this.mInvitationsManager.mJobsPending == 0 &&
+ this.mJobQueueFinishedCallBack) {
+ this.mJobQueueFinishedCallBack();
+ }
+ },
+
+ onGetResult: function(aCalendar,
+ aStatus,
+ aItemType,
+ aDetail,
+ aCount,
+ aItems) {
+
+ }
+ };
+
+ this.mJobsPending = 0;
+ for (let i = 0; i < queue.length; i++) {
+ let job = queue[i];
+ let oldItem = job.oldItem;
+ let newItem = job.newItem;
+ switch (job.action) {
+ case "modify":
+ this.mJobsPending++;
+ newItem.calendar.modifyItem(newItem,
+ oldItem,
+ new operationListener(this, jobQueueFinishedCallBack, oldItem));
+ break;
+ default:
+ break;
+ }
+ }
+ if (this.mJobsPending == 0 && jobQueueFinishedCallBack) {
+ jobQueueFinishedCallBack();
+ }
+ },
+
+ /**
+ * Checks if the internal item list contains the given item
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to look for.
+ * @return A boolean value indicating if the item was found.
+ */
+ hasItem: function(item) {
+ let hid = item.hashId;
+ return this.mItemList.some(item_ => hid == item_.hashId);
+ },
+
+ /**
+ * Adds an item to the internal item list.
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to add.
+ */
+ addItem: function(item) {
+ let recInfo = item.recurrenceInfo;
+ if (recInfo && !cal.isOpenInvitation(item)) {
+ // scan exceptions:
+ let ids = recInfo.getExceptionIds({});
+ for (let id of ids) {
+ let ex = recInfo.getExceptionFor(id);
+ if (ex && this.validateItem(ex) && !this.hasItem(ex)) {
+ this.mItemList.push(ex);
+ }
+ }
+ } else if (this.validateItem(item) && !this.hasItem(item)) {
+ this.mItemList.push(item);
+ }
+ },
+
+ /**
+ * Removes an item from the internal item list
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to remove.
+ */
+ deleteItem: function(item) {
+ let id = item.id;
+ this.mItemList.filter(item_ => id != item_.id);
+ },
+
+ /**
+ * Remove all items from the internal item list
+ * XXXdbo Please document these correctly.
+ */
+ deleteAllItems: function() {
+ this.mItemList = [];
+ },
+
+ /**
+ * Helper function to create a start date to search from. This date is the
+ * current time with hour/minute/second set to zero.
+ *
+ * @return Potential start date.
+ */
+ getStartDate: function() {
+ let date = now();
+ date.second = 0;
+ date.minute = 0;
+ date.hour = 0;
+ return date;
+ },
+
+ /**
+ * Updates the start date for the invitations manager to the date returned
+ * from this.getStartDate(), unless the previously existing start date is
+ * the same or after what getStartDate() returned.
+ */
+ updateStartDate: function() {
+ if (this.mStartDate) {
+ let startDate = this.getStartDate();
+ if (startDate.compare(this.mStartDate) > 0) {
+ this.mStartDate = startDate;
+ }
+ } else {
+ this.mStartDate = this.getStartDate();
+ }
+ },
+
+ /**
+ * Checks if the item is valid for the invitation manager. Checks if the
+ * item is in the range of the invitation manager and if the item is a valid
+ * invitation.
+ *
+ * @param item The item to check
+ * @return A boolean indicating if the item is a valid invitation.
+ */
+ validateItem: function(item) {
+ if (item.calendar instanceof Components.interfaces.calISchedulingSupport &&
+ !item.calendar.isInvitation(item)) {
+ return false; // exclude if organizer has invited himself
+ }
+ let start = item[calGetStartDateProp(item)] || item[calGetEndDateProp(item)];
+ return (cal.isOpenInvitation(item) &&
+ start.compare(this.mStartDate) >= 0);
+ }
+};
diff --git a/calendar/base/content/calendar-item-bindings.xml b/calendar/base/content/calendar-item-bindings.xml
new file mode 100644
index 000000000..58b4a1e40
--- /dev/null
+++ b/calendar/base/content/calendar-item-bindings.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<bindings id="calendar-item-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- Header with a line beside it, used i.e in the event dialog -->
+ <binding id="calendar-caption" extends="xul:hbox">
+ <content align="center">
+ <xul:label xbl:inherits="value=label,control" class="header"/>
+ <xul:separator class="groove" flex="1"/>
+ </content>
+ </binding>
+
+ <binding id="item-date-row" extends="xul:row">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/calendar-event-dialog.css"/>
+ </resources>
+ <content xbl:inherits="mode">
+ <xul:label anonid="item-datetime-label"
+ class="headline"
+ xbl:inherits="align"/>
+ <xul:label anonid="item-datetime-value"/>
+ </content>
+ <implementation>
+ <field name="mItem">null</field>
+ <property name="mode"
+ readonly="true">
+ <getter><![CDATA[
+ if (this.hasAttribute("mode")) {
+ return this.getAttribute("mode");
+ } else {
+ return "start";
+ }
+ ]]></getter>
+ </property>
+ <property name="Item">
+ <getter><![CDATA[
+ return mItem;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mItem = val;
+ let headerLabel = document.getAnonymousElementByAttribute(this, "anonid", "item-datetime-label");
+ let itemDateTimeLabel = document.getAnonymousElementByAttribute(this, "anonid", "item-datetime-value");
+ let date;
+ if (this.mode == "start") {
+ date = this.mItem[calGetStartDateProp(this.mItem)];
+ if (date) {
+ let label;
+ if (isToDo(this.mItem)) {
+ label = this.getAttribute("taskStartLabel");
+ } else {
+ label = this.getAttribute("eventStartLabel");
+ }
+ headerLabel.value = label;
+ }
+ } else {
+ date = this.mItem[calGetEndDateProp(this.mItem)];
+ if (date) {
+ let label;
+ if (isToDo(this.mItem)) {
+ label = this.getAttribute("taskDueLabel");
+ } else {
+ label = this.getAttribute("eventEndLabel");
+ }
+ headerLabel.value = label;
+ }
+ }
+ let hideLabels = (date == null);
+ if (hideLabels) {
+ this.setAttribute("hidden", "true");
+ } else {
+ const kDefaultTimezone = cal.calendarDefaultTimezone();
+ let localTime = date.getInTimezone(kDefaultTimezone);
+ let formatter = getDateFormatter();
+ itemDateTimeLabel.value = formatter.formatDateTime(localTime);
+ if (!date.timezone.isFloating && date.timezone.tzid != kDefaultTimezone.tzid) {
+ // we additionally display the original datetime with timezone
+ let orgTime = cal.calGetString("calendar",
+ "datetimeWithTimezone",
+ [formatter.formatDateTime(date),
+ date.timezone.tzid]);
+ itemDateTimeLabel.value += " (" + orgTime + ")";
+ }
+ this.removeAttribute("hidden");
+ }
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-item-editing.js b/calendar/base/content/calendar-item-editing.js
new file mode 100644
index 000000000..c505e8552
--- /dev/null
+++ b/calendar/base/content/calendar-item-editing.js
@@ -0,0 +1,742 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/* exported modifyEventWithDialog, undo, redo, setContextPartstat */
+
+/**
+ * Takes a job and makes sure the dispose function on it is called. If there is
+ * no dispose function or the job is null, ignore it.
+ *
+ * @param job The job to dispose.
+ */
+function disposeJob(job) {
+ if (job && job.dispose) {
+ job.dispose();
+ }
+}
+
+/**
+ * Sets the default values for new items, taking values from either the passed
+ * parameters or the preferences
+ *
+ * @param aItem The item to set up
+ * @param aCalendar (optional) The calendar to apply.
+ * @param aStartDate (optional) The start date to set.
+ * @param aEndDate (optional) The end date/due date to set.
+ * @param aInitialDate (optional) The reference date for the date pickers
+ * @param aForceAllday (optional) Force the event/task to be an allday item.
+ */
+function setDefaultItemValues(aItem, aCalendar=null, aStartDate=null, aEndDate=null, aInitialDate=null, aForceAllday=false) {
+ function endOfDay(aDate) {
+ let eod = aDate ? aDate.clone() : cal.now();
+ eod.hour = Preferences.get("calendar.view.dayendhour", 19);
+ eod.minute = 0;
+ eod.second = 0;
+ return eod;
+ }
+ function startOfDay(aDate) {
+ let sod = aDate ? aDate.clone() : cal.now();
+ sod.hour = Preferences.get("calendar.view.daystarthour", 8);
+ sod.minute = 0;
+ sod.second = 0;
+ return sod;
+ }
+
+ let initialDate = aInitialDate ? aInitialDate.clone() : cal.now();
+ initialDate.isDate = true;
+
+ if (cal.isEvent(aItem)) {
+ if (aStartDate) {
+ aItem.startDate = aStartDate.clone();
+ if (aStartDate.isDate && !aForceAllday) {
+ // This is a special case where the date is specified, but the
+ // time is not. To take care, we setup up the time to our
+ // default event start time.
+ aItem.startDate = cal.getDefaultStartDate(aItem.startDate);
+ } else if (aForceAllday) {
+ // If the event should be forced to be allday, then don't set up
+ // any default hours and directly make it allday.
+ aItem.startDate.isDate = true;
+ aItem.startDate.timezone = cal.floating();
+ }
+ } else {
+ // If no start date was passed, then default to the next full hour
+ // of today, but with the date of the selected day
+ aItem.startDate = cal.getDefaultStartDate(initialDate);
+ }
+
+ if (aEndDate) {
+ aItem.endDate = aEndDate.clone();
+ if (aForceAllday) {
+ // XXX it is currently not specified, how callers that force all
+ // day should pass the end date. Right now, they should make
+ // sure that the end date is 00:00:00 of the day after.
+ aItem.endDate.isDate = true;
+ aItem.endDate.timezone = cal.floating();
+ }
+ } else {
+ aItem.endDate = aItem.startDate.clone();
+ if (aForceAllday) {
+ // All day events need to go to the beginning of the next day.
+ aItem.endDate.day++;
+ } else {
+ // If the event is not all day, then add the default event
+ // length.
+ aItem.endDate.minute += Preferences.get("calendar.event.defaultlength", 60);
+ }
+ }
+
+ // Free/busy status is only valid for events, must not be set for tasks.
+ aItem.setProperty("TRANSP", cal.getEventDefaultTransparency(aForceAllday));
+ } else if (cal.isToDo(aItem)) {
+ let now = cal.now();
+ let initDate = initialDate ? initialDate.clone() : now;
+ initDate.isDate = false;
+ initDate.hour = now.hour;
+ initDate.minute = now.minute;
+ initDate.second = now.second;
+
+ if (aStartDate) {
+ aItem.entryDate = aStartDate.clone();
+ } else {
+ let defaultStart = Preferences.get("calendar.task.defaultstart", "none");
+ if (Preferences.get("calendar.alarms.onfortodos", 0) == 1 && defaultStart == "none") {
+ // start date is required if we want to set an alarm
+ defaultStart = "offsetcurrent";
+ }
+
+ let units = Preferences.get("calendar.task.defaultstartoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let startOffset = cal.createDuration();
+ startOffset[units] = Preferences.get("calendar.task.defaultstartoffset", 0);
+ let start;
+
+ switch (defaultStart) {
+ case "none":
+ break;
+ case "startofday":
+ start = startOfDay(initDate);
+ break;
+ case "tomorrow":
+ start = startOfDay(initDate);
+ start.day++;
+ break;
+ case "nextweek":
+ start = startOfDay(initDate);
+ start.day += 7;
+ break;
+ case "offsetcurrent":
+ start = initDate.clone();
+ start.addDuration(startOffset);
+ break;
+ case "offsetnexthour":
+ start = initDate.clone();
+ start.second = 0;
+ start.minute = 0;
+ start.hour++;
+ start.addDuration(startOffset);
+ break;
+ }
+
+ if (start) {
+ aItem.entryDate = start;
+ }
+ }
+
+ if (aEndDate) {
+ aItem.dueDate = aEndDate.clone();
+ } else {
+ let defaultDue = Preferences.get("calendar.task.defaultdue", "none");
+
+ let units = Preferences.get("calendar.task.defaultdueoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let dueOffset = cal.createDuration();
+ dueOffset[units] = Preferences.get("calendar.task.defaultdueoffset", 0);
+
+ let start = aItem.entryDate ? aItem.entryDate.clone() : initDate.clone();
+ let due;
+
+ switch (defaultDue) {
+ case "none":
+ break;
+ case "endofday":
+ due = endOfDay(start);
+ // go to tomorrow if we're past the end of today
+ if (start.compare(due) > 0) {
+ due.day++;
+ }
+ break;
+ case "tomorrow":
+ due = endOfDay(start);
+ due.day++;
+ break;
+ case "nextweek":
+ due = endOfDay(start);
+ due.day += 7;
+ break;
+ case "offsetcurrent":
+ due = start.clone();
+ due.addDuration(dueOffset);
+ break;
+ case "offsetnexthour":
+ due = start.clone();
+ due.second = 0;
+ due.minute = 0;
+ due.hour++;
+ due.addDuration(dueOffset);
+ break;
+ }
+
+ if (aItem.entryDate && due && aItem.entryDate.compare(due) > 0) {
+ // due can't be earlier than start date.
+ due = aItem.entryDate;
+ }
+
+ if (due) {
+ aItem.dueDate = due;
+ }
+ }
+ }
+
+ // Calendar
+ aItem.calendar = aCalendar || getSelectedCalendar();
+
+ // Alarms
+ cal.alarms.setDefaultValues(aItem);
+}
+
+/**
+ * Creates an event with the calendar event dialog.
+ *
+ * @param calendar (optional) The calendar to create the event in
+ * @param startDate (optional) The event's start date.
+ * @param endDate (optional) The event's end date.
+ * @param summary (optional) The event's title.
+ * @param event (optional) A template event to show in the dialog
+ * @param aForceAllDay (optional) Make sure the event shown in the dialog is an
+ * allday event.
+ */
+function createEventWithDialog(calendar, startDate, endDate, summary, event, aForceAllday) {
+ let onNewEvent = function(item, opcalendar, originalItem, listener) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener);
+ }
+ };
+
+ if (event) {
+ if (!event.isMutable) {
+ event = event.clone();
+ }
+ // If the event should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ event.id = null;
+
+ if (aForceAllday) {
+ event.startDate.isDate = true;
+ event.endDate.isDate = true;
+ if (event.startDate.compare(event.endDate) == 0) {
+ // For a one day all day event, the end date must be 00:00:00 of
+ // the next day.
+ event.endDate.day++;
+ }
+ }
+
+ if (!event.calendar) {
+ event.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ event = cal.createEvent();
+
+ let refDate = currentView().initialized && currentView().selectedDay.clone();
+ setDefaultItemValues(event, calendar, startDate, endDate, refDate, aForceAllday);
+ if (summary) {
+ event.title = summary;
+ }
+ }
+ openEventDialog(event, event.calendar, "new", onNewEvent);
+}
+
+/**
+ * Creates a task with the calendar event dialog.
+ *
+ * @param calendar (optional) The calendar to create the task in
+ * @param dueDate (optional) The task's due date.
+ * @param summary (optional) The task's title.
+ * @param todo (optional) A template task to show in the dialog.
+ * @param initialDate (optional) The initial date for new task datepickers
+ */
+function createTodoWithDialog(calendar, dueDate, summary, todo, initialDate) {
+ let onNewItem = function(item, opcalendar, originalItem, listener) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener);
+ }
+ };
+
+ if (todo) {
+ // If the todo should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ if (todo.id) {
+ todo = todo.clone();
+ todo.id = null;
+ }
+
+ if (!todo.calendar) {
+ todo.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ todo = cal.createTodo();
+ setDefaultItemValues(todo, calendar, null, dueDate, initialDate);
+
+ if (summary) {
+ todo.title = summary;
+ }
+ }
+
+ openEventDialog(todo, calendar, "new", onNewItem, null, initialDate);
+}
+
+/**
+ * Modifies the passed event in the event dialog.
+ *
+ * @param aItem The item to modify.
+ * @param job (optional) The job object that controls this
+ * modification.
+ * @param aPromptOccurrence If the user should be prompted to select if the
+ * parent item or occurrence should be modified.
+ * @param initialDate (optional) The initial date for new task datepickers
+ * @param aCounterProposal (optional) An object representing the counterproposal
+ * {
+ * {JsObject} result: {
+ * type: {String} "OK"|"OUTDATED"|"NOTLATESTUPDATE"|"ERROR"|"NODIFF"
+ * descr: {String} a technical description of the problem if type is ERROR or NODIFF,
+ * otherwise an empty string
+ * },
+ * (empty if result.type = "ERROR"|"NODIFF"){Array} differences: [{
+ * property: {String} a property that is subject to the proposal
+ * proposed: {String} the proposed value
+ * original: {String} the original value
+ * }]
+ * }
+ */
+function modifyEventWithDialog(aItem, job=null, aPromptOccurrence, initialDate=null, aCounterProposal) {
+ let dlg = cal.findItemWindow(aItem);
+ if (dlg) {
+ dlg.focus();
+ disposeJob(job);
+ return;
+ }
+
+ let onModifyItem = function(item, calendar, originalItem, listener) {
+ doTransaction("modify", item, calendar, originalItem, listener);
+ };
+
+ let item = aItem;
+ let response;
+ if (aPromptOccurrence !== false) {
+ [item, , response] = promptOccurrenceModification(aItem, true, "edit");
+ }
+
+ if (item && (response || response === undefined)) {
+ openEventDialog(item, item.calendar, "modify", onModifyItem, job, initialDate,
+ aCounterProposal);
+ } else {
+ disposeJob(job);
+ }
+}
+
+/**
+ * Opens the event dialog with the given item (task OR event)
+ *
+ * @param calendarItem The item to open the dialog with
+ * @param calendar The calendar to open the dialog with.
+ * @param mode The operation the dialog should do ("new", "modify")
+ * @param callback The callback to call when the dialog has completed.
+ * @param job (optional) The job object for the modification.
+ * @param initialDate (optional) The initial date for new task datepickers
+ * @param counterProposal (optional) An object representing the counterproposal - see
+ * description for modifyEventWithDialog()
+ */
+function openEventDialog(calendarItem, calendar, mode, callback, job=null, initialDate=null, counterProposal) {
+ let dlg = cal.findItemWindow(calendarItem);
+ if (dlg) {
+ dlg.focus();
+ disposeJob(job);
+ return;
+ }
+
+ // Set up some defaults
+ mode = mode || "new";
+ calendar = calendar || getSelectedCalendar();
+ let calendars = getCalendarManager().getCalendars({});
+ calendars = calendars.filter(isCalendarWritable);
+
+ let isItemSupported;
+ if (isToDo(calendarItem)) {
+ isItemSupported = function(aCalendar) {
+ return (aCalendar.getProperty("capabilities.tasks.supported") !== false);
+ };
+ } else if (isEvent(calendarItem)) {
+ isItemSupported = function(aCalendar) {
+ return (aCalendar.getProperty("capabilities.events.supported") !== false);
+ };
+ }
+
+ // Filter out calendars that don't support the given calendar item
+ calendars = calendars.filter(isItemSupported);
+
+ // Filter out calendar/items that we cannot write to/modify
+ if (mode == "new") {
+ calendars = calendars.filter(userCanAddItemsToCalendar);
+ } else { /* modify */
+ calendars = calendars.filter((aCalendar) => {
+ /* If the calendar is the item calendar, we check that the item
+ * can be modified. If the calendar is NOT the item calendar, we
+ * check that the user can remove items from that calendar and
+ * add items to the current one.
+ */
+ let isSameCalendar = calendarItem.calendar == aCalendar;
+ let canModify = userCanModifyItem(calendarItem);
+ let canMoveItems = userCanDeleteItemsFromCalendar(calendarItem.calendar) &&
+ userCanAddItemsToCalendar(aCalendar);
+
+ return isSameCalendar ? canModify : canMoveItems;
+ });
+ }
+
+ if (mode == "new" &&
+ (!isCalendarWritable(calendar) ||
+ !userCanAddItemsToCalendar(calendar) ||
+ !isItemSupported(calendar))) {
+ if (calendars.length < 1) {
+ // There are no writable calendars or no calendar supports the given
+ // item. Don't show the dialog.
+ disposeJob(job);
+ return;
+ } else {
+ // Pick the first calendar that supports the item and is writable
+ calendar = calendars[0];
+ if (calendarItem) {
+ // XXX The dialog currently uses the items calendar as a first
+ // choice. Since we are shortly before a release to keep
+ // regression risk low, explicitly set the item's calendar here.
+ calendarItem.calendar = calendars[0];
+ }
+ }
+ }
+
+ // Setup the window arguments
+ let args = {};
+ args.calendarEvent = calendarItem;
+ args.calendar = calendar;
+ args.mode = mode;
+ args.onOk = callback;
+ args.job = job;
+ args.initialStartDateValue = initialDate || getDefaultStartDate();
+ args.counterProposal = counterProposal;
+ args.inTab = Preferences.get("calendar.item.editInTab", false);
+ args.useNewItemUI = Preferences.get("calendar.item.useNewItemUI", false);
+
+ // this will be called if file->new has been selected from within the dialog
+ args.onNewEvent = function(opcalendar) {
+ createEventWithDialog(opcalendar, null, null);
+ };
+ args.onNewTodo = function(opcalendar) {
+ createTodoWithDialog(opcalendar);
+ };
+
+ // the dialog will reset this to auto when it is done loading.
+ window.setCursor("wait");
+
+ // ask the provide if this item is an invitation. if this is the case
+ // we'll open the summary dialog since the user is not allowed to change
+ // the details of the item.
+ let wrappedCalendar = cal.wrapInstance(calendar, Components.interfaces.calISchedulingSupport);
+ let isInvitation = wrappedCalendar && wrappedCalendar.isInvitation(calendarItem);
+
+ // open the dialog modeless
+ let url;
+ let isEditable = mode == "modify" && !isInvitation && userCanModifyItem(calendarItem);
+ if (isCalendarWritable(calendar) && (mode == "new" || isEditable)) {
+ if (args.inTab) {
+ url = args.useNewItemUI ? "chrome://lightning/content/html-item-editing/lightning-item-iframe.html"
+ : "chrome://lightning/content/lightning-item-iframe.xul";
+ } else {
+ url = "chrome://calendar/content/calendar-event-dialog.xul";
+ }
+ } else {
+ url = "chrome://calendar/content/calendar-summary-dialog.xul";
+ args.inTab = false;
+ }
+
+ if (args.inTab) {
+ // open in a tab, currently the read-only summary dialog is
+ // never opened in a tab
+ args.url = url;
+ let tabmail = document.getElementById("tabmail");
+ let tabtype = cal.isEvent(args.calendarEvent) ? "calendarEvent" : "calendarTask";
+ tabmail.openTab(tabtype, args);
+ } else {
+ // open in a window
+
+ // reminder: event dialog should not be modal (cf bug 122671)
+ let features;
+ // keyword "dependent" should not be used (cf bug 752206)
+ if (Services.appinfo.OS == "WINNT") {
+ features = "chrome,titlebar,resizable";
+ } else {
+ // All other targets, mostly Linux flavors using gnome.
+ features = "chrome,titlebar,resizable,minimizable=no,dialog=no";
+ }
+ openDialog(url, "_blank", features, args);
+ }
+}
+
+/**
+ * Prompts the user how the passed item should be modified. If the item is an
+ * exception or already a parent item, the item is returned without prompting.
+ * If "all occurrences" is specified, the parent item is returned. If "this
+ * occurrence only" is specified, then aItem is returned. If "this and following
+ * occurrences" is selected, aItem's parentItem is modified so that the
+ * recurrence rules end (UNTIL) just before the given occurrence. If
+ * aNeedsFuture is specified, a new item is made from the part that was stripped
+ * off the passed item.
+ *
+ * EXDATEs and RDATEs that do not fit into the items recurrence are removed. If
+ * the modified item or the future item only consist of a single occurrence,
+ * they are changed to be single items.
+ *
+ * @param aItem The item to check.
+ * @param aNeedsFuture If true, the future item is parsed.
+ * This parameter can for example be
+ * false if a deletion is being made.
+ * @param aAction Either "edit" or "delete". Sets up
+ * the labels in the occurrence prompt
+ * @return [modifiedItem, futureItem, promptResponse]
+ * If "this and all following" was chosen,
+ * an array containing the item *until*
+ * the given occurrence (modifiedItem),
+ * and the item *after* the given
+ * occurrence (futureItem).
+ *
+ * If any other option was chosen,
+ * futureItem is null and the
+ * modifiedItem is either the parent item
+ * or the passed occurrence, or null if
+ * the dialog was canceled.
+ *
+ * The promptResponse parameter gives the
+ * response of the dialog as a constant.
+ */
+function promptOccurrenceModification(aItem, aNeedsFuture, aAction) {
+ const CANCEL = 0;
+ const MODIFY_OCCURRENCE = 1;
+ const MODIFY_FOLLOWING = 2;
+ const MODIFY_PARENT = 3;
+
+ let futureItem = false;
+ let pastItem;
+ let type = CANCEL;
+
+ // Check if this actually is an instance of a recurring event
+ if (aItem == aItem.parentItem) {
+ type = MODIFY_PARENT;
+ } else if (aItem.parentItem.recurrenceInfo.getExceptionFor(aItem.recurrenceId)) {
+ // If the user wants to edit an occurrence which is already an exception
+ // always edit this single item.
+ // XXX Why? I think its ok to ask also for exceptions.
+ type = MODIFY_OCCURRENCE;
+ } else {
+ // Prompt the user. Setting modal blocks the dialog until it is closed. We
+ // use rv to pass our return value.
+ let rv = { value: CANCEL, item: aItem, action: aAction };
+ window.openDialog("chrome://calendar/content/calendar-occurrence-prompt.xul",
+ "PromptOccurrenceModification",
+ "centerscreen,chrome,modal,titlebar",
+ rv);
+ type = rv.value;
+ }
+
+ switch (type) {
+ case MODIFY_PARENT:
+ pastItem = aItem.parentItem;
+ break;
+ case MODIFY_FOLLOWING:
+ // TODO tbd in a different bug
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ case MODIFY_OCCURRENCE:
+ pastItem = aItem;
+ break;
+ case CANCEL:
+ // Since we have not set past or futureItem, the return below will
+ // take care.
+ break;
+ }
+
+ return [pastItem, futureItem, type];
+}
+
+// Undo/Redo code
+
+/**
+ * Helper to return the transaction manager service.
+ *
+ * @return The calITransactionManager service.
+ */
+function getTransactionMgr() {
+ return Components.classes["@mozilla.org/calendar/transactionmanager;1"]
+ .getService(Components.interfaces.calITransactionManager);
+}
+
+
+/**
+ * Create and commit a transaction with the given arguments to the transaction
+ * manager. Also updates the undo/redo menu.
+ *
+ * @see calITransactionManager
+ * @param aAction The action to do.
+ * @param aItem The new item to add/modify/delete
+ * @param aCalendar The calendar to do the transaction on
+ * @param aOldItem (optional) some actions require an old item
+ * @param aListener (optional) the listener to call when complete.
+ */
+function doTransaction(aAction, aItem, aCalendar, aOldItem, aListener) {
+ // This is usually a user-initiated transaction, so make sure the calendar
+ // this transaction is happening on is visible.
+ ensureCalendarVisible(aCalendar);
+
+ // Now use the transaction manager to execute the action
+ getTransactionMgr().createAndCommitTxn(aAction,
+ aItem,
+ aCalendar,
+ aOldItem,
+ aListener ? aListener : null);
+ updateUndoRedoMenu();
+}
+
+/**
+ * Undo the last operation done through the transaction manager.
+ */
+function undo() {
+ if (canUndo()) {
+ getTransactionMgr().undo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Redo the last undone operation in the transaction manager.
+ */
+function redo() {
+ if (canRedo()) {
+ getTransactionMgr().redo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Start a batch transaction on the transaction manager. Can be called multiple
+ * times, which nests transactions.
+ */
+function startBatchTransaction() {
+ getTransactionMgr().beginBatch();
+}
+
+/**
+ * End a previously started batch transaction. NOTE: be sure to call this in a
+ * try-catch-finally-block in case you have code that could fail between
+ * startBatchTransaction and this call.
+ */
+function endBatchTransaction() {
+ getTransactionMgr().endBatch();
+ updateUndoRedoMenu();
+}
+
+/**
+ * Checks if the last operation can be undone (or if there is a last operation
+ * at all).
+ */
+function canUndo() {
+ return getTransactionMgr().canUndo();
+}
+
+/**
+ * Checks if the last undone operation can be redone.
+ */
+function canRedo() {
+ return getTransactionMgr().canRedo();
+}
+
+/**
+ * Update the undo and redo commands.
+ */
+function updateUndoRedoMenu() {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+}
+
+function setContextPartstat(value, scope, items) {
+ startBatchTransaction();
+ try {
+ for (let oldItem of items) {
+ // Skip this item if its calendar is read only.
+ if (oldItem.calendar.readOnly) {
+ continue;
+ }
+ if (scope == "all-occurrences") {
+ oldItem = oldItem.parentItem;
+ }
+ let attendee = null;
+ if (cal.isInvitation(oldItem)) {
+ // Check for the invited attendee first, this is more important
+ attendee = cal.getInvitedAttendee(oldItem);
+ } else if (oldItem.organizer && oldItem.getAttendees({}).length) {
+ // Now check the organizer. This should be done last.
+ let calOrgId = oldItem.calendar.getProperty("organizerId");
+ if (calOrgId == oldItem.organizer.id) {
+ attendee = oldItem.organizer;
+ }
+ }
+
+ if (attendee) {
+ let newItem = oldItem.clone();
+ let newAttendee = attendee.clone();
+
+ newAttendee.participationStatus = value;
+ if (newAttendee.isOrganizer) {
+ newItem.organizer = newAttendee;
+ } else {
+ newItem.removeAttendee(attendee);
+ newItem.addAttendee(newAttendee);
+ }
+
+ doTransaction("modify", newItem, newItem.calendar, oldItem, null);
+ }
+ }
+ } catch (e) {
+ cal.ERROR("Error setting partstat: " + e);
+ } finally {
+ endBatchTransaction();
+ }
+}
diff --git a/calendar/base/content/calendar-management.js b/calendar/base/content/calendar-management.js
new file mode 100644
index 000000000..873fc29ab
--- /dev/null
+++ b/calendar/base/content/calendar-management.js
@@ -0,0 +1,444 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported promptDeleteCalendar, loadCalendarManager, unloadCalendarManager,
+ * updateSortOrderPref, calendarListTooltipShowing,
+ * calendarListSetupContextMenu, ensureCalendarVisible, toggleCalendarVisible,
+ * showAllCalendars, showOnlyCalendar, openCalendarSubscriptionsDialog,
+ * calendarOfflineManager
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/**
+ * Get this window's currently selected calendar.
+ *
+ * @return The currently selected calendar.
+ */
+function getSelectedCalendar() {
+ return getCompositeCalendar().defaultCalendar;
+}
+
+/**
+ * Deletes the passed calendar, prompting the user if he really wants to do
+ * this. If there is only one calendar left, no calendar is removed and the user
+ * is not prompted.
+ *
+ * @param aCalendar The calendar to delete.
+ */
+function promptDeleteCalendar(aCalendar) {
+ const nIPS = Components.interfaces.nsIPromptService;
+ const cICM = Components.interfaces.calICalendarManager;
+
+ let calMgr = cal.getCalendarManager();
+ let calendars = calMgr.getCalendars({});
+ if (calendars.length <= 1) {
+ // If this is the last calendar, don't delete it.
+ return;
+ }
+
+ let modes = new Set(aCalendar.getProperty("capabilities.removeModes") || ["unsubscribe"]);
+ let title = cal.calGetString("calendar", "removeCalendarTitle");
+
+ let textKey, b0text, b2text;
+ let removeFlags = 0;
+ let promptFlags = (nIPS.BUTTON_POS_0 * nIPS.BUTTON_TITLE_IS_STRING) +
+ (nIPS.BUTTON_POS_1 * nIPS.BUTTON_TITLE_CANCEL);
+
+ if (modes.has("delete") && !modes.has("unsubscribe")) {
+ textKey = "removeCalendarMessageDelete";
+ promptFlags += nIPS.BUTTON_DELAY_ENABLE;
+ b0text = cal.calGetString("calendar", "removeCalendarButtonDelete");
+ } else if (modes.has("delete")) {
+ textKey = "removeCalendarMessageDeleteOrUnsubscribe";
+ promptFlags += (nIPS.BUTTON_POS_2 * nIPS.BUTTON_TITLE_IS_STRING);
+ b0text = cal.calGetString("calendar", "removeCalendarButtonUnsubscribe");
+ b2text = cal.calGetString("calendar", "removeCalendarButtonDelete");
+ } else if (modes.has("unsubscribe")) {
+ textKey = "removeCalendarMessageUnsubscribe";
+ removeFlags |= cICM.REMOVE_NO_DELETE;
+ b0text = cal.calGetString("calendar", "removeCalendarButtonUnsubscribe");
+ } else {
+ return;
+ }
+
+ let text = cal.calGetString("calendar", textKey, [aCalendar.name]);
+ let res = Services.prompt.confirmEx(window, title, text, promptFlags,
+ b0text, null, b2text, null, {});
+
+ if (res != 1) { // Not canceled
+ if (textKey == "removeCalendarMessageDeleteOrUnsubscribe" && res == 0) {
+ // Both unsubscribing and deleting is possible, but unsubscribing was
+ // requested. Make sure no delete is executed.
+ removeFlags |= cICM.REMOVE_NO_DELETE;
+ }
+
+ calMgr.removeCalendar(aCalendar, removeFlags);
+ }
+}
+
+/**
+ * Called to initialize the calendar manager for a window.
+ */
+function loadCalendarManager() {
+ // Set up the composite calendar in the calendar list widget.
+ let tree = document.getElementById("calendar-list-tree-widget");
+ let compositeCalendar = getCompositeCalendar();
+ tree.compositeCalendar = compositeCalendar;
+
+ // Initialize our composite observer
+ compositeCalendar.addObserver(compositeObserver);
+
+ // Create the home calendar if no calendar exists.
+ let calendars = cal.getCalendarManager().getCalendars({});
+ if (calendars.length) {
+ // migration code to make sure calendars, which do not support caching have cache enabled
+ // required to further clean up on top of bug 1182264
+ for (let calendar of calendars) {
+ if (calendar.getProperty("cache.supported") === false &&
+ calendar.getProperty("cache.enabled") === true) {
+ calendar.deleteProperty("cache.enabled");
+ }
+ }
+ } else {
+ initHomeCalendar();
+ }
+}
+
+/**
+ * Creates the initial "Home" calendar if no calendar exists.
+ */
+function initHomeCalendar() {
+ let calMgr = cal.getCalendarManager();
+ let composite = getCompositeCalendar();
+ let url = cal.makeURL("moz-storage-calendar://");
+ let homeCalendar = calMgr.createCalendar("storage", url);
+ homeCalendar.name = calGetString("calendar", "homeCalendarName");
+ calMgr.registerCalendar(homeCalendar);
+ Preferences.set("calendar.list.sortOrder", homeCalendar.id);
+ composite.addCalendar(homeCalendar);
+
+ // Wrapping this in a try/catch block, as if any of the migration code
+ // fails, the app may not load.
+ if (Preferences.get("calendar.migrator.enabled", true)) {
+ try {
+ gDataMigrator.checkAndMigrate();
+ } catch (e) {
+ Components.utils.reportError("Migrator error: " + e);
+ }
+ }
+
+ return homeCalendar;
+}
+
+/**
+ * Called to clean up the calendar manager for a window.
+ */
+function unloadCalendarManager() {
+ let compositeCalendar = getCompositeCalendar();
+ compositeCalendar.setStatusObserver(null, null);
+ compositeCalendar.removeObserver(compositeObserver);
+}
+
+/**
+ * Updates the sort order preference based on the given event. The event is a
+ * "SortOrderChanged" event, emitted from the calendar-list-tree binding. You
+ * can also pass in an object like { sortOrder: "Space separated calendar ids" }
+ *
+ * @param event The SortOrderChanged event described above.
+ */
+function updateSortOrderPref(event) {
+ let sortOrderString = event.sortOrder.join(" ");
+ Preferences.set("calendar.list.sortOrder", sortOrderString);
+ try {
+ Services.prefs.savePrefFile(null);
+ } catch (e) {
+ cal.ERROR(e);
+ }
+}
+
+/**
+ * Handler function to call when the tooltip is showing on the calendar list.
+ *
+ * @param event The DOM event provoked by the tooltip showing.
+ */
+function calendarListTooltipShowing(event) {
+ let tree = document.getElementById("calendar-list-tree-widget");
+ let calendar = tree.getCalendarFromEvent(event);
+ let tooltipText = false;
+ if (calendar) {
+ let currentStatus = calendar.getProperty("currentStatus");
+ if (!Components.isSuccessCode(currentStatus)) {
+ tooltipText = calGetString("calendar", "tooltipCalendarDisabled", [calendar.name]);
+ } else if (calendar.readOnly) {
+ tooltipText = calGetString("calendar", "tooltipCalendarReadOnly", [calendar.name]);
+ }
+ }
+ setElementValue("calendar-list-tooltip", tooltipText, "label");
+ return (tooltipText != false);
+}
+
+/**
+ * A handler called to set up the context menu on the calendar list.
+ *
+ * @param event The DOM event that caused the context menu to open.
+ * @return Returns true if the context menu should be shown.
+ */
+function calendarListSetupContextMenu(event) {
+ let col = {};
+ let row = {};
+ let calendar;
+ let calendars = getCalendarManager().getCalendars({});
+ let treeNode = document.getElementById("calendar-list-tree-widget");
+ let composite = getCompositeCalendar();
+
+ if (document.popupNode.localName == "tree") {
+ // Using VK_APPS to open the context menu will target the tree
+ // itself. In that case we won't have a client point even for
+ // opening the context menu. The "target" element should then be the
+ // selected calendar.
+ row.value = treeNode.tree.currentIndex;
+ col.value = treeNode.getColumn("calendarname-treecol");
+ calendar = treeNode.getCalendar(row.value);
+ } else {
+ // Using the mouse, the context menu will open on the treechildren
+ // element. Here we can use client points.
+ calendar = treeNode.getCalendarFromEvent(event, col, row);
+ }
+
+ if (col.value &&
+ col.value.element.getAttribute("anonid") == "checkbox-treecol") {
+ // Don't show the context menu if the checkbox was clicked.
+ return false;
+ }
+
+ document.getElementById("list-calendars-context-menu").contextCalendar = calendar;
+
+ // Only enable calendar search if there's actually the chance of finding something:
+ let hasProviders = getCalendarSearchService().getProviders({}).length < 1 && "true";
+ setElementValue("list-calendars-context-find", hasProviders, "collapsed");
+
+ if (calendar) {
+ enableElement("list-calendars-context-edit");
+ enableElement("list-calendars-context-publish");
+
+ enableElement("list-calendars-context-togglevisible");
+ setElementValue("list-calendars-context-togglevisible", false, "collapsed");
+ let stringName = composite.getCalendarById(calendar.id) ? "hideCalendar" : "showCalendar";
+ setElementValue("list-calendars-context-togglevisible",
+ cal.calGetString("calendar", stringName, [calendar.name]),
+ "label");
+ let accessKey = document.getElementById("list-calendars-context-togglevisible")
+ .getAttribute(composite.getCalendarById(calendar.id) ?
+ "accesskeyhide" : "accesskeyshow");
+ setElementValue("list-calendars-context-togglevisible", accessKey, "accesskey");
+
+ enableElement("list-calendars-context-showonly");
+ setElementValue("list-calendars-context-showonly", false, "collapsed");
+ setElementValue("list-calendars-context-showonly",
+ cal.calGetString("calendar", "showOnlyCalendar", [calendar.name]),
+ "label");
+
+ setupDeleteMenuitem("list-calendars-context-delete", calendar);
+ // Only enable the delete calendars item if there is more than one
+ // calendar. We don't want to have the last calendar deleted.
+ setElementValue("list-calendars-context-delete", calendars.length < 2 && "true", "disabled");
+ } else {
+ disableElement("list-calendars-context-edit");
+ disableElement("list-calendars-context-publish");
+ disableElement("list-calendars-context-delete");
+ disableElement("list-calendars-context-togglevisible");
+ setElementValue("list-calendars-context-togglevisible", true, "collapsed");
+ disableElement("list-calendars-context-showonly");
+ setElementValue("list-calendars-context-showonly", true, "collapsed");
+ setupDeleteMenuitem("list-calendars-context-delete", null);
+ }
+ return true;
+}
+
+/**
+ * Changes the "delete calendar" menuitem to have the right label based on the
+ * removeModes. The menuitem must have the attributes "labelremove",
+ * "labeldelete" and "labelunsubscribe".
+ *
+ * @param aDeleteId The id of the menuitem to delete the calendar
+ */
+function setupDeleteMenuitem(aDeleteId, aCalendar) {
+ let calendar = (aCalendar === undefined ? getSelectedCalendar() : aCalendar);
+ let modes = new Set(calendar ? calendar.getProperty("capabilities.removeModes") || ["unsubscribe"] : []);
+
+ let type = "remove";
+ if (modes.has("delete") && !modes.has("unsubscribe")) {
+ type = "delete";
+ } else if (modes.has("unsubscribe") && !modes.has("delete")) {
+ type = "unsubscribe";
+ }
+
+ let deleteItem = document.getElementById(aDeleteId);
+ setElementValue(deleteItem, deleteItem.getAttribute("label" + type), "label");
+ setElementValue(deleteItem, deleteItem.getAttribute("accesskey" + type), "accesskey");
+ setElementValue(deleteItem, modes.size == 0 && "true", "disabled");
+}
+
+/**
+ * Makes sure the passed calendar is visible to the user
+ *
+ * @param aCalendar The calendar to make visible.
+ */
+function ensureCalendarVisible(aCalendar) {
+ // We use the main window's calendar list to ensure that the calendar is visible.
+ // If the main window has been closed this function may still be called,
+ // like when an event/task window is still open and the user clicks 'save',
+ // thus we have the extra checks.
+ let list = document.getElementById("calendar-list-tree-widget");
+ if (list && list.ensureCalendarVisible) {
+ list.ensureCalendarVisible(aCalendar);
+ }
+}
+
+/**
+ * Hides the specified calendar if it is visible, or shows it if it is hidden.
+ *
+ * @param aCalendar The calendar to show or hide
+ */
+function toggleCalendarVisible(aCalendar) {
+ let composite = getCompositeCalendar();
+ if (composite.getCalendarById(aCalendar.id)) {
+ composite.removeCalendar(aCalendar);
+ } else {
+ composite.addCalendar(aCalendar);
+ }
+}
+
+/**
+ * Shows all hidden calendars.
+ */
+function showAllCalendars() {
+ let composite = getCompositeCalendar();
+ let cals = cal.getCalendarManager().getCalendars({});
+
+ composite.startBatch();
+ for (let calendar of cals) {
+ if (!composite.getCalendarById(calendar.id)) {
+ composite.addCalendar(calendar);
+ }
+ }
+ composite.endBatch();
+}
+
+/**
+ * Shows only the specified calendar, and hides all others.
+ *
+ * @param aCalendar The calendar to show as the only visible calendar
+ */
+function showOnlyCalendar(aCalendar) {
+ let composite = getCompositeCalendar();
+ let cals = composite.getCalendars({}) || [];
+
+ composite.startBatch();
+ for (let calendar of cals) {
+ if (calendar.id != aCalendar.id) {
+ composite.removeCalendar(calendar);
+ }
+ }
+ composite.addCalendar(aCalendar);
+ composite.endBatch();
+}
+
+var compositeObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIObserver,
+ Components.interfaces.calICompositeObserver]),
+
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onAddItem: function() {},
+ onModifyItem: function() {},
+ onDeleteItem: function() {},
+ onError: function() {},
+ onPropertyChanged: function() {},
+ onPropertyDeleting: function() {},
+
+ onLoad: function() {
+ calendarUpdateNewItemsCommand();
+ document.commandDispatcher.updateCommands("calendar_commands");
+ },
+
+ onCalendarAdded: function(aCalendar) {
+ // Update the calendar commands for number of remote calendars and for
+ // more than one calendar
+ document.commandDispatcher.updateCommands("calendar_commands");
+ },
+
+ onCalendarRemoved: function(aCalendar) {
+ // Update commands to disallow deleting the last calendar and only
+ // allowing reload remote calendars when there are remote calendars.
+ document.commandDispatcher.updateCommands("calendar_commands");
+ },
+
+ onDefaultCalendarChanged: function(aNewCalendar) {
+ // A new default calendar may mean that the new calendar has different
+ // ACLs. Make sure the commands are updated.
+ calendarUpdateNewItemsCommand();
+ document.commandDispatcher.updateCommands("calendar_commands");
+ }
+};
+
+/**
+ * Opens the subscriptions dialog modally.
+ */
+function openCalendarSubscriptionsDialog() {
+ // the dialog will reset this to auto when it is done loading
+ window.setCursor("wait");
+
+ // open the dialog modally
+ window.openDialog("chrome://calendar/content/calendar-subscriptions-dialog.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable");
+}
+
+/**
+ * Calendar Offline Manager
+ */
+var calendarOfflineManager = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
+
+ init: function() {
+ if (this.initialized) {
+ throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
+ }
+ Services.obs.addObserver(this, "network:offline-status-changed", false);
+
+ this.updateOfflineUI(!this.isOnline());
+ this.initialized = true;
+ },
+
+ uninit: function() {
+ if (!this.initialized) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ Services.obs.removeObserver(this, "network:offline-status-changed", false);
+ this.initialized = false;
+ },
+
+ isOnline: function() {
+ return !Services.io.offline;
+ },
+
+ updateOfflineUI: function(aIsOffline) {
+ // Refresh the current view
+ currentView().goToDay(currentView().selectedDay);
+
+ // Set up disabled locks for offline
+ document.commandDispatcher.updateCommands("calendar_commands");
+ },
+
+ observe: function(aSubject, aTopic, aState) {
+ if (aTopic == "network:offline-status-changed") {
+ this.updateOfflineUI(aState == "offline");
+ }
+ }
+};
diff --git a/calendar/base/content/calendar-menus.xml b/calendar/base/content/calendar-menus.xml
new file mode 100644
index 000000000..c210306fb
--- /dev/null
+++ b/calendar/base/content/calendar-menus.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<bindings id="calendar-menu-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="task-menupopup" extends="xul:menupopup">
+ <implementation>
+ <field name="mType">null</field>;
+ <field name="mPopupHandler">null</field>
+ <field name="mParentMenuPopup">null</field>
+
+ <constructor><![CDATA[
+ this.mPopupHandler = () => { this.schangeMenuByPropertyName(); };
+ this.mParentMenuPopup = getParentNodeOrThis(this, "menupopup");
+ this.mParentMenuPopup.addEventListener("popupshowing", this.mPopupHandler, true);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.mParentMenuPopup.removeEventListener("popupshowing", this.mPopupHandler, true);
+ ]]></destructor>
+
+ <!-- This method checks a command which naming follows
+ the notation 'calendar_' + mType + ' + '-' + propertyValue + 'command',
+ when its propertyValue part matches the propertyValue of the selected tasks
+ as long as the selected tasks share common propertyValues. -->
+ <method name="changeMenuByPropertyName">
+ <body><![CDATA[
+ let liveList = document.getAnonymousNodes(this);
+ for (let item of liveList) {
+ let commandName = item.getAttribute("command");
+ let command = document.getElementById(commandName);
+ if (command) {
+ command.setAttribute("checked", "false");
+ item.setAttribute("checked", "false");
+ }
+ }
+ let propertyValue;
+ if (gTabmail && gTabmail.currentTabInfo.mode.type == "calendarTask") {
+ // We are in a task tab (editing a single task).
+ propertyValue = gConfig[this.mType];
+ } else {
+ // We are in the Tasks tab.
+ let tasks = getSelectedTasks();
+ let tasksSelected = (tasks != null) && (tasks.length > 0);
+ if (tasksSelected) {
+ let task = tasks[0];
+ if (isPropertyValueSame(tasks, this.mType)) {
+ propertyValue = task[this.mType];
+ }
+ } else {
+ applyAttributeToMenuChildren(this, "disabled", !tasksSelected);
+ }
+ }
+ if (propertyValue || propertyValue == 0) {
+ let command = document.getElementById("calendar_" + this.mType + "-" + propertyValue + "_command");
+ if (command) {
+ command.setAttribute("checked", "true");
+ }
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="task-progress-menupopup" extends="chrome://calendar/content/calendar-menus.xml#task-menupopup">
+ <content>
+ <xul:menuitem anonid="percent-0-menuitem"
+ type="checkbox"
+ label="&progress.level.0;"
+ accesskey="&progress.level.0.accesskey;"
+ observes="calendar_percentComplete-0_command"
+ command="calendar_percentComplete-0_command"/>
+ <xul:menuitem anonid="percent-25-menuitem"
+ type="checkbox"
+ label="&progress.level.25;"
+ accesskey="&progress.level.25.accesskey;"
+ observes="calendar_percentComplete-25_command"
+ command="calendar_percentComplete-25_command"/>
+ <xul:menuitem anonid="percent-50-menuitem"
+ type="checkbox"
+ label="&progress.level.50;"
+ accesskey="&progress.level.50.accesskey;"
+ observes="calendar_percentComplete-50_command"
+ command="calendar_percentComplete-50_command"/>
+ <xul:menuitem anonid="percent-75-menuitem"
+ type="checkbox"
+ label="&progress.level.75;"
+ accesskey="&progress.level.75.accesskey;"
+ observes="calendar_percentComplete-75_command"
+ command="calendar_percentComplete-75_command"/>
+ <xul:menuitem anonid="percent-100-menuitem"
+ type="checkbox"
+ label="&progress.level.100;"
+ accesskey="&progress.level.100.accesskey;"
+ observes="calendar_percentComplete-100_command"
+ command="calendar_percentComplete-100_command"/>
+ <children/>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ this.mType = "percentComplete";
+ this.changeMenuByPropertyName();
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <binding id="task-priority-menupopup" extends="chrome://calendar/content/calendar-menus.xml#task-menupopup">
+ <content>
+ <xul:menuitem id="priority-0-menuitem"
+ type="checkbox"
+ label="&priority.level.none;"
+ accesskey="&priority.level.none.accesskey;"
+ command="calendar_priority-0_command"
+ observes="calendar_priority-0_command"/>
+ <xul:menuitem id="priority-9-menuitem"
+ type="checkbox"
+ label="&priority.level.low;"
+ accesskey="&priority.level.low.accesskey;"
+ command="calendar_priority-9_command"
+ observes="calendar_priority-9_command"/>
+ <xul:menuitem id="priority-5-menuitem"
+ type="checkbox"
+ label="&priority.level.normal;"
+ accesskey="&priority.level.normal.accesskey;"
+ command="calendar_priority-5_command"
+ observes="calendar_priority-5_command"/>
+ <xul:menuitem id="priority-1-menuitem"
+ type="checkbox"
+ label="&priority.level.high;"
+ accesskey="&priority.level.high.accesskey;"
+ command="calendar_priority-1_command"
+ observes="calendar_priority-1_command"/>
+ <children/>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ this.mType = "priority";
+ this.changeMenuByPropertyName();
+ ]]></constructor>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-month-view.xml b/calendar/base/content/calendar-month-view.xml
new file mode 100644
index 000000000..eab5f8f91
--- /dev/null
+++ b/calendar/base/content/calendar-month-view.xml
@@ -0,0 +1,1137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Note that this file depends on helper functions in calUtils.js-->
+
+<!DOCTYPE bindings SYSTEM "chrome://global/locale/global.dtd" >
+
+<bindings id="calendar-month-view-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="calendar-month-day-box-item" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
+ <content mousethrough="never" tooltip="itemTooltip">
+ <xul:vbox flex="1">
+ <xul:hbox>
+ <xul:box anonid="event-container"
+ class="calendar-color-box"
+ xbl:inherits="calendar-uri,calendar-id"
+ flex="1">
+ <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
+ <xul:stack anonid="eventbox"
+ class="calendar-event-box-container"
+ xbl:inherits="readonly,flashing,alarm,allday,priority,progress,status,calendar,categories"
+ flex="1">
+ <xul:hbox anonid="event-detail-box"
+ class="calendar-event-details">
+ <xul:vbox pack="center">
+ <xul:image anonid="item-icon"
+ class="calendar-item-image"
+ xbl:inherits="progress,allday,itemType,todoType"/>
+ </xul:vbox>
+ <xul:label anonid="item-label"
+ class="calendar-month-day-box-item-label"
+ xbl:inherits="context"/>
+ <xul:vbox align="left"
+ flex="1"
+ xbl:inherits="context">
+ <xul:label anonid="event-name"
+ crop="end"
+ flex="1"
+ style="margin: 0;"/>
+ <xul:textbox anonid="event-name-textbox"
+ class="plain calendar-event-name-textbox"
+ crop="end"
+ hidden="true"
+ wrap="true"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ <xul:stack anonid="category-box-stack">
+ <xul:calendar-category-box anonid="category-box" xbl:inherits="categories" pack="end"/>
+ <xul:hbox align="center">
+ <xul:hbox anonid="alarm-icons-box"
+ class="alarm-icons-box"
+ pack="end"
+ align="top"
+ xbl:inherits="flashing"/>
+ <xul:image anonid="item-classification-box"
+ class="item-classification-box"
+ pack="end"/>
+ </xul:hbox>
+ </xul:stack>
+ </xul:hbox>
+ </xul:stack>
+ </xul:box>
+ </xul:box>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <property name="occurrence">
+ <getter><![CDATA[
+ return this.mOccurrence;
+ ]]></getter>
+ <setter><![CDATA[
+ ASSERT(!this.mOccurrence, "Code changes needed to set the occurrence twice", true);
+ this.mOccurrence = val;
+ if (cal.isEvent(val)) {
+ if (!val.startDate.isDate) {
+ let label = document.getAnonymousElementByAttribute(this, "anonid", "item-label");
+ let formatter = Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+ let timezone = this.calendarView ? this.calendarView.mTimezone
+ : calendarDefaultTimezone();
+ let parentDate = ensureDateTime(this.parentBox.date);
+ let startTime = val.startDate.getInTimezone(timezone);
+ let endTime = val.endDate.getInTimezone(timezone);
+ let nextDay = parentDate.clone();
+ nextDay.day++;
+ let comp = endTime.compare(nextDay);
+ if (startTime.compare(parentDate) == -1) {
+ if (comp == 1) {
+ label.value = "↔";
+ } else if (comp == 0) {
+ label.value = "↤";
+ } else {
+ label.value = "⇥ " + formatter.formatTime(endTime);
+ }
+ } else if (comp == 1) {
+ label.value = "⇤ " + formatter.formatTime(startTime);
+ } else {
+ label.value = formatter.formatTime(startTime);
+ }
+ label.setAttribute("time", "true");
+ }
+ }
+
+ this.setEditableLabel();
+ this.setCSSClasses();
+ return val;
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-month-day-box" extends="chrome://calendar/content/widgets/calendar-widgets.xml#dragndropContainer">
+ <content orient="vertical">
+ <xul:hbox anonid="monthday-labels" style="overflow: hidden;">
+ <xul:label anonid="week-label"
+ flex="1"
+ crop="end"
+ hidden="true"
+ mousethrough="always"
+ class="calendar-month-day-box-week-label calendar-month-day-box-date-label"
+ xbl:inherits="relation,selected"/>
+ <xul:label anonid="day-label"
+ flex="1"
+ mousethrough="always"
+ class="calendar-month-day-box-date-label"
+ xbl:inherits="relation,selected,value"/>
+ </xul:hbox>
+ <xul:vbox anonid="day-items" class="calendar-month-day-box-items-box" flex="1">
+ <children/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <field name="mDate">null</field>
+ <field name="mItemHash">{}</field>
+ <field name="mShowMonthLabel">false</field>
+
+ <property name="date"
+ onget="return this.mDate"
+ onset="this.setDate(val); return val;"/>
+
+ <property name="selected">
+ <getter><![CDATA[
+ let sel = this.getAttribute("selected");
+ if (sel && sel == "true") {
+ return true;
+ }
+
+ return false;
+ ]]></getter>
+ <setter><![CDATA[
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="dayitems">
+ <getter>return document.getAnonymousElementByAttribute(this, "anonid", "day-items");</getter>
+ </property>
+
+ <property name="showMonthLabel">
+ <getter><![CDATA[
+ return this.mShowMonthLabel;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.mShowMonthLabel == val) {
+ return val;
+ }
+ this.mShowMonthLabel = val;
+
+ if (!this.mDate) {
+ return val;
+ }
+ if (val) {
+ this.setAttribute("value", getDateFormatter().formatDateWithoutYear(this.mDate));
+ } else {
+ this.setAttribute("value", this.mDate.day);
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="setDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ if (!aDate) {
+ throw Components.results.NS_ERROR_NULL_POINTER;
+ }
+
+ // Remove all the old events
+ this.mItemHash = {};
+ removeChildren(this);
+
+ if (this.mDate && this.mDate.compare(aDate) == 0) {
+ return;
+ }
+
+ this.mDate = aDate;
+
+ // Set up DOM attributes for custom CSS coloring.
+ let weekTitle = cal.getWeekInfoService().getWeekTitle(aDate);
+ this.setAttribute("year", aDate.year);
+ this.setAttribute("month", aDate.month + 1);
+ this.setAttribute("week", weekTitle);
+ this.setAttribute("day", aDate.day);
+
+ if (this.mShowMonthLabel) {
+ let monthName = calGetString("dateFormat", "month." + (aDate.month + 1) + ".Mmm");
+ this.setAttribute("value", aDate.day + " " + monthName);
+ } else {
+ this.setAttribute("value", aDate.day);
+ }
+ ]]></body>
+ </method>
+
+ <method name="addItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ if (aItem.hashId in this.mItemHash) {
+ this.deleteItem(aItem);
+ }
+
+ let box = createXULElement("calendar-month-day-box-item");
+ let context = this.getAttribute("item-context") ||
+ this.getAttribute("context");
+ box.setAttribute("context", context);
+ box.setAttribute("calendar-uri", aItem.calendar.uri.spec);
+ box.setAttribute("calendar-id", aItem.calendar.id);
+
+ cal.binaryInsertNode(this, box, aItem, cal.view.compareItems, false);
+
+ box.calendarView = this.calendarView;
+ box.item = aItem;
+ box.parentBox = this;
+ box.occurrence = aItem;
+
+ this.mItemHash[aItem.hashId] = box;
+ return box;
+ ]]></body>
+ </method>
+
+ <method name="selectItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ if (aItem.hashId in this.mItemHash) {
+ this.mItemHash[aItem.hashId].selected = true;
+ }
+ ]]></body>
+ </method>
+
+ <method name="unselectItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ if (aItem.hashId in this.mItemHash) {
+ this.mItemHash[aItem.hashId].selected = false;
+ }
+ ]]></body>
+ </method>
+
+ <method name="deleteItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ if (aItem.hashId in this.mItemHash) {
+ let node = this.mItemHash[aItem.hashId];
+ node.remove();
+ delete this.mItemHash[aItem.hashId];
+ }
+ ]]></body>
+ </method>
+
+ <method name="onDropItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ // When item's timezone is different than the default one, the
+ // item might get moved on a day different than the drop day.
+ // Changing the drop day allows to compensate a possible difference.
+
+ // Figure out if the timezones cause a days difference.
+ let start = (aItem[calGetStartDateProp(aItem)] ||
+ aItem[calGetEndDateProp(aItem)]).clone();
+ let dayboxDate = this.mDate.clone();
+ if (start.timezone != dayboxDate.timezone) {
+ let startInDefaultTz = start.clone().getInTimezone(dayboxDate.timezone);
+ start.isDate = true;
+ startInDefaultTz.isDate = true;
+ startInDefaultTz.timezone = start.timezone;
+ let dayDiff = start.subtractDate(startInDefaultTz);
+ // Change the day where to drop the item.
+ dayboxDate.addDuration(dayDiff);
+ }
+
+ return cal.moveItem(aItem, dayboxDate);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown"><![CDATA[
+ event.stopPropagation();
+ if (this.mDate) {
+ this.calendarView.selectedDay = this.mDate;
+ }
+ ]]></handler>
+ <handler event="dblclick"><![CDATA[
+ event.stopPropagation();
+ this.calendarView.controller.createNewEvent();
+ ]]></handler>
+ <handler event="click" button="0"><![CDATA[
+ if (!(event.ctrlKey || event.metaKey)) {
+ this.calendarView.setSelectedItems(0, []);
+ }
+ ]]></handler>
+ <handler event="wheel"><![CDATA[
+ if (getParentNodeOrThisByAttribute(event.originalTarget, "anonid", "day-label") == null) {
+ if (this.dayitems.scrollHeight > this.dayitems.clientHeight) {
+ event.stopPropagation();
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-month-base-view" extends="chrome://calendar/content/calendar-base-view.xml#calendar-base-view">
+ <content style="overflow: auto;" flex="1" xbl:inherits="context,item-context">
+ <xul:vbox anonid="mainbox" flex="1">
+ <xul:hbox class="labeldaybox-container"
+ anonid="labeldaybox"
+ equalsize="always"/>
+
+ <xul:grid anonid="monthgrid" flex="1">
+ <xul:columns anonid="monthgridcolumns" equalsize="always">
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ <xul:column flex="1" class="calendar-month-view-grid-column"/>
+ </xul:columns>
+
+ <xul:rows anonid="monthgridrows" equalsize="always">
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ <xul:row flex="1" class="calendar-month-view-grid-row">
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ <xul:calendar-month-day-box/>
+ </xul:row>
+ </xul:rows>
+ </xul:grid>
+ </xul:vbox>
+ </content>
+
+ <implementation implements="calICalendarView">
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+ Components.utils.import("resource://calendar/modules/calViewUtils.jsm");
+
+ // Set the preference for the default start of the week
+ this.weekStartOffset = Preferences.get("calendar.week.start", 0);
+
+ for (let i = 0; i < 7; i++) {
+ let hdr = createXULElement("calendar-day-label");
+ this.labeldaybox.appendChild(hdr);
+ hdr.weekDay = (i + this.mWeekStartOffset) % 7;
+ hdr.shortWeekNames = false;
+ }
+
+ // Set the preference for displaying the week number
+ this.mShowWeekNumber = Preferences.get("calendar.view-minimonth.showWeekNumber", true);
+ ]]></constructor>
+
+ <!-- fields -->
+ <field name="mDateBoxes">null</field>
+ <field name="mSelectedDayBox">null</field>
+
+ <field name="mShowDaysOutsideMonth">true</field>
+ <field name="mShowFullMonth">true</field>
+ <field name="mShowWeekNumber">true</field>
+
+ <field name="mClickedTime">null</field>
+
+ <!-- other methods -->
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ let needsrelayout = (aAttr == "context" || aAttr == "item-context");
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+
+ if (needsrelayout) {
+ this.relayout();
+ }
+
+ return ret;
+ ]]></body>
+ </method>
+
+ <!-- calICalendarView -->
+
+ <property name="supportsDisjointDates" readonly="true"
+ onget="return false;"/>
+ <property name="hasDisjointDates" readonly="true"
+ onget="return false;"/>
+
+ <property name="startDate" readonly="true"
+ onget="return this.mStartDate"/>
+
+ <property name="endDate" readonly="true"
+ onget="return this.mEndDate"/>
+
+ <property name="showFullMonth">
+ <getter><![CDATA[
+ return this.mShowFullMonth;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mShowFullMonth = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- this property may be overridden by the
+ descendent classes if neeeded -->
+ <property name="weeksInView">
+ <getter><![CDATA[
+ return 0;
+ ]]></getter>
+ <setter><![CDATA[
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="handlePreference">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aPreference"/>
+ <body><![CDATA[
+ aSubject.QueryInterface(Components.interfaces.nsIPrefBranch);
+
+ switch (aPreference) {
+ case "calendar.previousweeks.inview":
+ this.updateDaysOffPrefs();
+ this.refreshView();
+ break;
+
+ case "calendar.week.start":
+ this.weekStartOffset = aSubject.getIntPref(aPreference);
+ // Refresh the view so the settings take effect
+ this.refreshView();
+ break;
+
+ case "calendar.weeks.inview":
+ this.weeksInView = aSubject.getIntPref(aPreference);
+ break;
+
+ case "calendar.view-minimonth.showWeekNumber":
+ this.mShowWeekNumber = aSubject.getBoolPref(aPreference);
+ if (this.mShowWeekNumber) {
+ this.refreshView();
+ } else {
+ this.hideWeekNumbers();
+ }
+ break;
+
+ default:
+ this.handleCommonPreference(aSubject, aTopic, aPreference);
+ break;
+ }
+ return;
+ ]]></body>
+ </method>
+
+ <method name="getSelectedItems">
+ <parameter name="aCount"/>
+ <body><![CDATA[
+ aCount.value = this.mSelectedItems.length;
+ return this.mSelectedItems;
+ ]]></body>
+ </method>
+
+ <method name="setSelectedItems">
+ <parameter name="aCount"/>
+ <parameter name="aItems"/>
+ <parameter name="aSuppressEvent"/>
+ <body><![CDATA[
+ if (this.mSelectedItems.length) {
+ for (let item of this.mSelectedItems) {
+ let oldboxes = this.findDayBoxesForItem(item);
+ for (let oldbox of oldboxes) {
+ oldbox.unselectItem(item);
+ }
+ }
+ }
+
+ this.mSelectedItems = aItems || [];
+
+ if (this.mSelectedItems.length) {
+ for (let item of this.mSelectedItems) {
+ let newboxes = this.findDayBoxesForItem(item);
+ for (let newbox of newboxes) {
+ newbox.selectItem(item);
+ }
+ }
+ }
+
+ if (!aSuppressEvent) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+ ]]></body>
+ </method>
+
+ <method name="centerSelectedItems">
+ <body>
+ </body>
+ </method>
+
+ <property name="selectedDay">
+ <getter><![CDATA[
+ if (this.mSelectedDayBox) {
+ return this.mSelectedDayBox.date.clone();
+ }
+
+ return null;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.mSelectedDayBox) {
+ this.mSelectedDayBox.selected = false;
+ }
+
+ let realVal = val;
+ if (!realVal.isDate) {
+ realVal = val.clone();
+ realVal.isDate = true;
+ }
+ let box = this.findDayBoxForDate(realVal);
+ if (box) {
+ box.selected = true;
+ this.mSelectedDayBox = box;
+ }
+ this.fireEvent("dayselect", realVal);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="selectedDateTime">
+ <getter><![CDATA[
+ return getDefaultStartDate(this.selectedDay);
+ ]]></getter>
+ <setter><![CDATA[
+ this.mClickedTime = val;
+ ]]></setter>
+ </property>
+
+ <method name="showDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ // If aDate is null it means that only a refresh is needed
+ // without changing the start and end of the view.
+ if (aDate) {
+ this.setDateRange(aDate.startOfMonth, aDate.endOfMonth);
+ this.selectedDay = aDate;
+ } else {
+ this.refresh();
+ // Refresh the selected day if it doesn't appear in the view.
+ this.selectedDay = this.selectedDay;
+ }
+ ]]></body>
+ </method>
+
+ <method name="onResize">
+ <parameter name="aBinding"/>
+ <body><![CDATA[
+ aBinding.adjustWeekdayLength();
+ // Delete the timer for the time indicator in day/week view.
+ timeIndicator.cancel();
+ ]]></body>
+ </method>
+
+ <method name="setDateRange">
+ <parameter name="aStartDate"/>
+ <parameter name="aEndDate"/>
+ <body><![CDATA[
+ this.rangeStartDate = aStartDate;
+ this.rangeEndDate = aEndDate;
+ let viewStart = cal.getWeekInfoService().getStartOfWeek(
+ aStartDate.getInTimezone(this.mTimezone));
+ let viewEnd = cal.getWeekInfoService().getEndOfWeek(
+ aEndDate.getInTimezone(this.mTimezone));
+
+ viewStart.isDate = true;
+ viewStart.makeImmutable();
+ viewEnd.isDate = true;
+ viewEnd.makeImmutable();
+ this.mStartDate = viewStart;
+ this.mEndDate = viewEnd;
+
+ // check values of tasksInView, workdaysOnly, showCompleted
+ // see setDateRange comment in calendar-multiday-view.xml
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ // Update the navigation bar only when changes are related to the current view.
+ if (this.isVisible()) {
+ cal.navigationBar.setDateRange(aStartDate, aEndDate);
+ }
+
+ // Check whether view range has been changed since last call to
+ // relayout()
+ if (!this.mViewStart || !this.mViewEnd ||
+ this.mViewEnd.compare(viewEnd) != 0 ||
+ this.mViewStart.compare(viewStart) != 0 ||
+ this.mToggleStatus != toggleStatus) {
+ this.refresh();
+ }
+
+ ]]></body>
+ </method>
+
+ <method name="getDateList">
+ <parameter name="aCount"/>
+ <body><![CDATA[
+ if (!this.mStartDate || !this.mEndDate) {
+ aCount.value = 0;
+ return [];
+ }
+
+ let results = [];
+ let curDate = this.mStartDate.clone();
+ curDate.isDate = true;
+
+ while (curDate.compare(this.mEndDate) <= 0) {
+ results.push(curDate.clone());
+ curDate.day += 1;
+ }
+ aCount.value = results.length;
+ return results;
+ ]]></body>
+ </method>
+
+ <!-- public properties and methods -->
+
+ <!-- whether to show days outside of the current month -->
+ <property name="showDaysOutsideMonth">
+ <getter><![CDATA[
+ return this.mShowDaysOutsideMonth;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.mShowDaysOutsideMonth != val) {
+ this.mShowDaysOutsideMonth = val;
+ this.refresh();
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- private properties and methods -->
+
+ <property name="monthgrid" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'monthgrid');"/>
+
+ <property name="monthgridrows" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'monthgridrows');"/>
+
+ <method name="relayout">
+ <body><![CDATA[
+ // Adjust headers based on the starting day of the week, if necessary
+ if (this.labeldaybox.firstChild.weekDay != this.weekStartOffset) {
+ for (let i = 0; i < this.labeldaybox.childNodes.length; i++) {
+ this.labeldaybox.childNodes[i].weekDay = (i + this.weekStartOffset) % 7;
+ }
+ }
+
+ if (this.mSelectedItems.length) {
+ this.mSelectedItems = [];
+ }
+
+ if (!this.mStartDate || !this.mEndDate) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+
+ // Days that are not in the main month on display are displayed with
+ // a gray background. Unless the month actually starts on a Sunday,
+ // this means that mStartDate.month is 1 month less than the main month
+ let mainMonth = this.mStartDate.month;
+ if (this.mStartDate.day != 1) {
+ mainMonth++;
+ mainMonth = mainMonth % 12;
+ }
+
+ let dateBoxes = [];
+ let today = this.today();
+
+ // This gets set to true, telling us to collapse the rest of the rows
+ let finished = false;
+ let dateList = this.getDateList({});
+
+ // This allows to find the first column of dayboxes where to set the
+ // week labels taking into account whether days-off are displayed or not.
+ let weekLabelColumnPos = -1;
+
+ let rows = this.monthgridrows.childNodes;
+
+ // Iterate through each monthgridrow and set up the day-boxes that
+ // are its child nodes. Remember, childNodes is not a normal array,
+ // so don't use the in operator if you don't want extra properties
+ // coming out.
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ // If we've already assigned all of the day-boxes that we need, just
+ // collapse the rest of the rows, otherwise expand them if needed.
+ if (finished) {
+ row.setAttribute("collapsed", true);
+ continue;
+ } else {
+ row.removeAttribute("collapsed");
+ }
+ for (let j = 0; j < row.childNodes.length; j++) {
+ let daybox = row.childNodes[j];
+ let date = dateList[dateBoxes.length];
+
+ // Remove the attribute "relation" for all the column headers.
+ // Consider only the first row index otherwise it will be
+ // removed again afterwards the correct setting.
+ if (i == 0) {
+ this.labeldaybox.childNodes[j].removeAttribute("relation");
+ }
+
+ daybox.setAttribute("context", this.getAttribute("context"));
+ daybox.setAttribute("item-context", this.getAttribute("item-context") || this.getAttribute("context"));
+
+ // Set the box-class depending on if this box displays a day in
+ // the month being currently shown or not.
+ let boxClass;
+ if (this.showFullMonth) {
+ boxClass = "calendar-month-day-box-" +
+ (mainMonth == date.month ? "current-month" : "other-month");
+ } else {
+ boxClass = "calendar-month-day-box-current-month";
+ }
+ if (this.mDaysOffArray.some(dayOffNum => dayOffNum == date.weekday)) {
+ boxClass = "calendar-month-day-box-day-off " + boxClass;
+ }
+
+ // Set up date relations
+ switch (date.compare(today)) {
+ case -1:
+ daybox.setAttribute("relation", "past");
+ break;
+ case 0:
+ daybox.setAttribute("relation", "today");
+ this.labeldaybox.childNodes[j].setAttribute("relation", "today");
+ break;
+ case 1:
+ daybox.setAttribute("relation", "future");
+ break;
+ }
+
+ // Set up label with the week number in the first day of the row.
+ if (this.mShowWeekNumber) {
+ let weekLabel = document.getAnonymousElementByAttribute(daybox, "anonid", "week-label");
+ if (weekLabelColumnPos < 0) {
+ let isDayOff = this.mDaysOffArray.includes((j + this.mWeekStartOffset) % 7);
+ if (this.mDisplayDaysOff || !isDayOff) {
+ weekLabelColumnPos = j;
+ }
+ }
+ // Build and set the label.
+ if (j == weekLabelColumnPos) {
+ weekLabel.removeAttribute("hidden");
+ let weekNumber = cal.getWeekInfoService().getWeekTitle(date);
+ let weekString = cal.calGetString("calendar", "abbreviationOfWeek", [weekNumber]);
+ weekLabel.value = weekString;
+ } else {
+ weekLabel.hidden = true;
+ }
+ }
+
+ daybox.setAttribute("class", boxClass);
+
+ daybox.setDate(date);
+ if (date.day == 1 || date.day == date.endOfMonth.day) {
+ daybox.showMonthLabel = true;
+ } else {
+ daybox.showMonthLabel = false;
+ }
+ daybox.calendarView = this;
+ daybox.date = date;
+ dateBoxes.push(daybox);
+
+ // If we've now assigned all of our dates, set this to true so we
+ // know we can just collapse the rest of the rows.
+ if (dateBoxes.length == dateList.length) {
+ finished = true;
+ }
+ }
+ }
+
+ // If we're not showing a full month, then add a few extra labels to
+ // help the user orient themselves in the view.
+ if (!this.mShowFullMonth) {
+ dateBoxes[0].showMonthLabel = true;
+ dateBoxes[dateBoxes.length - 1].showMonthLabel = true;
+ }
+
+ // Store these, so that we can access them later
+ this.mDateBoxes = dateBoxes;
+ this.hideDaysOff();
+
+ this.adjustWeekdayLength();
+
+ // Store the start and end of current view. Next time when
+ // setDateRange is called, it will use mViewStart and mViewEnd to
+ // check if view range has been changed.
+ this.mViewStart = this.mStartDate;
+ this.mViewEnd = this.mEndDate;
+
+ // Store toggle status of current view
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ this.mToggleStatus = toggleStatus;
+ ]]></body>
+ </method>
+
+ <method name="hideWeekNumbers">
+ <body><![CDATA[
+ let rows = this.monthgridrows.childNodes;
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ for (let j = 0; j < row.childNodes.length; j++) {
+ let daybox = row.childNodes[j];
+ let weekLabel = document.getAnonymousElementByAttribute(daybox, "anonid", "week-label");
+ weekLabel.hidden = true;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="hideDaysOff">
+ <body><![CDATA[
+ let columns = document.getAnonymousElementByAttribute(this, "anonid", "monthgridcolumns").childNodes;
+ let headerkids = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox").childNodes;
+ for (let i = 0; i < columns.length; i++) {
+ let dayForColumn = (i + this.mWeekStartOffset) % 7;
+ let dayOff = this.mDaysOffArray.includes(dayForColumn);
+ columns[i].collapsed = dayOff && !this.mDisplayDaysOff;
+ headerkids[i].collapsed = dayOff && !this.mDisplayDaysOff;
+ }
+ ]]></body>
+ </method>
+
+ <method name="findDayBoxForDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ if (!this.mDateBoxes) {
+ return null;
+ }
+ for (let box of this.mDateBoxes) {
+ if (box.mDate.compare(aDate) == 0) {
+ return box;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="findDayBoxesForItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let targetDate = null;
+ let finishDate = null;
+ let boxes = [];
+
+ // All our boxes are in default tz, so we need these times in them too.
+ if (cal.isEvent(aItem)) {
+ targetDate = aItem.startDate.getInTimezone(this.mTimezone);
+ finishDate = aItem.endDate.getInTimezone(this.mTimezone);
+ } else if (cal.isToDo(aItem)) {
+ // Consider tasks without entry OR due date.
+ if (aItem.entryDate || aItem.dueDate) {
+ targetDate = (aItem.entryDate || aItem.dueDate).getInTimezone(this.mTimezone);
+ finishDate = (aItem.dueDate || aItem.entryDate).getInTimezone(this.mTimezone);
+ }
+ }
+
+ if (!targetDate) {
+ return boxes;
+ }
+
+ if (!finishDate) {
+ let maybeBox = this.findDayBoxForDate(targetDate);
+ if (maybeBox) {
+ boxes.push(maybeBox);
+ }
+ return boxes;
+ }
+
+ if (!targetDate.isDate) {
+ // Reset the time to 00:00, so that we really get all the boxes
+ targetDate.hour = 0;
+ targetDate.minute = 0;
+ targetDate.second = 0;
+ }
+
+ if (targetDate.compare(finishDate) == 0) {
+ // We have also to handle zero length events in particular for
+ // tasks without entry or due date.
+ let box = this.findDayBoxForDate(targetDate);
+ if (box) {
+ boxes.push(box);
+ }
+ }
+
+ while (targetDate.compare(finishDate) == -1) {
+ let box = this.findDayBoxForDate(targetDate);
+
+ // This might not exist, if the event spans the view start or end
+ if (box) {
+ boxes.push(box);
+ }
+ targetDate.day += 1;
+ }
+
+ return boxes;
+ ]]></body>
+ </method>
+
+ <method name="doAddItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let boxes = this.findDayBoxesForItem(aItem);
+
+ if (!boxes.length) {
+ return;
+ }
+
+ for (let box of boxes) {
+ box.addItem(aItem);
+ }
+ ]]></body>
+ </method>
+
+ <method name="doDeleteItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let boxes = this.findDayBoxesForItem(aItem);
+
+ if (!boxes.length) {
+ return;
+ }
+
+ function isNotItem(a) {
+ return (a.hashId != aItem.hashId);
+ }
+ let oldLength = this.mSelectedItems.length;
+ this.mSelectedItems = this.mSelectedItems.filter(isNotItem);
+
+ for (let box of boxes) {
+ box.deleteItem(aItem);
+ }
+
+ // If a deleted event was selected, we need to announce that the
+ // selection changed.
+ if (oldLength != this.mSelectedItems.length) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+ ]]></body>
+ </method>
+
+ <method name="deleteItemsFromCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ if (!this.mDateBoxes) {
+ return;
+ }
+ for (let box of this.mDateBoxes) {
+ for (let id in box.mItemHash) {
+ let node = box.mItemHash[id];
+ let item = node.item;
+ if (item.calendar.id == aCalendar.id) {
+ box.deleteItem(item);
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="flashAlarm">
+ <parameter name="aAlarmItem"/>
+ <parameter name="aStop"/>
+ <body><![CDATA[
+ let showIndicator = Preferences.get("calendar.alarms.indicator.show", true);
+ let totaltime = Preferences.get("calendar.alarms.indicator.totaltime", 3600);
+
+ if (!aStop && (!showIndicator || totaltime < 1)) {
+ // No need to animate if the indicator should not be shown.
+ return;
+ }
+
+ // Make sure the flashing attribute is set or reset on all visible
+ // boxes.
+ let boxes = this.findDayBoxesForItem(aAlarmItem);
+ for (let box of boxes) {
+ for (let id in box.mItemHash) {
+ let itemData = box.mItemHash[id];
+ if (itemData.item.hasSameIds(aAlarmItem)) {
+ if (aStop) {
+ itemData.removeAttribute("flashing");
+ } else {
+ itemData.setAttribute("flashing", "true");
+ }
+ }
+ }
+ }
+
+ if (aStop) {
+ // We are done flashing, prevent newly created event boxes from flashing.
+ delete this.mFlashingEvents[aAlarmItem.hashId];
+ } else {
+ // Set up a timer to stop the flashing after the total time.
+ this.mFlashingEvents[aAlarmItem.hashId] = aAlarmItem;
+ setTimeout(() => this.flashAlarm(aAlarmItem, true), totaltime);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="wheel"><![CDATA[
+ const pixelThreshold = 150;
+ let scrollEnabled = Preferences.get("calendar.view.mousescroll", true);
+ if (!event.ctrlKey && !event.shiftKey &&
+ !event.altKey && !event.metaKey && scrollEnabled) {
+ // In the month view, the only thing that can be scrolled
+ // is the month the user is in. calendar-base-view takes care
+ // of the shift key, so only move the view when no modifier
+ // is pressed.
+ let deltaView = 0;
+ if (event.deltaMode == event.DOM_DELTA_LINE) {
+ if (event.deltaY != 0) {
+ deltaView = event.deltaY < 0 ? -1 : 1;
+ }
+ } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
+ this.mPixelScrollDelta += event.deltaY;
+ if (this.mPixelScrollDelta > pixelThreshold) {
+ deltaView = 1;
+ this.mPixelScrollDelta = 0;
+ } else if (this.mPixelScrollDelta < -pixelThreshold) {
+ deltaView = -1;
+ this.mPixelScrollDelta = 0;
+ }
+ }
+
+ if (deltaView != 0) {
+ this.moveView(deltaView);
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-multiday-view.xml b/calendar/base/content/calendar-multiday-view.xml
new file mode 100644
index 000000000..1163a2e8d
--- /dev/null
+++ b/calendar/base/content/calendar-multiday-view.xml
@@ -0,0 +1,3886 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Note that this file depends on helper functions in calUtils.js-->
+
+<!DOCTYPE bindings SYSTEM "chrome://global/locale/global.dtd" >
+
+<bindings id="calendar-multiday-view-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!--
+ - This is the time bar that displays time divisions to the side
+ - or top of a multiday view.
+ -->
+ <binding id="calendar-time-bar">
+ <content>
+ <xul:stack anonid="timebarboxstack" style="display: block; position: relative" xbl:inherits="orient,width,height" flex="1">
+ <xul:box anonid="topbox" xbl:inherits="orient,width,height" flex="1"/>
+ <xul:box anonid="timeIndicatorBoxTimeBar" class="timeIndicator-timeBar" xbl:inherits="orient" hidden="true"/>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <field name="mPixPerMin">0.6</field>
+ <field name="mStartMin">0</field>
+ <field name="mEndMin">24 * 60</field>
+ <field name="mDayStartHour">0</field>
+ <field name="mDayEndHour">24</field>
+
+ <constructor>
+ this.relayout();
+ </constructor>
+
+ <method name="setDayStartEndHours">
+ <parameter name="aDayStartHour"/>
+ <parameter name="aDayEndHour"/>
+ <body><![CDATA[
+ if (aDayStartHour * 60 < this.mStartMin ||
+ aDayStartHour > aDayEndHour ||
+ aDayEndHour * 60 > this.mEndMin) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.mDayStartHour != aDayStartHour ||
+ this.mDayEndHour != aDayEndHour) {
+ this.mDayEndHour = aDayEndHour;
+ this.mDayStartHour = aDayStartHour;
+
+ let topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
+ if (topbox.childNodes.length) {
+ // This only needs to be done if the initial relayout has
+ // already happened, otherwise it will be done then.
+ for (let hour = this.mStartMin / 60; hour < this.mEndMin / 60; hour++) {
+ if (hour < this.mDayStartHour || hour >= this.mDayEndHour) {
+ topbox.childNodes[hour].setAttribute("off-time", "true");
+ } else {
+ topbox.childNodes[hour].removeAttribute("off-time");
+ }
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ let needsrelayout = false;
+ if (aAttr == "orient") {
+ if (this.getAttribute("orient") != aVal) {
+ needsrelayout = true;
+ }
+ }
+
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+
+ if (needsrelayout) {
+ this.relayout();
+ }
+
+ return ret;
+ ]]></body>
+ </method>
+
+ <property name="pixelsPerMinute"
+ onget="return this.mPixPerMin"
+ onset="if (this.mPixPerMin != val) { this.mPixPerMin = val; this.relayout(); } return val;"/>
+
+ <method name="relayout">
+ <body><![CDATA[
+ let topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
+ let orient = topbox.getAttribute("orient");
+
+ function makeTimeBox(timestr, size) {
+ let box = createXULElement("box");
+ box.setAttribute("orient", orient);
+
+ if (orient == "horizontal") {
+ box.setAttribute("width", size);
+ } else {
+ box.setAttribute("height", size);
+ }
+
+ let label = createXULElement("label");
+ label.setAttribute("class", "calendar-time-bar-label");
+ label.setAttribute("value", timestr);
+ label.setAttribute("align", "center");
+
+ box.appendChild(label);
+
+ return box;
+ }
+
+ while (topbox.hasChildNodes()) {
+ topbox.lastChild.remove();
+ }
+
+ let formatter = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Components.interfaces.nsIScriptableDateFormat);
+ let timeString;
+ let theMin = this.mStartMin;
+ let theHour = Math.floor(theMin / 60);
+ let durLeft = this.mEndMin - this.mStartMin;
+
+ while (durLeft > 0) {
+ let dur;
+ if (this.mEndMin - theMin < 60) {
+ dur = this.mEndMin - theMin;
+ } else {
+ dur = theMin % 60;
+ }
+ theMin += dur;
+ if (dur == 0) {
+ dur = 60;
+ }
+
+ // calculate duration pixel as the difference between
+ // start pixel and end pixel to avoid rounding errors.
+ let startPix = Math.round(theMin * this.mPixPerMin);
+ let endPix = Math.round((theMin + dur) * this.mPixPerMin);
+ let durPix = endPix - startPix;
+ let box;
+ if (dur == 60) {
+ timeString = formatter.FormatTime("",
+ Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
+ theHour, 0, 0);
+ box = makeTimeBox(timeString, durPix);
+ } else {
+ box = makeTimeBox("", durPix);
+ }
+
+ // Set up workweek hours
+ if (theHour < this.mDayStartHour || theHour >= this.mDayEndHour) {
+ box.setAttribute("off-time", "true");
+ }
+
+ box.setAttribute("class", "calendar-time-bar-box-" + (theHour % 2 == 0 ? "even" : "odd"));
+ topbox.appendChild(box);
+
+ durLeft -= dur;
+ theMin += dur;
+ theHour++;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!--
+ - A simple gripbar that is displayed at the start and end of an
+ - event box. Needs to handle being dragged and resizing the
+ - event, thus changing its start/end time.
+ -->
+ <binding id="calendar-event-gripbar">
+ <content>
+ <xul:box anonid="thebox"
+ flex="1"
+ pack="center"
+ xbl:inherits="align=whichside">
+ <xul:image xbl:inherits="class"/>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <property name="parentorient">
+ <getter><![CDATA[
+ return this.getAttribute("parentorient");
+ ]]></getter>
+ <setter><![CDATA[
+ let thebox = document.getAnonymousElementByAttribute(this, "anonid", "thebox");
+ this.setAttribute("parentorient", val);
+ thebox.setAttribute("orient", getOtherOrientation(val));
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- private -->
+ <constructor><![CDATA[
+ this.parentorient = this.getAttribute("parentorient");
+ ]]></constructor>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown" button="0"><![CDATA[
+ // store the attribute 'whichside' in the event object
+ // but *don't* call stopPropagation(). as soon as the
+ // enclosing event box will receive the event it will
+ // make use of this information in order to invoke the
+ // appropriate action.
+ event.whichside = this.getAttribute("whichside");
+ ]]></handler>
+ <handler event="click" button="0"><![CDATA[
+ event.stopPropagation();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <!--
+ - A column for displaying event boxes in. One column per
+ - day; it manages the layout of the events given via add/deleteEvent.
+ -->
+ <binding id="calendar-event-column">
+ <content>
+ <xul:stack anonid="boxstack" flex="1" class="multiday-column-box-stack" style="min-width: 1px; min-height: 1px">
+ <xul:box anonid="bgbox" flex="1" class="multiday-column-bg-box" style="min-width: 1px; min-height: 1px"/>
+ <xul:box anonid="topbox" class="multiday-column-top-box" flex="1" style="min-width: 1px; min-height: 1px"
+ xbl:inherits="context" equalsize="always" mousethrough="always"/>
+ <xul:box anonid="timeIndicatorBox" xbl:inherits="orient" class="timeIndicator" mousethrough="always" hidden="true"/>
+ <xul:box anonid="fgbox" flex="1" class="fgdragcontainer" style="min-width: 1px; min-height: 1px; overflow:hidden;">
+ <xul:box anonid="fgdragspacer" style="display: inherit; overflow: hidden;">
+ <xul:spacer flex="1"/>
+ <xul:label anonid="fgdragbox-startlabel" class="fgdragbox-label"/>
+ </xul:box>
+ <xul:box anonid="fgdragbox" class="fgdragbox" />
+ <xul:label anonid="fgdragbox-endlabel" class="fgdragbox-label"/>
+ </xul:box>
+ </xul:stack>
+ <xul:calendar-event-box anonid="config-box" hidden="true" xbl:inherits="orient"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.mEventInfos = [];
+ this.mTimezone = UTC();
+ this.mSelectedItemIds = {};
+ ]]></constructor>
+
+ <!-- fields -->
+ <field name="mPixPerMin">0.6</field>
+ <field name="mStartMin">0</field>
+ <field name="mEndMin">24 * 60</field>
+ <field name="mDayStartMin">8 * 60</field>
+ <field name="mDayEndMin">17 * 60</field>
+ <!--an array of objects that contain information about the events that are to be
+ displayed. The contained fields are:
+ event: The event that is to be displayed in a 'calendar-event-box'
+ layoutStart: The 'start'-datetime object of the event in the timezone of the view
+ layoutEnd: The 'end'-datetime object of the event in the timezone of the view.
+ The 'layoutEnd' may be different from the real 'end' time of the
+ event because it considers a certain minimum duration of the event
+ that is basically dependent of the font-size of the event-box label -->
+ <field name="mEventInfos">[]</field>
+ <field name="mEventMap">null</field>
+ <field name="mCalendarView">null</field>
+ <field name="mDate">null</field>
+ <field name="mTimezone">null</field>
+ <field name="mDragState">null</field>
+ <field name="mLayoutBatchCount">0</field>
+ <!-- Since we'll often be getting many events in rapid succession, this
+ timer helps ensure that we don't re-compute the event map too many
+ times in a short interval, and therefore improves performance.-->
+ <field name="mEventMapTimeout">null</field>
+ <!-- Sometimes we need to add resize handlers for columns with special
+ widths. When we relayout, we need to cancel those handlers -->
+ <field name="mHandlersToRemove">[]</field>
+
+ <!-- Set this true so that we know in our onAddItem listener to start
+ - modifying an event when it comes back to us as created
+ -->
+ <field name="mCreatedNewEvent">false</field>
+ <field name="mEventToEdit">null</field>
+ <field name="mSelectedItemIds">null</field>
+
+ <!-- properties -->
+ <property name="pixelsPerMinute">
+ <getter><![CDATA[
+ return this.mPixPerMin;
+ ]]></getter>
+ <setter><![CDATA[
+ if (val <= 0.0) {
+ val = 0.01;
+ }
+ if (val != this.mPixPerMin) {
+ this.mPixPerMin = val;
+ this.relayout();
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="mSelected">false</field>
+ <property name="selected">
+ <getter><![CDATA[
+ return this.mSelected;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mSelected = val;
+ if (this.bgbox && this.bgbox.hasChildNodes()) {
+ let child = this.bgbox.firstChild;
+ while (child) {
+ if (val) {
+ child.setAttribute("selected", "true");
+ } else {
+ child.removeAttribute("selected");
+ }
+ child = child.nextSibling;
+ }
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="date">
+ <getter><![CDATA[
+ return this.mDate;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mDate = val;
+
+ if (!compareObjects(val.timezone, this.mTimezone)) {
+ this.mTimezone = val.timezone;
+ if (!this.mLayoutBatchCount) {
+ this.recalculateStartEndMinutes();
+ }
+ }
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="calendarView"
+ onget="return this.mCalendarView;"
+ onset="return (this.mCalendarView = val);" />
+
+ <property name="topbox" readonly="true">
+ <getter><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "anonid", "topbox");
+ ]]></getter>
+ </property>
+
+ <property name="bgbox" readonly="true">
+ <getter><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "anonid", "bgbox");
+ ]]></getter>
+ </property>
+
+ <field name="mFgboxes">null</field>
+ <field name="mMinDuration">null</field>
+ <property name="fgboxes" readonly="true">
+ <getter><![CDATA[
+ if (this.mFgboxes == null) {
+ this.mFgboxes = {
+ box: document.getAnonymousElementByAttribute(this, "anonid", "fgbox"),
+ dragbox: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox"),
+ dragspacer: document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer"),
+ startlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-startlabel"),
+ endlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-endlabel")
+ };
+ }
+ return this.mFgboxes;
+ ]]></getter>
+ </property>
+
+ <property name="timeIndicatorBox"
+ readonly="true">
+ <getter><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "anonid", "timeIndicatorBox");
+ ]]></getter>
+ </property>
+
+ <property name="events" readonly="true" onget="return this.methods"/>
+
+ <field name="mDayOff">false</field>
+ <property name="dayOff">
+ <getter><![CDATA[
+ return this.mDayOff;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mDayOff = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- mEventInfos -->
+ <field name="mSelectedChunks">[]</field>
+
+ <method name="selectOccurrence">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ if (aOccurrence) {
+ this.mSelectedItemIds[aOccurrence.hashId] = true;
+ let chunk = this.findChunkForOccurrence(aOccurrence);
+ if (!chunk) {
+ return;
+ }
+ chunk.selected = true;
+ this.mSelectedChunks.push(chunk);
+ }
+ ]]></body>
+ </method>
+
+ <method name="unselectOccurrence">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ if (aOccurrence) {
+ delete this.mSelectedItemIds[aOccurrence.hashId];
+ let chunk = this.findChunkForOccurrence(aOccurrence);
+ if (!chunk) {
+ return;
+ }
+ chunk.selected = false;
+ let index = this.mSelectedChunks.indexOf(chunk);
+ this.mSelectedChunks.splice(index, 1);
+ }
+ ]]></body>
+ </method>
+
+ <method name="findChunkForOccurrence">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ for (let chunk of this.mEventBoxes) {
+ if (chunk.occurrence.hashId == aOccurrence.hashId) {
+ return chunk;
+ }
+ }
+
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="startLayoutBatchChange">
+ <body><![CDATA[
+ this.mLayoutBatchCount++;
+ ]]></body>
+ </method>
+ <method name="endLayoutBatchChange">
+ <body><![CDATA[
+ this.mLayoutBatchCount--;
+ if (this.mLayoutBatchCount == 0) {
+ this.relayout();
+ }
+ ]]></body>
+ </method>
+
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+
+ if (aAttr == "orient" && this.getAttribute("orient") != aVal) {
+ this.relayout();
+ }
+
+ return ret;
+ ]]></body>
+ </method>
+
+ <method name="internalDeleteEvent">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ let itemIndex = -1;
+ let occ;
+ for (let i in this.mEventInfos) {
+ occ = this.mEventInfos[i].event;
+ if (occ.hashId == aOccurrence.hashId) {
+ itemIndex = i;
+ break;
+ }
+ }
+
+ if (itemIndex == -1) {
+ return false;
+ } else {
+ delete this.mSelectedItemIds[occ.hashId];
+ this.mSelectedChunks = this.mSelectedChunks.filter((item) => {
+ return !item.occurrence || (item.occurrence.hashId != aOccurrence.hashId);
+ });
+ this.mEventInfos.splice(itemIndex, 1);
+ return true;
+ }
+ ]]></body>
+ </method>
+
+ <method name="recalculateStartEndMinutes">
+ <body><![CDATA[
+ for (let chunk of this.mEventInfos) {
+ let mins = this.getStartEndMinutesForOccurrence(chunk.event);
+ chunk.startMinute = mins.start;
+ chunk.endMinute = mins.end;
+ }
+
+ this.relayout();
+ ]]></body>
+ </method>
+
+ <!-- This function returns the start and end minutes of the occurrence
+ part in the day of this column, moreover, the real start and end
+ minutes of the whole occurrence (which could span multiple days)
+ relative to the time 0:00 of the day in this column -->
+ <method name="getStartEndMinutesForOccurrence">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ let stdate = aOccurrence.startDate || aOccurrence.entryDate || aOccurrence.dueDate;
+ let enddate = aOccurrence.endDate || aOccurrence.dueDate || aOccurrence.entryDate;
+
+ if (!compareObjects(stdate.timezone, this.mTimezone)) {
+ stdate = stdate.getInTimezone(this.mTimezone);
+ }
+
+ if (!compareObjects(enddate.timezone, this.mTimezone)) {
+ enddate = enddate.getInTimezone(this.mTimezone);
+ }
+
+ let startHour = stdate.hour;
+ let startMinute = stdate.minute;
+ let endHour = enddate.hour;
+ let endMinute = enddate.minute;
+
+ // Handle cases where an event begins or ends on a day other than this
+ if (stdate.compare(this.mDate) == -1) {
+ startHour = 0;
+ startMinute = 0;
+ }
+ if (enddate.compare(this.mDate) == 1) {
+ endHour = 24;
+ endMinute = 0;
+ }
+
+ // For occurrences that span multiple days, we figure out the real
+ // occurrence start and end minutes relative to the date of this
+ // column and time 0:00
+ let durend = enddate.subtractDate(this.mDate);
+ let durstart = stdate.subtractDate(this.mDate);
+ // 'durend' is always positive, instead 'durstart' might be negative
+ // if the event starts one or more days before the date of this column
+ let realStart_ = (durstart.days * 24 + durstart.hours) * 60 + durstart.minutes;
+ realStart_ = durstart.isNegative ? -1 * realStart_ : realStart_;
+ let realEnd_ = (durend.days * 24 + durend.hours) * 60 + durend.minutes;
+
+ return {
+ start: startHour * 60 + startMinute,
+ end: endHour * 60 + endMinute,
+ realStart: realStart_,
+ realEnd: realEnd_
+ };
+ ]]></body>
+ </method>
+
+ <method name="createChunk">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ let mins = this.getStartEndMinutesForOccurrence(aOccurrence);
+
+ let chunk = {
+ startMinute: mins.start,
+ endMinute: mins.end,
+ event: aOccurrence
+ };
+ return chunk;
+ ]]></body>
+ </method>
+
+ <method name="addEvent">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ this.internalDeleteEvent(aOccurrence);
+
+ let chunk = this.createChunk(aOccurrence);
+ this.mEventInfos.push(chunk);
+ if (this.mEventMapTimeout) {
+ clearTimeout(this.mEventMapTimeout);
+ }
+
+ if (this.mCreatedNewEvent) {
+ this.mEventToEdit = aOccurrence;
+ }
+
+ this.mEventMapTimeout = setTimeout(() => this.relayout(), 5);
+ ]]></body>
+ </method>
+
+ <method name="deleteEvent">
+ <parameter name="aOccurrence"/>
+ <body><![CDATA[
+ if (this.internalDeleteEvent(aOccurrence)) {
+ this.relayout();
+ }
+ ]]></body>
+ </method>
+
+ <method name="clear">
+ <body><![CDATA[
+ while (this.bgbox && this.bgbox.hasChildNodes()) {
+ this.bgbox.lastChild.remove();
+ }
+ while (this.topbox && this.topbox.hasChildNodes()) {
+ this.topbox.lastChild.remove();
+ }
+ for (let handler of this.mHandlersToRemove) {
+ this.calendarView.viewBroadcaster.removeEventListener(this.calendarView.getAttribute("type") + "viewresized", handler, true);
+ }
+ this.mHandlersToRemove = [];
+ this.mSelectedChunks = [];
+ ]]></body>
+ </method>
+
+ <method name="relayout">
+ <body><![CDATA[
+ if (this.mLayoutBatchCount > 0) {
+ return;
+ }
+ this.clear();
+
+ let orient = this.getAttribute("orient");
+ this.bgbox.setAttribute("orient", orient);
+
+ // bgbox is used mainly for drawing the grid. at some point it may
+ // also be used for all-day events.
+ let otherorient = getOtherOrientation(orient);
+ let configBox = document.getAnonymousElementByAttribute(this, "anonid", "config-box");
+ configBox.removeAttribute("hidden");
+ let minSize = configBox.getOptimalMinSize();
+ configBox.setAttribute("hidden", "true");
+ this.mMinDuration = Components.classes["@mozilla.org/calendar/duration;1"]
+ .createInstance(Components.interfaces.calIDuration);
+ this.mMinDuration.minutes = Math.trunc(minSize / this.mPixPerMin);
+
+ let theMin = this.mStartMin;
+ while (theMin < this.mEndMin) {
+ let dur = theMin % 60;
+ theMin += dur;
+ if (dur == 0) {
+ dur = 60;
+ }
+
+ let box = createXULElement("spacer");
+ // we key off this in a CSS selector
+ box.setAttribute("orient", orient);
+ box.setAttribute("class", "calendar-event-column-linebox");
+
+ if (this.mSelected) {
+ box.setAttribute("selected", "true");
+ }
+ if (this.mDayOff) {
+ box.setAttribute("weekend", "true");
+ }
+ if (theMin < this.mDayStartMin || theMin >= this.mDayEndMin) {
+ box.setAttribute("off-time", "true");
+ }
+
+ // Carry forth the day relation
+ box.setAttribute("relation", this.getAttribute("relation"));
+
+ // calculate duration pixel as the difference between
+ // start pixel and end pixel to avoid rounding errors.
+ let startPix = Math.round(theMin * this.mPixPerMin);
+ let endPix = Math.round((theMin + dur) * this.mPixPerMin);
+ let durPix = endPix - startPix;
+ if (orient == "vertical") {
+ box.setAttribute("height", durPix);
+ } else {
+ box.setAttribute("width", durPix);
+ }
+
+ box.setAttribute("style", "min-width: 1px; min-height: 1px;");
+
+ this.bgbox.appendChild(box);
+ theMin += 60;
+ }
+
+ // fgbox is used for dragging events
+ this.fgboxes.box.setAttribute("orient", orient);
+ document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer").setAttribute("orient", orient);
+
+ // this one is set to otherorient, since it will contain
+ // child boxes set to "orient" (one for each set of
+ // overlapping event areas)
+ this.topbox.setAttribute("orient", otherorient);
+
+ this.mEventMap = this.computeEventMap();
+ this.mEventBoxes = [];
+
+ if (!this.mEventMap.length) {
+ return;
+ }
+
+ // First of all we create a xul:stack which
+ // will hold all events for this event column.
+ // The stack will be grouped below .../calendar-event-column/stack/topbox.
+ let stack = createXULElement("stack");
+ stack.setAttribute("flex", "1");
+ this.topbox.appendChild(stack);
+
+ let boxToEdit;
+ let columnCount = 1;
+ let spanTotal = 0;
+
+ for (let layer of this.mEventMap) {
+ // The event-map (this.mEventMap) contains an array of layers.
+ // For each layer we create a box below the stack just created above.
+ // So each different layer lives in a box that's contained in the stack.
+ let xulColumn = createXULElement("box");
+ xulColumn.setAttribute("orient", otherorient);
+ xulColumn.setAttribute("flex", "1");
+ xulColumn.setAttribute("style", "min-width: 1px; min-height: 1px;");
+ stack.appendChild(xulColumn);
+
+ let numBlocksInserted = 0;
+
+ // column count determined by layer with no special span columns
+ if (layer.every(e => !e.specialSpan)) {
+ columnCount = layer.length;
+ }
+ spanTotal = 0;
+
+ // Each layer contains a list of the columns that
+ // need to be created for a span.
+ for (let column of layer) {
+ let innerColumn = createXULElement("box");
+ innerColumn.setAttribute("orient", orient);
+
+ let colFlex = column.specialSpan ? columnCount * column.specialSpan : 1;
+ innerColumn.setAttribute("flex", colFlex);
+ spanTotal += colFlex;
+
+ innerColumn.style.minWidth = "1px";
+ innerColumn.style.minHeight = "1px";
+ innerColumn.style.width = colFlex + "px";
+ innerColumn.style.height = colFlex + "px";
+
+ xulColumn.appendChild(innerColumn);
+ let duration;
+ for (let chunk of column) {
+ duration = chunk.duration;
+ if (!duration) {
+ continue;
+ }
+
+ if (chunk.event) {
+ let chunkBox = createXULElement("calendar-event-box");
+ let durMinutes = duration.inSeconds / 60;
+ let size = Math.max(durMinutes * this.mPixPerMin, minSize);
+ if (orient == "vertical") {
+ chunkBox.setAttribute("height", size);
+ } else {
+ chunkBox.setAttribute("width", size);
+ }
+ chunkBox.setAttribute("context",
+ this.getAttribute("item-context") ||
+ this.getAttribute("context"));
+ chunkBox.setAttribute("orient", orient);
+
+ // Set the gripBars visibility in the chunk. Keep it
+ // hidden for tasks with only entry date OR due date.
+ if ((chunk.event.entryDate || !chunk.event.dueDate) &&
+ (!chunk.event.entryDate || chunk.event.dueDate)) {
+ let startGripVisible = (chunk.event.startDate || chunk.event.entryDate)
+ .compare(chunk.startDate) == 0;
+ let endGripVisible = (chunk.event.endDate || chunk.event.dueDate)
+ .compare(chunk.endDate) <= 0;
+ if (startGripVisible && endGripVisible) {
+ chunkBox.setAttribute("gripBars", "both");
+ } else if (endGripVisible) {
+ chunkBox.setAttribute("gripBars", "end");
+ } else if (startGripVisible) {
+ chunkBox.setAttribute("gripBars", "start");
+ }
+ }
+
+ innerColumn.appendChild(chunkBox);
+ chunkBox.calendarView = this.calendarView;
+ chunkBox.occurrence = chunk.event;
+ chunkBox.parentColumn = this;
+ if (chunk.event.hashId in this.mSelectedItemIds) {
+ chunkBox.selected = true;
+ this.mSelectedChunks.push(chunkBox);
+ }
+
+ this.mEventBoxes.push(chunkBox);
+
+ if (this.mEventToEdit &&
+ chunkBox.occurrence.hashId == this.mEventToEdit.hashId) {
+ boxToEdit = chunkBox;
+ }
+ } else {
+ let chunkBox = createXULElement("spacer");
+ chunkBox.setAttribute("context", this.getAttribute("context"));
+ chunkBox.setAttribute("style", "min-width: 1px; min-height: 1px;");
+ chunkBox.setAttribute("orient", orient);
+ chunkBox.setAttribute("class", "calendar-empty-space-box");
+ innerColumn.appendChild(chunkBox);
+
+ let durMinutes = duration.inSeconds / 60;
+ if (orient == "vertical") {
+ chunkBox.setAttribute("height", durMinutes * this.mPixPerMin);
+ } else {
+ chunkBox.setAttribute("width", durMinutes * this.mPixPerMin);
+ }
+ }
+ }
+
+ numBlocksInserted++;
+ }
+
+ // add last empty column if necessary
+ if (spanTotal < columnCount) {
+ let lastColumn = createXULElement("box");
+ lastColumn.setAttribute("orient", orient);
+ lastColumn.setAttribute("flex", columnCount - spanTotal);
+ lastColumn.style.minWidth = "1px";
+ lastColumn.style.minHeight = "1px";
+ lastColumn.style.width = (columnCount - spanTotal) + "px";
+ lastColumn.style.height = (columnCount - spanTotal) + "px";
+
+ xulColumn.appendChild(lastColumn);
+ }
+
+ if (boxToEdit) {
+ this.mCreatedNewEvent = false;
+ this.mEventToEdit = null;
+ boxToEdit.startEditing();
+ }
+
+ if (numBlocksInserted == 0) {
+ // if we didn't insert any blocks, then
+ // forget about this column
+ xulColumn.remove();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="computeEventMap">
+ <body><![CDATA[
+ /* We're going to create a series of 'blobs'. A blob is a series of
+ * events that create a continuous block of busy time. In other
+ * words, a blob ends when there is some time such that no events
+ * occupy that time.
+ *
+ * Each blob will be an array of objects with the following properties:
+ * item: the event/task
+ * startCol: the starting column to display the event in (0-indexed)
+ * colSpan: the number of columns the item spans
+ *
+ * An item with no conflicts will have startCol: 0 and colSpan: 1.
+ */
+ let blobs = [];
+ let currentBlob = [];
+ function sortByStart(aEventInfo, bEventInfo) {
+ // If you pass in tasks without both entry and due dates, I will
+ // kill you
+ let startComparison = aEventInfo.layoutStart.compare(bEventInfo.layoutStart);
+ if (startComparison == 0) {
+ // If the items start at the same time, return the longer one
+ // first
+ return bEventInfo.layoutEnd.compare(aEventInfo.layoutEnd);
+ } else {
+ return startComparison;
+ }
+ }
+ this.mEventInfos.forEach((aEventInfo) => {
+ let item = aEventInfo.event.clone();
+ let start = item.startDate || item.entryDate || item.dueDate;
+ start = start.getInTimezone(this.mTimezone);
+ aEventInfo.layoutStart = start;
+ let end = item.endDate || item.dueDate || item.entryDate;
+ end = end.getInTimezone(this.mTimezone);
+ let secEnd = start.clone();
+ secEnd.addDuration(this.mMinDuration);
+ if (secEnd.nativeTime > end.nativeTime) {
+ aEventInfo.layoutEnd = secEnd;
+ } else {
+ aEventInfo.layoutEnd = end;
+ }
+ return aEventInfo;
+ });
+ this.mEventInfos.sort(sortByStart);
+
+ // The end time of the last ending event in the entire blob
+ let latestItemEnd;
+
+ // This array keeps track of the last (latest ending) item in each of
+ // the columns of the current blob. We could reconstruct this data at
+ // any time by looking at the items in the blob, but that would hurt
+ // perf.
+ let colEndArray = [];
+
+ /* Go through a 3 step process to try and place each item.
+ * Step 1: Look for an existing column with room for the item.
+ * Step 2: Look for a previously placed item that can be shrunk in
+ * width to make room for the item.
+ * Step 3: Give up and create a new column for the item.
+ *
+ * (The steps are explained in more detail as we come to them)
+ */
+ for (let i in this.mEventInfos) {
+ let curItemInfo = {
+ event: this.mEventInfos[i].event,
+ layoutStart: this.mEventInfos[i].layoutStart,
+ layoutEnd: this.mEventInfos[i].layoutEnd
+ };
+ if (!latestItemEnd) {
+ latestItemEnd = curItemInfo.layoutEnd;
+ }
+ if (currentBlob.length && latestItemEnd &&
+ curItemInfo.layoutStart.compare(latestItemEnd) != -1) {
+ // We're done with this current blob because item starts
+ // after the last event in the current blob ended.
+ blobs.push({ blob: currentBlob, totalCols: colEndArray.length });
+
+ // Reset our variables
+ currentBlob = [];
+ colEndArray = [];
+ }
+
+ // Place the item in its correct place in the blob
+ let placedItem = false;
+
+ // Step 1
+ // Look for a possible column in the blob that has been left open. This
+ // would happen if we already have multiple columns but some of
+ // the cols have events before latestItemEnd. For instance
+ // | | |
+ // |______| |
+ // |ev1 |______|
+ // | |ev2 |
+ // |______| |
+ // | | |
+ // |OPEN! | |<--Our item's start time might be here
+ // | |______|
+ // | | |
+ //
+ // Remember that any time we're starting a new blob, colEndArray
+ // will be empty, but that's ok.
+ for (let j = 0; j < colEndArray.length; ++j) {
+ let colEnd = colEndArray[j].layoutEnd;
+ if (colEnd.compare(curItemInfo.layoutStart) != 1) {
+ // Yay, we can jump into this column
+ colEndArray[j] = curItemInfo;
+
+ // Check and see if there are any adjacent columns we can
+ // jump into as well.
+ let lastCol = Number(j) + 1;
+ while (lastCol < colEndArray.length) {
+ let nextColEnd = colEndArray[lastCol].layoutEnd;
+ // If the next column's item ends after we start, we
+ // can't expand any further
+ if (nextColEnd.compare(curItemInfo.layoutStart) == 1) {
+ break;
+ }
+ colEndArray[lastCol] = curItemInfo;
+ lastCol++;
+ }
+ // Now construct the info we need to push into the blob
+ currentBlob.push({
+ itemInfo: curItemInfo,
+ startCol: j,
+ colSpan: lastCol - j
+ });
+
+ // Update latestItemEnd
+ if (latestItemEnd &&
+ curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
+ latestItemEnd = curItemInfo.layoutEnd;
+ }
+ placedItem = true;
+ break; // Stop iterating through colEndArray
+ }
+ }
+
+ if (placedItem) {
+ // Go get the next item
+ continue;
+ }
+
+ // Step 2
+ // OK, all columns (if there are any) overlap us. Look if the
+ // last item in any of the last items in those columns is taking
+ // up 2 or more cols. If so, shrink it and stick the item in the
+ // created space. For instance
+ // |______|______|______|
+ // |ev1 |ev3 |ev4 |
+ // | | | |
+ // | |______| |
+ // | | |______|
+ // | |_____________|
+ // | |ev2 |
+ // |______| |<--If our item's start time is
+ // | |_____________| here, we can shrink ev2 and jump
+ // | | | | in column #3
+ //
+ for (let j = 1; j < colEndArray.length; ++j) {
+ if (colEndArray[j].event.hashId == colEndArray[j - 1].event.hashId) {
+ // Good we found a item that spanned multiple columns.
+ // Find it in the blob so we can modify its properties
+ for (let blobKey in currentBlob) {
+ if (currentBlob[blobKey].itemInfo.event.hashId == colEndArray[j].event.hashId) {
+ // Take all but the first spot that the item spanned
+ let spanOfShrunkItem = currentBlob[blobKey].colSpan;
+ currentBlob.push({
+ itemInfo: curItemInfo,
+ startCol: Number(currentBlob[blobKey].startCol) + 1,
+ colSpan: spanOfShrunkItem - 1
+ });
+
+ // Update colEndArray
+ for (let k = j; k < j + spanOfShrunkItem - 1; k++) {
+ colEndArray[k] = curItemInfo;
+ }
+
+ // Modify the data on the old item
+ currentBlob[blobKey] = {
+ itemInfo: currentBlob[blobKey].itemInfo,
+ startCol: currentBlob[blobKey].startCol,
+ colSpan: 1
+ };
+ // Update latestItemEnd
+ if (latestItemEnd &&
+ curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
+ latestItemEnd = curItemInfo.layoutEnd;
+ }
+ break; // Stop iterating through currentBlob
+ }
+ }
+ placedItem = true;
+ break; // Stop iterating through colEndArray
+ }
+ }
+
+ if (placedItem) {
+ // Go get the next item
+ continue;
+ }
+
+ // Step 3
+ // Guess what? We still haven't placed the item. We need to
+ // create a new column for it.
+
+ // All the items in the last column, except for the one* that
+ // conflicts with the item we're trying to place, need to have
+ // their span extended by 1, since we're adding the new column
+ //
+ // * Note that there can only be one, because we sorted our
+ // events by start time, so this event must start later than
+ // the start of any possible conflicts.
+ let lastColNum = colEndArray.length;
+ for (let blobKey in currentBlob) {
+ let blobKeyEnd = currentBlob[blobKey].itemInfo.layoutEnd;
+ if (currentBlob[blobKey].startCol + currentBlob[blobKey].colSpan == lastColNum &&
+ blobKeyEnd.compare(curItemInfo.layoutStart) != 1) {
+ currentBlob[blobKey] = {
+ itemInfo: currentBlob[blobKey].itemInfo,
+ startCol: currentBlob[blobKey].startCol,
+ colSpan: currentBlob[blobKey].colSpan + 1
+ };
+ }
+ }
+ currentBlob.push({
+ itemInfo: curItemInfo,
+ startCol: colEndArray.length,
+ colSpan: 1
+ });
+ colEndArray.push(curItemInfo);
+
+ // Update latestItemEnd
+ if (latestItemEnd && curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
+ latestItemEnd = curItemInfo.layoutEnd;
+ }
+ // Go get the next item
+ }
+ // Add the last blob
+ blobs.push({
+ blob: currentBlob,
+ totalCols: colEndArray.length
+ });
+ return this.setupBoxStructure(blobs);
+ ]]></body>
+ </method>
+
+ <method name="setupBoxStructure">
+ <parameter name="aBlobs"/>
+ <body><![CDATA[
+ // This is actually going to end up being a 3-d array
+ // 1st dimension: "layers", sets of columns of events that all
+ // should have equal width*
+ // 2nd dimension: "columns", individual columns of non-conflicting
+ // items
+ // 3rd dimension: "chunks", individual items or placeholders for
+ // the blank time in between them
+ //
+ // * Note that 'equal width' isn't strictly correct. If we're
+ // oriented differently, it will be height (and we'll have rows
+ // not columns). What's more, in the 'specialSpan' case, the
+ // columns won't actually have the same size, but will only all
+ // be multiples of a common size. See the note in the relayout
+ // function for more info on this (fairly rare) case.
+ let layers = [];
+
+ // When we start a new blob, move to a new set of layers
+ let layerOffset = 0;
+ for (let glob of aBlobs) {
+ let layerArray = [];
+ let layerCounter = 1;
+
+ for (let data of glob.blob) {
+ // from the item at hand we need to figure out on which
+ // layer and on which column it should go.
+ let layerIndex;
+ let specialSpan = null;
+
+ // each blob receives its own layer, that's the first part of the story. within
+ // a given blob we need to distribute the items on different layers depending on
+ // the number of columns each item spans. if each item just spans a single column
+ // the blob will cover *one* layer. if the blob contains items that span more than
+ // a single column, this blob will cover more than one layer. the algorithm places
+ // the items on the first layer in the case an item covers a single column. new layers
+ // are introduced based on the start column and number of spanning columns of an item.
+ if (data.colSpan == 1) {
+ layerIndex = 0;
+ } else {
+ let index = glob.totalCols * data.colSpan + data.startCol;
+ layerIndex = layerArray[index];
+ if (!layerIndex) {
+ layerIndex = layerCounter++;
+ layerArray[index] = layerIndex;
+ }
+ let offset = (glob.totalCols - data.colSpan) % glob.totalCols;
+ if (offset != 0) {
+ specialSpan = data.colSpan / glob.totalCols;
+ }
+ }
+ layerIndex += layerOffset;
+
+ // Make sure there's room to insert stuff
+ while (layerIndex >= layers.length) {
+ layers.push([]);
+ }
+
+ while (data.startCol >= layers[layerIndex].length) {
+ layers[layerIndex].push([]);
+ if (specialSpan) {
+ layers[layerIndex][layers[layerIndex].length - 1].specialSpan = 1 / glob.totalCols;
+ }
+ }
+
+ // we now retrieve the column from 'layerIndex' and 'startCol'.
+ let col = layers[layerIndex][data.startCol];
+ if (specialSpan) {
+ col.specialSpan = specialSpan;
+ }
+
+ // take into account that items can span several days.
+ // that's why i'm clipping the start- and end-time to the
+ // timespan of this column.
+ let start = data.itemInfo.layoutStart;
+ let end = data.itemInfo.layoutEnd;
+ if (start.year != this.date.year ||
+ start.month != this.date.month ||
+ start.day != this.date.day) {
+ start = start.clone();
+ start.resetTo(this.date.year,
+ this.date.month,
+ this.date.day,
+ 0, this.mStartMin, 0,
+ start.timezone);
+ }
+ if (end.year != this.date.year ||
+ end.month != this.date.month ||
+ end.day != this.date.day) {
+ end = end.clone();
+ end.resetTo(this.date.year,
+ this.date.month,
+ this.date.day,
+ 0, this.mEndMin, 0,
+ end.timezone);
+ }
+ let prevEnd;
+ if (col.length > 0) {
+ // Fill in time gaps with a placeholder
+ prevEnd = col[col.length - 1].endDate.clone();
+ } else {
+ // First event in the column, add a placeholder for the
+ // blank time from this.mStartMin to the event's start
+ prevEnd = start.clone();
+ prevEnd.hour = 0;
+ prevEnd.minute = this.mStartMin;
+ }
+ prevEnd.timezone = floating();
+ // the reason why we need to calculate time durations
+ // based on floating timezones is that we need avoid
+ // dst gaps in this case. converting the date/times to
+ // floating conveys this idea in a natural way. note that
+ // we explicitly don't use getInTimezone() as it would
+ // be slightly more expensive in terms of performance.
+ let floatstart = start.clone();
+ floatstart.timezone = floating();
+ let dur = floatstart.subtractDate(prevEnd);
+ if (dur.inSeconds) {
+ col.push({ duration: dur });
+ }
+ let floatend = end.clone();
+ floatend.timezone = floating();
+ col.push({
+ event: data.itemInfo.event,
+ endDate: end,
+ startDate: start,
+ duration: floatend.subtractDate(floatstart)
+ });
+ }
+ layerOffset = layers.length;
+ }
+ return layers;
+ ]]></body>
+ </method>
+
+ <method name="getShadowElements">
+ <parameter name="aStart"/>
+ <parameter name="aEnd"/>
+ <body><![CDATA[
+ // aStart and aEnd are start and end minutes of the occurrence
+ // from time 0:00 of the dragging column
+ let shadows = 1;
+ let offset = 0;
+ let startMin;
+ if (aStart < 0) {
+ shadows += Math.ceil(Math.abs(aStart) / this.mEndMin);
+ offset = shadows - 1;
+ let reminder = Math.abs(aStart) % this.mEndMin;
+ startMin = this.mEndMin - (reminder ? reminder : this.mEndMin);
+ } else {
+ startMin = aStart;
+ }
+ shadows += Math.floor(aEnd / this.mEndMin);
+
+ // return values needed to build the shadows while dragging
+ return {
+ shadows: shadows, // number of shadows
+ offset: offset, // Offset first<->selected shadows
+ startMin: startMin, // First shadow start minute
+ endMin: aEnd % this.mEndMin // Last shadow end minute
+ };
+ ]]></body>
+ </method>
+
+ <method name="firstLastShadowColumns">
+ <parameter name="aOffset"/>
+ <parameter name="aShadows"/>
+ <body><![CDATA[
+ let firstCol = this; // eslint-disable-line consistent-this
+ let lastCol = this; // eslint-disable-line consistent-this
+ let firstIndex = aOffset == null ? this.mDragState.offset : aOffset;
+ let lastIndex = firstIndex;
+ while (firstCol.previousSibling && firstIndex > 0) {
+ firstCol = firstCol.previousSibling;
+ firstIndex--;
+ }
+ let lastShadow = aShadows == null ? this.mDragState.shadows : aShadows;
+ while (lastCol.nextSibling && lastIndex < lastShadow - 1) {
+ lastCol = lastCol.nextSibling;
+ lastIndex++;
+ }
+
+ // returns first and last column with shadows that are visible in the
+ // week and the positions of these (visible) columns in the set of
+ // columns shadows of the occurrence
+ return {
+ firstCol: firstCol,
+ firstIndex: firstIndex,
+ lastCol: lastCol,
+ lastIndex: lastIndex
+ };
+ ]]></body>
+ </method>
+
+ <method name="updateShadowsBoxes">
+ <parameter name="aStart"/>
+ <parameter name="aEnd"/>
+ <parameter name="aCurrentOffset"/>
+ <parameter name="aCurrentShadows"/>
+ <parameter name="aSizeattr"/>
+ <body><![CDATA[
+ let lateralColumns = this.firstLastShadowColumns(aCurrentOffset, aCurrentShadows);
+ let firstCol = lateralColumns.firstCol;
+ let firstIndex = lateralColumns.firstIndex;
+ let lastCol = lateralColumns.lastCol;
+ let lastIndex = lateralColumns.lastIndex;
+
+ // remove the first/last shadow when start/end time goes in the
+ // next/previous day. This happens when current offset is different
+ // from offset stored in mDragState
+ if (aCurrentOffset != null) {
+ if (this.mDragState.offset > aCurrentOffset && firstCol.previousSibling) {
+ firstCol.previousSibling.fgboxes.dragbox.removeAttribute("dragging");
+ firstCol.previousSibling.fgboxes.box.removeAttribute("dragging");
+ }
+ let currentOffsetEndSide = aCurrentShadows - 1 - aCurrentOffset;
+ if ((this.mDragState.shadows - 1 - this.mDragState.offset) > currentOffsetEndSide &&
+ lastCol.nextSibling) {
+ lastCol.nextSibling.fgboxes.dragbox.removeAttribute("dragging");
+ lastCol.nextSibling.fgboxes.box.removeAttribute("dragging");
+ }
+ }
+
+ // set shadow boxes size for every part of the occurrence
+ let firstShadowSize = (aCurrentShadows == 1 ? aEnd : this.mEndMin) - aStart;
+ let column = firstCol;
+ for (let i = firstIndex; column && i <= lastIndex; i++) {
+ column.fgboxes.box.setAttribute("dragging", "true");
+ column.fgboxes.dragbox.setAttribute("dragging", "true");
+ if (i == 0) {
+ // first shadow
+ column.fgboxes.dragspacer.setAttribute(aSizeattr, aStart * column.mPixPerMin);
+ column.fgboxes.dragbox.setAttribute(aSizeattr, firstShadowSize * column.mPixPerMin);
+ } else if (i == (aCurrentShadows - 1)) {
+ // last shadow
+ column.fgboxes.dragspacer.setAttribute(aSizeattr, 0);
+ column.fgboxes.dragbox.setAttribute(aSizeattr, aEnd * column.mPixPerMin);
+ } else {
+ // an intermediate shadow (full day)
+ column.fgboxes.dragspacer.setAttribute(aSizeattr, 0);
+ column.fgboxes.dragbox.setAttribute(aSizeattr, this.mEndMin * column.mPixPerMin);
+ }
+ column = column.nextSibling;
+ }
+ ]]></body>
+ </method>
+
+ <method name="onEventSweepKeypress">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let col = document.calendarEventColumnDragging;
+ if (col && event.keyCode == event.DOM_VK_ESCAPE) {
+ window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
+ window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
+ window.removeEventListener("keypress", col.onEventSweepKeypress, false);
+
+ let lateralColumns = col.firstLastShadowColumns();
+ let column = lateralColumns.firstCol;
+ let index = lateralColumns.firstIndex;
+ while (column && index < col.mDragState.shadows) {
+ column.fgboxes.dragbox.removeAttribute("dragging");
+ column.fgboxes.box.removeAttribute("dragging");
+ column = column.nextSibling;
+ index++;
+ }
+
+ col.mDragState = null;
+ document.calendarEventColumnDragging = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="clearMagicScroll">
+ <body><![CDATA[
+ if (this.mMagicScrollTimer) {
+ clearTimeout(this.mMagicScrollTimer);
+ this.mMagicScrollTimer = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="setupMagicScroll">
+ <parameter name="event"/>
+ <body><![CDATA[
+ this.clearMagicScroll();
+
+ // If we are at the bottom or top of the view (or left/right when
+ // rotated), calculate the difference and start accelerating the
+ // scrollbar.
+ let diffStart, diffEnd;
+ let orient = event.target.getAttribute("orient");
+ let scrollbox = document.getAnonymousElementByAttribute(
+ event.target, "anonid", "scrollbox");
+ if (orient == "vertical") {
+ diffStart = event.clientY - scrollbox.boxObject.y;
+ diffEnd = scrollbox.boxObject.y + scrollbox.boxObject.height - event.clientY;
+ } else {
+ diffStart = event.clientX - scrollbox.boxObject.x;
+ diffEnd = scrollbox.boxObject.x + scrollbox.boxObject.width - event.clientX;
+ }
+
+ const SCROLLZONE = 55; // Size (pixels) of the top/bottom view where the scroll starts.
+ const MAXTIMEOUT = 250; // Max and min time interval (ms) between
+ const MINTIMEOUT = 30; // two consecutive scrolls.
+ const SCROLLBYHOUR = 0.33; // Part of hour to move for each scroll.
+ let insideScrollZone = 0;
+ let pxPerHr = event.target.mPixPerMin * 60;
+ let scrollBy = Math.floor(pxPerHr * SCROLLBYHOUR);
+ if (diffStart < SCROLLZONE) {
+ insideScrollZone = SCROLLZONE - diffStart;
+ scrollBy *= -1;
+ } else if (diffEnd < SCROLLZONE) {
+ insideScrollZone = SCROLLZONE - diffEnd;
+ }
+
+ if (insideScrollZone) {
+ let sbo = scrollbox.boxObject;
+ let timeout = MAXTIMEOUT - insideScrollZone * (MAXTIMEOUT - MINTIMEOUT) / SCROLLZONE;
+ this.mMagicScrollTimer = setTimeout(() => {
+ sbo.scrollBy(orient == "horizontal" && scrollBy,
+ orient == "vertical" && scrollBy);
+ this.onEventSweepMouseMove(event);
+ }, timeout);
+ }
+ ]]></body>
+ </method>
+
+ <!--
+ - Event sweep handlers
+ -->
+ <method name="onEventSweepMouseMove">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let col = document.calendarEventColumnDragging;
+ if (!col) {
+ return;
+ }
+
+ col.setupMagicScroll(event);
+
+ let dragState = col.mDragState;
+
+ let lateralColumns = col.firstLastShadowColumns();
+ let firstCol = lateralColumns.firstCol;
+ let firstIndex = lateralColumns.firstIndex;
+
+ // If we leave the view, then stop our internal sweeping and start a
+ // real drag session. Someday we need to fix the sweep to soely be a
+ // drag session, no sweeping.
+ if (event.clientX < (event.target.boxObject.x) ||
+ event.clientX > (event.target.boxObject.x + event.target.boxObject.width) ||
+ event.clientY < (event.target.boxObject.y) ||
+ event.clientY > (event.target.boxObject.y + event.target.boxObject.height)) {
+ // Remove the drag state
+ for (let column = firstCol, i = firstIndex;
+ column && i < col.mDragState.shadows;
+ column = column.nextSibling, i++) {
+ column.fgboxes.dragbox.removeAttribute("dragging");
+ column.fgboxes.box.removeAttribute("dragging");
+ }
+
+ window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
+ window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
+ window.removeEventListener("keypress", col.onEventSweepKeypress, false);
+ document.calendarEventColumnDragging = null;
+ col.mDragState = null;
+
+ // the multiday view currently exhibits a less than optimal strategy
+ // in terms of item selection. items don't get automatically selected
+ // when clicked and dragged, as to differentiate inline editing from
+ // the act of selecting an event. but the application internal drop
+ // targets will ask for selected items in order to pull the data from
+ // the packets. that's why we need to make sure at least the currently
+ // dragged event is contained in the set of selected items.
+ let selectedItems = this.getSelectedItems({});
+ if (!selectedItems.some(aItem => aItem.hashId == item.hashId)) {
+ col.calendarView.setSelectedItems(1,
+ [event.ctrlKey ? item.parentItem : item]);
+ }
+ invokeEventDragSession(dragState.dragOccurrence, col);
+ return;
+ }
+
+ col.fgboxes.box.setAttribute("dragging", "true");
+ col.fgboxes.dragbox.setAttribute("dragging", "true");
+ let minutesInDay = col.mEndMin - col.mStartMin;
+
+ // check if we need to jump a column
+ let jumpedColumns;
+ let newcol = col.calendarView.findColumnForClientPoint(event.screenX, event.screenY);
+ if (newcol && newcol != col) {
+ // Find how many columns we are jumping by subtracting the dates.
+ let dur = newcol.mDate.subtractDate(col.mDate);
+ jumpedColumns = dur.days;
+ jumpedColumns *= dur.isNegative ? -1 : 1;
+ if (dragState.dragType == "modify-start") {
+ // prevent dragging the start date after the end date in a new column
+ if ((dragState.limitEndMin - minutesInDay * jumpedColumns) < 0) {
+ return;
+ }
+ dragState.limitEndMin -= minutesInDay * jumpedColumns;
+ } else if (dragState.dragType == "modify-end") {
+ // prevent dragging the end date before the start date in a new column
+ if ((dragState.limitStartMin - minutesInDay * jumpedColumns) > minutesInDay) {
+ return;
+ }
+ dragState.limitStartMin -= minutesInDay * jumpedColumns;
+ } else if (dragState.dragType == "new") {
+ dragState.limitEndMin -= minutesInDay * jumpedColumns;
+ dragState.limitStartMin -= minutesInDay * jumpedColumns;
+ dragState.jumpedColumns += jumpedColumns;
+ }
+ // kill our drag state
+ for (let column = firstCol, i = firstIndex;
+ column && i < col.mDragState.shadows;
+ column = column.nextSibling, i++) {
+ column.fgboxes.dragbox.removeAttribute("dragging");
+ column.fgboxes.box.removeAttribute("dragging");
+ }
+
+ // jump ship
+ newcol.acceptInProgressSweep(dragState);
+
+ // restart event handling
+ col.onEventSweepMouseMove(event);
+
+ return;
+ }
+
+ let mousePos;
+ let sizeattr;
+ if (col.getAttribute("orient") == "vertical") {
+ mousePos = event.screenY - col.parentNode.boxObject.screenY;
+ sizeattr = "height";
+ } else {
+ mousePos = event.screenX - col.parentNode.boxObject.screenX;
+ sizeattr = "width";
+ }
+ // don't let mouse position go outside the window edges
+ let pos = Math.max(0, mousePos) - dragState.mouseOffset;
+
+ // snap interval: 15 minutes or 1 minute if modifier key is pressed
+ let snapIntMin = (event.shiftKey &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ !event.metaKey) ? 1 : 15;
+ let interval = col.mPixPerMin * snapIntMin;
+ let curmin = Math.floor(pos / interval) * snapIntMin;
+ let deltamin = curmin - dragState.origMin;
+
+ let shadowElements;
+ if (dragState.dragType == "new") {
+ // Extend deltamin in a linear way over the columns
+ deltamin += minutesInDay * dragState.jumpedColumns;
+ if (deltamin < 0) {
+ // create a new event modifying the start. End time is fixed
+ shadowElements = {
+ shadows: 1 - dragState.jumpedColumns,
+ offset: 0,
+ startMin: curmin,
+ endMin: dragState.origMin
+ };
+ } else {
+ // create a new event modifying the end. Start time is fixed
+ shadowElements = {
+ shadows: dragState.jumpedColumns + 1,
+ offset: dragState.jumpedColumns,
+ startMin: dragState.origMin,
+ endMin: curmin
+ };
+ }
+ dragState.startMin = shadowElements.startMin;
+ dragState.endMin = shadowElements.endMin;
+ } else if (dragState.dragType == "move") {
+ // if we're moving, we modify startMin and endMin of the shadow.
+ shadowElements = col.getShadowElements(dragState.origMinStart + deltamin,
+ dragState.origMinEnd + deltamin);
+ dragState.startMin = shadowElements.startMin;
+ dragState.endMin = shadowElements.endMin;
+ // Keep track of the last start position because it will help to
+ // build the event at the end of the drag session.
+ dragState.lastStart = dragState.origMinStart + deltamin;
+ } else if (dragState.dragType == "modify-start") {
+ // if we're modifying the start, the end time is fixed.
+ shadowElements = col.getShadowElements(dragState.origMin + deltamin, dragState.limitEndMin);
+ dragState.startMin = shadowElements.startMin;
+ dragState.endMin = shadowElements.endMin;
+
+ // but we need to not go past the end; if we hit
+ // the end, then we'll clamp to the previous snap interval minute
+ if (dragState.startMin >= dragState.limitEndMin) {
+ dragState.startMin = Math.ceil((dragState.limitEndMin - snapIntMin) / snapIntMin) * snapIntMin;
+ }
+ } else if (dragState.dragType == "modify-end") {
+ // if we're modifying the end, the start time is fixed.
+ shadowElements = col.getShadowElements(dragState.limitStartMin, dragState.origMin + deltamin);
+ dragState.startMin = shadowElements.startMin;
+ dragState.endMin = shadowElements.endMin;
+
+ // but we need to not go past the start; if we hit
+ // the start, then we'll clamp to the next snap interval minute
+ if (dragState.endMin <= dragState.limitStartMin) {
+ dragState.endMin = Math.floor((dragState.limitStartMin + snapIntMin) / snapIntMin) * snapIntMin;
+ }
+ }
+ let currentOffset = shadowElements.offset;
+ let currentShadows = shadowElements.shadows;
+
+ // now we can update the shadow boxes position and size
+ col.updateShadowsBoxes(dragState.startMin, dragState.endMin,
+ currentOffset, currentShadows,
+ sizeattr);
+
+ // update the labels
+ lateralColumns = col.firstLastShadowColumns(currentOffset, currentShadows);
+ col.updateDragLabels(lateralColumns.firstCol, lateralColumns.lastCol);
+
+ col.mDragState.offset = currentOffset;
+ col.mDragState.shadows = currentShadows;
+ ]]></body>
+ </method>
+
+ <method name="onEventSweepMouseUp">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let col = document.calendarEventColumnDragging;
+ if (!col) {
+ return;
+ }
+
+ let dragState = col.mDragState;
+
+ let lateralColumns = col.firstLastShadowColumns();
+ let column = lateralColumns.firstCol;
+ let index = lateralColumns.firstIndex;
+ while (column && index < dragState.shadows) {
+ column.fgboxes.dragbox.removeAttribute("dragging");
+ column.fgboxes.box.removeAttribute("dragging");
+ column = column.nextSibling;
+ index++;
+ }
+
+ col.clearMagicScroll();
+
+ window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
+ window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
+ window.removeEventListener("keypress", col.onEventSweepKeypress, false);
+
+ document.calendarEventColumnDragging = null;
+
+ // if the user didn't sweep out at least a few pixels, ignore
+ // unless we're in a different column
+ if (dragState.origColumn == col) {
+ let ignore = false;
+ let orient = col.getAttribute("orient");
+ let position = orient == "vertical" ? event.screenY : event.screenX;
+ if (Math.abs(position - dragState.origLoc) < 3) {
+ ignore = true;
+ }
+
+ if (ignore) {
+ col.mDragState = null;
+ return;
+ }
+ }
+
+ let newStart;
+ let newEnd;
+ let startTZ;
+ let endTZ;
+ let dragDay = col.mDate;
+ if (dragState.dragType != "new") {
+ let oldStart = dragState.dragOccurrence.startDate ||
+ dragState.dragOccurrence.entryDate ||
+ dragState.dragOccurrence.dueDate;
+ let oldEnd = dragState.dragOccurrence.endDate ||
+ dragState.dragOccurrence.dueDate ||
+ dragState.dragOccurrence.entryDate;
+ newStart = oldStart.clone();
+ newEnd = oldEnd.clone();
+
+ // Our views are pegged to the default timezone. If the event
+ // isn't also in the timezone, we're going to need to do some
+ // tweaking. We could just do this for every event but
+ // getInTimezone is slow, so it's much better to only do this
+ // when the timezones actually differ from the view's.
+ if (col.mTimezone != newStart.timezone ||
+ col.mTimezone != newEnd.timezone) {
+ startTZ = newStart.timezone;
+ endTZ = newEnd.timezone;
+ newStart = newStart.getInTimezone(col.calendarView.mTimezone);
+ newEnd = newEnd.getInTimezone(col.calendarView.mTimezone);
+ }
+ }
+
+ if (dragState.dragType == "modify-start") {
+ newStart.resetTo(dragDay.year, dragDay.month, dragDay.day,
+ 0, dragState.startMin + col.mStartMin, 0,
+ newStart.timezone);
+ } else if (dragState.dragType == "modify-end") {
+ newEnd.resetTo(dragDay.year, dragDay.month, dragDay.day,
+ 0, dragState.endMin + col.mStartMin, 0,
+ newEnd.timezone);
+ } else if (dragState.dragType == "new") {
+ let startDay = dragState.origColumn.mDate;
+ let draggedForward = (dragDay.compare(startDay) > 0);
+ newStart = draggedForward ? startDay.clone() : dragDay.clone();
+ newEnd = draggedForward ? dragDay.clone() : startDay.clone();
+ newStart.isDate = false;
+ newEnd.isDate = false;
+ newStart.resetTo(newStart.year, newStart.month, newStart.day,
+ 0, dragState.startMin + col.mStartMin, 0,
+ newStart.timezone);
+ newEnd.resetTo(newEnd.year, newEnd.month, newEnd.day,
+ 0, dragState.endMin + col.mStartMin, 0,
+ newEnd.timezone);
+
+ // Edit the event title on the first of the new event's occurrences
+ if (draggedForward) {
+ dragState.origColumn.mCreatedNewEvent = true;
+ } else {
+ col.mCreatedNewEvent = true;
+ }
+ } else if (dragState.dragType == "move") {
+ // Figure out the new date-times of the event by adding the duration
+ // of the total movement (days and minutes) to the old dates.
+ let duration = dragDay.subtractDate(dragState.origColumn.mDate);
+ let minutes = dragState.lastStart - dragState.realStart;
+
+ // Since both boxDate and beginMove are dates (note datetimes),
+ // subtractDate will only give us a non-zero number of hours on
+ // DST changes. While strictly speaking, subtractDate's behavior
+ // is correct, we need to move the event a discrete number of
+ // days here. There is no need for normalization here, since
+ // addDuration does the job for us. Also note, the duration used
+ // here is only used to move over multiple days. Moving on the
+ // same day uses the minutes from the dragState.
+ if (duration.hours == 23) {
+ // entering DST
+ duration.hours++;
+ } else if (duration.hours == 1) {
+ // leaving DST
+ duration.hours--;
+ }
+
+ if (duration.isNegative) {
+ // Adding negative minutes to a negative duration makes the
+ // duration more positive, but we want more negative, and
+ // vice versa.
+ minutes *= -1;
+ }
+ duration.minutes = minutes;
+ duration.normalize();
+
+ newStart.addDuration(duration);
+ newEnd.addDuration(duration);
+ }
+
+ // If we tweaked tzs, put times back in their original ones
+ if (startTZ) {
+ newStart = newStart.getInTimezone(startTZ);
+ }
+ if (endTZ) {
+ newEnd = newEnd.getInTimezone(endTZ);
+ }
+
+ if (dragState.dragType == "new") {
+ // We won't pass a calendar, since the display calendar is the
+ // composite anyway. createNewEvent() will use the selected
+ // calendar.
+ // TODO We might want to get rid of the extra displayCalendar
+ // member.
+ col.calendarView.controller.createNewEvent(null,
+ newStart,
+ newEnd);
+ } else if (dragState.dragType == "move" ||
+ dragState.dragType == "modify-start" ||
+ dragState.dragType == "modify-end") {
+ col.calendarView.controller.modifyOccurrence(dragState.dragOccurrence,
+ newStart, newEnd);
+ }
+ document.calendarEventColumnDragging = null;
+ col.mDragState = null;
+ ]]></body>
+ </method>
+
+ <!-- This is called by an event box when a grippy on either side is dragged,
+ - or when the middle is pressed to drag the event to move it. We create
+ - the same type of view that we use to sweep out a new event, but we
+ - initialize it based on the event's values and what type of dragging
+ - we're doing. In addition, we constrain things like not being able to
+ - drag the end before the start and vice versa.
+ -->
+ <method name="startSweepingToModifyEvent">
+ <parameter name="aEventBox"/>
+ <parameter name="aOccurrence"/>
+ <!-- "start", "end", "middle" -->
+ <parameter name="aGrabbedElement"/>
+ <!-- mouse screenX/screenY from the event -->
+ <parameter name="aMouseX"/>
+ <parameter name="aMouseY"/>
+ <parameter name="aSnapInt"/>
+ <body><![CDATA[
+ if (!isCalendarWritable(aOccurrence.calendar) ||
+ !userCanModifyItem(aOccurrence) ||
+ (aOccurrence.calendar instanceof Components.interfaces.calISchedulingSupport && aOccurrence.calendar.isInvitation(aOccurrence)) ||
+ aOccurrence.calendar.getProperty("capabilities.events.supported") === false) {
+ return;
+ }
+
+ this.mDragState = {
+ origColumn: this,
+ dragOccurrence: aOccurrence,
+ mouseOffset: 0,
+ offset: null,
+ shadows: null,
+ limitStartMin: null,
+ lastStart: 0,
+ jumpedColumns: 0
+ };
+
+ // snap interval: 15 minutes or 1 minute if modifier key is pressed
+ let snapIntMin = aSnapInt || 15;
+ let sizeattr;
+ if (this.getAttribute("orient") == "vertical") {
+ this.mDragState.origLoc = aMouseY;
+ sizeattr = "height";
+ } else {
+ this.mDragState.origLoc = aMouseX;
+ sizeattr = "width";
+ }
+
+ let mins = this.getStartEndMinutesForOccurrence(aOccurrence);
+
+ // these are only used to compute durations or to compute UI
+ // sizes, so offset by this.mStartMin for sanity here (at the
+ // expense of possible insanity later)
+ mins.start -= this.mStartMin;
+ mins.end -= this.mStartMin;
+
+ if (aGrabbedElement == "start") {
+ this.mDragState.dragType = "modify-start";
+ // we have to use "realEnd" as fixed end value
+ this.mDragState.limitEndMin = mins.realEnd;
+
+ // snap start
+ this.mDragState.origMin = Math.floor(mins.start / snapIntMin) * snapIntMin;
+
+ // show the shadows and drag labels when clicking on gripbars
+ let shadowElements = this.getShadowElements(this.mDragState.origMin,
+ this.mDragState.limitEndMin);
+ this.mDragState.startMin = shadowElements.startMin;
+ this.mDragState.endMin = shadowElements.endMin;
+ this.mDragState.shadows = shadowElements.shadows;
+ this.mDragState.offset = shadowElements.offset;
+ this.updateShadowsBoxes(this.mDragState.origMin, this.mDragState.endMin,
+ 0, this.mDragState.shadows,
+ sizeattr);
+
+ // update drag labels
+ let lastCol = this.firstLastShadowColumns().lastCol;
+ this.updateDragLabels(this, lastCol);
+ } else if (aGrabbedElement == "end") {
+ this.mDragState.dragType = "modify-end";
+ // we have to use "realStart" as fixed end value
+ this.mDragState.limitStartMin = mins.realStart;
+
+ // snap end
+ this.mDragState.origMin = Math.floor(mins.end / snapIntMin) * snapIntMin;
+
+ // show the shadows and drag labels when clicking on gripbars
+ let shadowElements = this.getShadowElements(this.mDragState.limitStartMin,
+ this.mDragState.origMin);
+ this.mDragState.startMin = shadowElements.startMin;
+ this.mDragState.endMin = shadowElements.endMin;
+ this.mDragState.shadows = shadowElements.shadows;
+ this.mDragState.offset = shadowElements.offset;
+ this.updateShadowsBoxes(this.mDragState.startMin, this.mDragState.endMin,
+ shadowElements.offset, this.mDragState.shadows,
+ sizeattr);
+
+ // update drag labels
+ let firstCol = this.firstLastShadowColumns().firstCol;
+ this.updateDragLabels(firstCol, this);
+ } else if (aGrabbedElement == "middle") {
+ this.mDragState.dragType = "move";
+ // in a move, origMin will be the start minute of the element where
+ // the drag occurs. Along with mouseOffset, it allows to track the
+ // shadow position. origMinStart and origMinEnd allow to figure out
+ // the real shadow size.
+ // We snap to the start and add the real duration to find the end
+ let limitDurationMin = mins.realEnd - mins.realStart;
+ this.mDragState.origMin = Math.floor(mins.start / snapIntMin) * snapIntMin;
+ this.mDragState.origMinStart = Math.floor(mins.realStart / snapIntMin) * snapIntMin;
+ this.mDragState.origMinEnd = this.mDragState.origMinStart + limitDurationMin;
+ // Keep also track of the real Start, it will be used at the end
+ // of the drag session to calculate the new start and end datetimes.
+ this.mDragState.realStart = mins.realStart;
+
+ let shadowElements = this.getShadowElements(this.mDragState.origMinStart,
+ this.mDragState.origMinEnd);
+ this.mDragState.shadows = shadowElements.shadows;
+ this.mDragState.offset = shadowElements.offset;
+ // we need to set a mouse offset, since we're not dragging from
+ // one end of the element
+ if (aEventBox) {
+ if (this.getAttribute("orient") == "vertical") {
+ this.mDragState.mouseOffset = aMouseY - aEventBox.boxObject.screenY;
+ } else {
+ this.mDragState.mouseOffset = aMouseX - aEventBox.boxObject.screenX;
+ }
+ }
+ } else {
+ // Invalid grabbed element.
+ }
+
+ document.calendarEventColumnDragging = this;
+
+ window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
+ window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
+ window.addEventListener("keypress", this.onEventSweepKeypress, false);
+ ]]></body>
+ </method>
+
+ <!-- called by sibling columns to tell us to take over the sweeping
+ - of an event.
+ -->
+ <method name="acceptInProgressSweep">
+ <parameter name="aDragState"/>
+ <body><![CDATA[
+ this.mDragState = aDragState;
+ document.calendarEventColumnDragging = this;
+
+ this.fgboxes.box.setAttribute("dragging", "true");
+ this.fgboxes.dragbox.setAttribute("dragging", "true");
+
+ // the same event handlers are still valid,
+ // because they use document.calendarEventColumnDragging.
+ // So we really don't have anything to do here.
+ ]]></body>
+ </method>
+
+ <method name="updateDragLabels">
+ <parameter name="aFirstColumn"/>
+ <parameter name="aLastColumn"/>
+ <body><![CDATA[
+ if (!this.mDragState) {
+ return;
+ }
+
+ let firstColumn = aFirstColumn || this;
+ let lastColumn = aLastColumn || this;
+ let realstartmin = this.mDragState.startMin + this.mStartMin;
+ let realendmin = this.mDragState.endMin + this.mStartMin;
+ let starthr = Math.floor(realstartmin / 60);
+ let startmin = realstartmin % 60;
+
+ let endhr = Math.floor(realendmin / 60);
+ let endmin = realendmin % 60;
+
+ let formatter = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Components.interfaces.nsIScriptableDateFormat);
+ let startstr = formatter.FormatTime("",
+ Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
+ starthr, startmin, 0);
+ let endstr = formatter.FormatTime("",
+ Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
+ endhr, endmin, 0);
+
+ // Tasks without Entry or Due date have a string as first label
+ // instead of the time.
+ if (cal.isToDo(this.mDragState.dragOccurrence)) {
+ if (!this.mDragState.dragOccurrence.dueDate) {
+ startstr = calGetString("calendar", "dragLabelTasksWithOnlyEntryDate");
+ } else if (!this.mDragState.dragOccurrence.entryDate) {
+ startstr = calGetString("calendar", "dragLabelTasksWithOnlyDueDate");
+ }
+ }
+ firstColumn.fgboxes.startlabel.setAttribute("value", startstr);
+ lastColumn.fgboxes.endlabel.setAttribute("value", endstr);
+
+ ]]></body>
+ </method>
+
+ <method name="setDayStartEndMinutes">
+ <parameter name="aDayStartMin"/>
+ <parameter name="aDayEndMin"/>
+ <body><![CDATA[
+ if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
+ aDayEndMin > this.mEndMin) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.mDayStartMin != aDayStartMin || this.mDayEndMin != aDayEndMin) {
+ this.mDayStartMin = aDayStartMin;
+ this.mDayEndMin = aDayEndMin;
+ }
+ ]]></body>
+ </method>
+
+ <method name="getClickedDateTime">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let newStart = this.date.clone();
+ newStart.isDate = false;
+ newStart.hour = 0;
+
+ const ROUND_INTERVAL = 15;
+
+ let interval = this.mPixPerMin * ROUND_INTERVAL;
+ let pos;
+ if (this.getAttribute("orient") == "vertical") {
+ pos = event.screenY - this.parentNode.boxObject.screenY;
+ } else {
+ pos = event.screenX - this.parentNode.boxObject.screenX;
+ }
+ newStart.minute = (Math.round(pos / interval) * ROUND_INTERVAL) + this.mStartMin;
+ event.stopPropagation();
+ return newStart;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="dblclick" button="0"><![CDATA[
+ if (this.calendarView.controller) {
+ let newStart = getClickedDateTime(event);
+ this.calendarView.controller.createNewEvent(null, newStart, null);
+ }
+ ]]></handler>
+
+ <handler event="click" button="0"><![CDATA[
+ if (!(event.ctrlKey || event.metaKey)) {
+ this.calendarView.setSelectedItems(0, []);
+ this.focus();
+ }
+ ]]></handler>
+
+ <handler event="click" button="2"><![CDATA[
+ let newStart = getClickedDateTime(event);
+ this.calendarView.selectedDateTime = newStart;
+ ]]></handler>
+
+ <!-- mouse down handler, in empty event column regions. Starts sweeping out a new
+ - event.
+ -->
+ <handler event="mousedown"><![CDATA[
+ // select this column
+ this.calendarView.selectedDay = this.mDate;
+
+ // If the selected calendar is readOnly, we don't want any sweeping.
+ let calendar = getSelectedCalendar();
+ if (!isCalendarWritable(calendar) ||
+ calendar.getProperty("capabilities.events.supported") === false) {
+ return;
+ }
+
+ // Only start sweeping out an event if the left button was clicked
+ if (event.button != 0) {
+ return;
+ }
+
+ this.mDragState = {
+ origColumn: this,
+ dragType: "new",
+ mouseOffset: 0,
+ offset: null,
+ shadows: null,
+ limitStartMin: null,
+ limitEndMin: null,
+ jumpedColumns: 0
+ };
+
+ // snap interval: 15 minutes or 1 minute if modifier key is pressed
+ let snapIntMin = (event.shiftKey &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ !event.metaKey) ? 1 : 15;
+ let interval = this.mPixPerMin * snapIntMin;
+
+ if (this.getAttribute("orient") == "vertical") {
+ this.mDragState.origLoc = event.screenY;
+ this.mDragState.origMin = Math.floor((event.screenY - this.parentNode.boxObject.screenY) / interval) * snapIntMin;
+ this.mDragState.limitEndMin = this.mDragState.origMin;
+ this.mDragState.limitStartMin = this.mDragState.origMin;
+ this.fgboxes.dragspacer.setAttribute("height", this.mDragState.origMin * this.mPixPerMin);
+ } else {
+ this.mDragState.origLoc = event.screenX;
+ this.mDragState.origMin = Math.floor((event.screenX - this.parentNode.boxObject.screenX) / interval) * snapIntMin;
+ this.fgboxes.dragspacer.setAttribute("width", this.mDragState.origMin * this.mPixPerMin);
+ }
+
+ document.calendarEventColumnDragging = this;
+
+ window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
+ window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
+ window.addEventListener("keypress", this.onEventSweepKeypress, false);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-header-container" extends="chrome://calendar/content/widgets/calendar-widgets.xml#dragndropContainer">
+ <content xbl:inherits="selected" flex="1" class="calendar-event-column-header">
+ <children/>
+ </content>
+
+ <implementation>
+ <field name="mItemBoxes">null</field>
+ <constructor><![CDATA[
+ this.mItemBoxes = [];
+ ]]></constructor>
+
+ <property name="date">
+ <getter><![CDATA[
+ return this.mDate;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mDate = val;
+ return val;
+ ]]></setter>
+ </property>
+ <method name="findBoxForItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ for (let item of this.mItemBoxes) {
+ if (aItem && item.occurrence.hasSameIds(aItem)) {
+ // We can return directly, since there will only be one box per
+ // item in the header.
+ return item;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="addEvent">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ // prevent same items being added
+ if (this.mItemBoxes.some(itemBox => itemBox.occurrence.hashId == aItem.hashId)) {
+ return;
+ }
+
+ let itemBox = createXULElement("calendar-editable-item");
+ this.appendChild(itemBox);
+ itemBox.calendarView = this.calendarView;
+ itemBox.occurrence = aItem;
+ let ctxt = this.calendarView.getAttribute("item-context") ||
+ this.calendarView.getAttribute("context");
+ itemBox.setAttribute("context", ctxt);
+
+ if (aItem.hashId in this.calendarView.mFlashingEvents) {
+ itemBox.setAttribute("flashing", "true");
+ }
+
+ this.mItemBoxes.push(itemBox);
+ itemBox.parentBox = this;
+ ]]></body>
+ </method>
+
+ <method name="deleteEvent">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ for (let i in this.mItemBoxes) {
+ if (this.mItemBoxes[i].occurrence.hashId == aItem.hashId) {
+ this.mItemBoxes[i].remove();
+ this.mItemBoxes.splice(i, 1);
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="onDropItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let newItem = cal.moveItem(aItem, this.mDate);
+ newItem = cal.setItemToAllDay(newItem, true);
+ return newItem;
+ ]]></body>
+ </method>
+
+ <method name="selectOccurrence">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ for (let itemBox of this.mItemBoxes) {
+ if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
+ itemBox.selected = true;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="unselectOccurrence">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ for (let itemBox of this.mItemBoxes) {
+ if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
+ itemBox.selected = false;
+ }
+ }
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="dblclick" button="0"><![CDATA[
+ this.calendarView.controller.createNewEvent(null, this.mDate, null, true);
+ ]]></handler>
+ <handler event="mousedown"><![CDATA[
+ this.calendarView.selectedDay = this.mDate;
+ ]]></handler>
+ <handler event="click" button="0"><![CDATA[
+ if (!(event.ctrlKey || event.metaKey)) {
+ this.calendarView.setSelectedItems(0, []);
+ }
+ ]]></handler>
+ <handler event="click" button="2"><![CDATA[
+ let newStart = this.calendarView.selectedDay.clone();
+ newStart.isDate = true;
+ this.calendarView.selectedDateTime = newStart;
+ event.stopPropagation();
+ ]]></handler>
+ <handler event="wheel"><![CDATA[
+ if (this.getAttribute("orient") == "vertical") {
+ // In vertical view (normal), don't let the parent multiday view
+ // handle the scrolling in its bubbling phase. The default action
+ // will make the box scroll here.
+
+ // TODO We could scroll by the height of exactly one event box, but
+ // since a normal box's boxObject doesn't implement nsIScrollBoxObject,
+ // there is no way to scroll by pixels. Using a xul:scrollbox has
+ // problems since the equalsize attribute isn't inherited by the
+ // inner box, and even if that is worked around, something makes the
+ // rotated view look bad in that case.
+ event.stopPropagation();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <!--
+ - An individual event box, to be inserted into a column.
+ -->
+ <binding id="calendar-event-box" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
+ <content mousethrough="never" tooltip="itemTooltip">
+ <xul:box xbl:inherits="orient,width,height" flex="1">
+ <xul:box anonid="event-container"
+ class="calendar-color-box"
+ xbl:inherits="orient,readonly,flashing,alarm,allday,priority,progress,
+ status,calendar,categories,calendar-uri,calendar-id,todoType"
+ flex="1">
+ <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
+ <xul:stack anonid="eventbox"
+ align="stretch"
+ class="calendar-event-box-container"
+ flex="1"
+ xbl:inherits="context,parentorient=orient,readonly,flashing,alarm,allday,priority,progress,status,calendar,categories">
+ <xul:hbox class="calendar-event-details"
+ anonid="calendar-event-details"
+ align="start">
+ <xul:image anonid="item-icon"
+ class="calendar-item-image"
+ xbl:inherits="progress,allday,itemType,todoType"/>
+ <xul:description anonid="event-name" class="calendar-event-details-core" flex="1"/>
+ <xul:textbox anonid="event-name-textbox"
+ class="plain calendar-event-details-core calendar-event-name-textbox"
+ flex="1"
+ hidden="true"
+ wrap="true"/>
+ </xul:hbox>
+ <xul:stack mousethrough="always">
+ <xul:calendar-category-box anonid="category-box" xbl:inherits="categories" pack="end" />
+ <xul:hbox align="right">
+ <xul:hbox anonid="alarm-icons-box"
+ class="alarm-icons-box"
+ pack="end"
+ align="top"
+ xbl:inherits="flashing"/>
+ <xul:image anonid="item-classification-box"
+ class="item-classification-box"
+ pack="end"/>
+ </xul:hbox>
+ </xul:stack>
+ <xul:box xbl:inherits="orient">
+ <xul:calendar-event-gripbar anonid="gripbar1"
+ class="calendar-event-box-grippy-top"
+ mousethrough="never"
+ whichside="start"
+ xbl:inherits="parentorient=orient"/>
+ <xul:spacer mousethrough="always" flex="1"/>
+ <xul:calendar-event-gripbar anonid="gripbar2"
+ class="calendar-event-box-grippy-bottom"
+ mousethrough="never"
+ whichside="end"
+ xbl:inherits="parentorient=orient"/>
+ </xul:box>
+ <!-- Do not insert anything here, otherwise the event boxes will
+ not be resizable using the gripbars. If you want to insert
+ additional elements, do so above the box with the gripbars. -->
+ </xul:stack>
+ </xul:box>
+ </xul:box>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.orient = this.getAttribute("orient");
+ ]]></constructor>
+
+ <!-- fields -->
+ <field name="mParentColumn">null</field>
+
+ <!-- methods/properties -->
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ let needsrelayout = false;
+ if (aAttr == "orient") {
+ if (this.getAttribute("orient") != aVal) {
+ needsrelayout = true;
+ }
+ }
+
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+
+ if (needsrelayout) {
+ let eventbox = document.getAnonymousElementByAttribute(this, "anonid", "eventbox");
+ eventbox.setAttribute("orient", val);
+ let gb1 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1");
+ gb1.parentorient = val;
+ let gb2 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar2");
+ gb2.parentorient = val;
+ }
+
+ return ret;
+ ]]></body>
+ </method>
+
+ <method name="getOptimalMinSize">
+ <body><![CDATA[
+ if (this.getAttribute("orient") == "vertical") {
+ let minHeight = getOptimalMinimumHeight(this.eventNameLabel) +
+ getSummarizedStyleValues(document.getAnonymousElementByAttribute(this, "anonid", "eventbox"), ["margin-bottom", "margin-top"]) +
+ getSummarizedStyleValues(this, ["border-bottom-width", "border-top-width"]);
+ this.setAttribute("minheight", minHeight);
+ this.setAttribute("minwidth", "1");
+ return minHeight;
+ } else {
+ this.eventNameLabel.setAttribute("style", "min-width: 2em");
+ let minWidth = getOptimalMinimumWidth(this.eventNameLabel);
+ this.setAttribute("minwidth", minWidth);
+ this.setAttribute("minheight", "1");
+ return minWidth;
+ }
+ ]]></body>
+ </method>
+
+ <property name="parentColumn"
+ onget="return this.mParentColumn;"
+ onset="return (this.mParentColumn = val);"/>
+
+ <property name="startMinute" readonly="true">
+ <getter><![CDATA[
+ if (!this.mOccurrence) {
+ return 0;
+ }
+ let startDate = this.mOccurrence.startDate || this.mOccurrence.entryDate;
+ return startDate.hour * 60 + startDate.minute;
+ ]]></getter>
+ </property>
+
+ <property name="endMinute" readonly="true">
+ <getter><![CDATA[
+ if (!this.mOccurrence) {
+ return 0;
+ }
+ let endDate = this.mOccurrence.endDate || this.mOccurrence.dueDate;
+ return endDate.hour * 60 + endDate.minute;
+ ]]></getter>
+ </property>
+
+ <method name="setEditableLabel">
+ <body><![CDATA[
+ let evl = this.eventNameLabel;
+ let item = this.mOccurrence;
+
+ if (item.title && item.title != "") {
+ // Use <description> textContent so it can wrap.
+ evl.textContent = item.title;
+ } else {
+ evl.textContent = calGetString("calendar", "eventUntitled");
+ }
+
+ let gripbar = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1").boxObject.height;
+ let height = document.getAnonymousElementByAttribute(this, "anonid", "eventbox").boxObject.height;
+ evl.setAttribute("style", "max-height: " + Math.max(0, height-gripbar * 2) + "px");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown" button="0"><![CDATA[
+ event.stopPropagation();
+
+ if (this.mEditing) {
+ return;
+ }
+
+ this.parentColumn.calendarView.selectedDay = this.parentColumn.mDate;
+ this.mMouseX = event.screenX;
+ this.mMouseY = event.screenY;
+
+ let whichside = event.whichside;
+ if (whichside) {
+ this.calendarView.setSelectedItems(1,
+ [event.ctrlKey ? this.mOccurrence.parentItem : this.mOccurrence]);
+
+ let snapIntMin = (event.shiftKey &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ !event.metaKey) ? 1 : 15;
+ // start edge resize drag
+ this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, whichside,
+ event.screenX, event.screenY,
+ snapIntMin);
+ } else {
+ // may be click or drag,
+ // so wait for mousemove (or mouseout if fast) to start item move drag
+ this.mInMouseDown = true;
+ return;
+ }
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ if (!this.mInMouseDown) {
+ return;
+ }
+
+ let deltaX = Math.abs(event.screenX - this.mMouseX);
+ let deltaY = Math.abs(event.screenY - this.mMouseY);
+ // more than a 3 pixel move?
+ if ((deltaX * deltaX + deltaY * deltaY) > 9) {
+ if (this.parentColumn) {
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ this.editingTimer = null;
+ }
+
+ this.calendarView.setSelectedItems(1, [this.mOccurrence]);
+
+ this.mEditing = false;
+
+ this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, "middle", this.mMouseX, this.mMouseY);
+ this.mInMouseDown = false;
+ }
+ }
+ ]]></handler>
+
+ <handler event="mouseout"><![CDATA[
+ if (!this.mEditing && this.mInMouseDown && this.parentColumn) {
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ this.editingTimer = null;
+ }
+
+ this.calendarView.setSelectedItems(1, [this.mOccurrence]);
+
+ this.mEditing = false;
+
+ this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, "middle", this.mMouseX, this.mMouseY);
+ this.mInMouseDown = false;
+ }
+ ]]></handler>
+
+ <handler event="mouseup"><![CDATA[
+ if (this.mEditing) {
+ return;
+ }
+
+ this.mInMouseDown = false;
+ ]]></handler>
+
+ <handler event="mouseover"><![CDATA[
+ if (this.calendarView && this.calendarView.controller) {
+ event.stopPropagation();
+ onMouseOverItem(event);
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-multiday-view" extends="chrome://calendar/content/calendar-base-view.xml#calendar-base-view">
+ <content flex="1" orient="vertical" xbl:inherits="context,item-context">
+ <xul:box anonid="mainbox" class="multiday-view-main-box" flex="1">
+ <!-- these boxes are tricky: width or height in CSS depend on orient -->
+ <xul:box anonid="labelbox" class="multiday-view-label-box">
+ <xul:box anonid="labeltimespacer" class="multiday-view-label-time-spacer"/>
+ <xul:box anonid="labeldaybox"
+ class="multiday-view-label-day-box"
+ flex="1"
+ equalsize="always"/>
+ <xul:box anonid="labelscrollbarspacer" class="multiday-labelscrollbarspacer"/>
+ </xul:box>
+ <xul:box anonid="headerbox" class="multiday-view-header-box">
+ <xul:box anonid="headertimespacer" class="multiday-view-header-time-spacer"/>
+ <xul:box anonid="headerdaybox" class="multiday-view-header-day-box" flex="1" equalsize="always" />
+ <xul:box anonid="headerscrollbarspacer" class="multiday-headerscrollbarspacer"/>
+ </xul:box>
+ <xul:scrollbox anonid="scrollbox" flex="1"
+ onoverflow="adjustScrollBarSpacers();" onunderflow="adjustScrollBarSpacers();">
+ <!-- the orient of the calendar-time-bar needs to be the opposite of the parent -->
+ <xul:calendar-time-bar xbl:inherits="orient" anonid="timebar"/>
+ <xul:box anonid="daybox" class="multiday-view-day-box" flex="1"
+ equalsize="always"/>
+ </xul:scrollbox>
+ </xul:box>
+ </content>
+
+ <implementation implements="calICalendarView">
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ // get day start/end hour from prefs and set on the view
+ this.setDayStartEndMinutes(Preferences.get("calendar.view.daystarthour", 8) * 60,
+ Preferences.get("calendar.view.dayendhour", 17) * 60);
+
+ // initially scroll to the day start hour in the view
+ this.scrollToMinute(this.mDayStartMin);
+
+ // get visible hours from prefs and set on the view
+ let visibleMinutes = Preferences.get("calendar.view.visiblehours", 9) * 60;
+ this.setVisibleMinutes(visibleMinutes);
+
+ // set the flex attribute at the scrollbox-innerbox
+ // (this can be removed, after Bug 343555 is fixed)
+ let scrollbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "scrollbox");
+ document.getAnonymousElementByAttribute(
+ scrollbox, "class", "box-inherit scrollbox-innerbox").flex = "1";
+
+ // set the time interval for the time indicator timer
+ this.setTimeIndicatorInterval(Preferences.get("calendar.view.timeIndicatorInterval", 15));
+ this.enableTimeIndicator();
+
+ this.reorient();
+ ]]></constructor>
+
+ <property name="daysInView" readonly="true">
+ <getter><![CDATA[
+ return this.labeldaybox.childNodes && this.labeldaybox.childNodes.length;
+ ]]></getter>
+ </property>
+
+ <property name="supportsZoom" readonly="true"
+ onget="return true;"/>
+ <property name="supportsRotation" readonly="true"
+ onget="return true"/>
+
+ <method name="setTimeIndicatorInterval">
+ <parameter name="aPrefInterval"/>
+ <body><![CDATA[
+ // If the preference just edited by the user is outside the valid
+ // range [0, 1440], we change it into the nearest limit (0 or 1440).
+ let newTimeInterval = Math.max(0, Math.min(1440, aPrefInterval));
+ if (newTimeInterval != aPrefInterval) {
+ Preferences.set("calendar.view.timeIndicatorInterval", newTimeInterval);
+ }
+
+ if (newTimeInterval != this.mTimeIndicatorInterval) {
+ this.mTimeIndicatorInterval = newTimeInterval;
+ }
+ if (this.mTimeIndicatorInterval == 0) {
+ timeIndicator.cancel();
+ }
+ ]]></body>
+ </method>
+
+ <method name="enableTimeIndicator">
+ <body><![CDATA[
+ // Hide or show the time indicator if the preference becomes 0 or greater than 0.
+ let hideIndicator = this.mTimeIndicatorInterval == 0;
+ setBooleanAttribute(this.timeBarTimeIndicator, "hidden", hideIndicator);
+ let todayColumn = this.findColumnForDate(this.today());
+ if (todayColumn) {
+ setBooleanAttribute(todayColumn.column.timeIndicatorBox, "hidden", hideIndicator);
+ }
+ // Update the timer but only under some circumstances, otherwise
+ // it will update the wrong view or it will start without need.
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ let currView = currentView().type;
+ if (currentMode == "calendar" && currView == this.type && !hideIndicator &&
+ (currView == "day" || currView == "week")) {
+ this.updateTimeIndicatorPosition(true);
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateTimeIndicatorPosition">
+ <parameter name="aUpdateTheTimer"/>
+ <parameter name="aPpmChanged"/>
+ <parameter name="aViewChanged"/>
+ <body><![CDATA[
+ let now = cal.now();
+ let nowMinutes = now.hour * 60 + now.minute;
+ if (aUpdateTheTimer) {
+ let prefInt = this.mTimeIndicatorInterval;
+ if (prefInt == 0) {
+ timeIndicator.cancel();
+ return;
+ }
+
+ // Increase the update interval if pixels per minute is small.
+ let oldPrefInt = prefInt;
+ if (aPpmChanged && this.mPixPerMin < 0.6) {
+ prefInt = Math.round(prefInt / this.mPixPerMin);
+ }
+ if (!aPpmChanged || aViewChanged || oldPrefInt != prefInt) {
+ // Synchronize the timer with a multiple of the interval.
+ let firstInterval = (prefInt - nowMinutes % prefInt) * 60 - now.second;
+ if (timeIndicator.timer) {
+ timeIndicator.cancel();
+ }
+ timeIndicator.lastView = this.id;
+ timeIndicator.timer = setTimeout(() => {
+ this.updateTimeIndicatorPosition(false);
+ timeIndicator.start(prefInt * 60, this);
+ }, firstInterval * 1000);
+
+ // Set the time for the first positioning of the indicator.
+ let time = Math.floor(nowMinutes / prefInt) * prefInt;
+ document.getElementById("day-view").mTimeIndicatorMinutes = time;
+ document.getElementById("week-view").mTimeIndicatorMinutes = time;
+ }
+ } else if (aUpdateTheTimer === false) {
+ // Set the time for every positioning after the first
+ document.getElementById("day-view").mTimeIndicatorMinutes = nowMinutes;
+ document.getElementById("week-view").mTimeIndicatorMinutes = nowMinutes;
+ }
+ // Update the position of the indicator.
+ let position = Math.round(this.mPixPerMin * this.mTimeIndicatorMinutes) - 1;
+ let posAttr = (this.orient == "vertical" ? "top: " : "left: ");
+ this.timeBarTimeIndicator.setAttribute("style", posAttr + position + "px;");
+ let todayColumn = this.findColumnForDate(this.today());
+ if (todayColumn) {
+ todayColumn.column.timeIndicatorBox.setAttribute("style", "margin-" + posAttr + position + "px;");
+ }
+ ]]></body>
+ </method>
+
+ <method name="handlePreference">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aPreference"/>
+ <body><![CDATA[
+ aSubject.QueryInterface(Components.interfaces.nsIPrefBranch);
+ switch (aPreference) {
+
+ case "calendar.view.daystarthour":
+ this.setDayStartEndMinutes(aSubject.getIntPref(aPreference) * 60,
+ this.mDayEndMin);
+ this.refreshView();
+ break;
+
+ case "calendar.view.dayendhour":
+ this.setDayStartEndMinutes(this.mDayStartMin,
+ aSubject.getIntPref(aPreference) * 60);
+ this.refreshView();
+ break;
+
+ case "calendar.view.visiblehours":
+ this.setVisibleMinutes(aSubject.getIntPref(aPreference) * 60);
+ this.refreshView();
+ break;
+
+ case "calendar.view.timeIndicatorInterval":
+ this.setTimeIndicatorInterval(aSubject.getIntPref(aPreference));
+ this.enableTimeIndicator();
+ break;
+
+ default:
+ this.handleCommonPreference(aSubject, aTopic, aPreference);
+ break;
+ }
+ return;
+ ]]></body>
+ </method>
+
+ <method name="onResize">
+ <parameter name="aRealSelf"/>
+ <body><![CDATA[
+ let self = aRealSelf || this; // eslint-disable-line consistent-this
+ let isARelayout = !aRealSelf;
+ let scrollbox = document.getAnonymousElementByAttribute(self, "anonid", "scrollbox");
+ let size;
+ if (self.orient == "horizontal") {
+ size = scrollbox.boxObject.width;
+ } else {
+ size = scrollbox.boxObject.height;
+ }
+ let ppm = size / self.mVisibleMinutes;
+ ppm = Math.floor(ppm * 1000) / 1000;
+ if (ppm < self.mMinPixelsPerMinute) {
+ ppm = self.mMinPixelsPerMinute;
+ }
+ let ppmChanged = (self.pixelsPerMinute != ppm);
+ self.pixelsPerMinute = ppm;
+ setTimeout(() => self.scrollToMinute(self.mFirstVisibleMinute), 0);
+
+ // Fit the weekday labels while scrolling.
+ self.adjustWeekdayLength(self.getAttribute("orient") == "horizontal");
+
+ // Adjust the time indicator position and the related timer.
+ if (this.mTimeIndicatorInterval != 0) {
+ let viewChanged = isARelayout && (timeIndicator.lastView != this.id);
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ if (currentMode == "calendar" && (!timeIndicator.timer || ppmChanged || viewChanged)) {
+ self.updateTimeIndicatorPosition(true, ppmChanged, viewChanged);
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- mDateList will always be sorted before being set -->
+ <field name="mDateList">null</field>
+ <!-- array of { date: calIDatetime, column: colbox, header: hdrbox } -->
+ <field name="mDateColumns">null</field>
+ <field name="mPixPerMin">0.6</field>
+ <field name="mMinPixelsPerMinute">0.1</field>
+ <field name="mSelectedDayCol">null</field>
+ <field name="mSelectedDay">null</field>
+ <field name="mStartMin">0</field>
+ <field name="mEndMin">24 * 60</field>
+ <field name="mDayStartMin">0</field>
+ <field name="mDayEndMin">0</field>
+ <field name="mVisibleMinutes">9 * 60</field>
+ <field name="mClickedTime">null</field>
+ <field name="mTimeIndicatorInterval">15</field>
+ <field name="mModeHandler">null</field>
+ <field name="mTimeIndicatorMinutes">0</field>
+
+ <method name="flashAlarm">
+ <parameter name="aAlarmItem"/>
+ <parameter name="aStop"/>
+ <body><![CDATA[
+ // Helper function to save some duplicate code
+ function setFlashingAttribute(aBox) {
+ if (aStop) {
+ aBox.removeAttribute("flashing");
+ } else {
+ aBox.setAttribute("flashing", "true");
+ }
+ }
+
+ let showIndicator = Preferences.get("calendar.alarms.indicator.show", true);
+ let totaltime = Preferences.get("calendar.alarms.indicator.totaltime", 3600);
+
+ if (!aStop && (!showIndicator || totaltime < 1)) {
+ // No need to animate if the indicator should not be shown.
+ return;
+ }
+
+
+ // Make sure the flashing attribute is set or reset on all visible
+ // boxes.
+ let columns = this.findColumnsForItem(aAlarmItem);
+ for (let col of columns) {
+ let box = col.column.findChunkForOccurrence(aAlarmItem);
+ if (box && box.eventbox) {
+ setFlashingAttribute(box.eventbox);
+ }
+ box = col.header.findBoxForItem(aAlarmItem);
+ if (box) {
+ setFlashingAttribute(box);
+ }
+ }
+
+ if (aStop) {
+ // We are done flashing, prevent newly created event boxes from flashing.
+ delete this.mFlashingEvents[aAlarmItem.hashId];
+ } else {
+ // Set up a timer to stop the flashing after the total time.
+ this.mFlashingEvents[aAlarmItem.hashId] = aAlarmItem;
+ setTimeout(() => this.flashAlarm(aAlarmItem, true), totaltime);
+ }
+ ]]></body>
+ </method>
+
+ <!-- calICalendarView -->
+ <property name="supportsDisjointDates"
+ onget="return true"/>
+ <property name="hasDisjointDates"
+ onget="return (this.mDateList != null);"/>
+
+ <property name="startDate">
+ <getter><![CDATA[
+ if (this.mStartDate) {
+ return this.mStartDate;
+ } else if (this.mDateList && this.mDateList.length > 0) {
+ return this.mDateList[0];
+ } else {
+ return null;
+ }
+ ]]></getter>
+ </property>
+
+ <property name="endDate">
+ <getter><![CDATA[
+ if (this.mEndDate) {
+ return this.mEndDate;
+ } else if (this.mDateList && this.mDateList.length > 0) {
+ return this.mDateList[this.mDateList.length - 1];
+ } else {
+ return null;
+ }
+ ]]></getter>
+ </property>
+
+ <method name="showDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ let targetDate = aDate.getInTimezone(this.mTimezone);
+ targetDate.isDate = true;
+
+ if (this.mStartDate && this.mEndDate) {
+ if (this.mStartDate.compare(targetDate) <= 0 &&
+ this.mEndDate.compare(targetDate) >= 0) {
+ return;
+ }
+ } else if (this.mDateList) {
+ for (let date of this.mDateList) {
+ // if date is already visible, nothing to do
+ if (date.compare(targetDate) == 0) {
+ return;
+ }
+ }
+ }
+
+ // if we're only showing one date, then continue
+ // to only show one date; otherwise, show the week.
+ if (this.numVisibleDates == 1) {
+ this.setDateRange(aDate, aDate);
+ } else {
+ this.setDateRange(aDate.startOfWeek, aDate.endOfWeek);
+ }
+
+ this.selectedDay = targetDate;
+ ]]></body>
+ </method>
+
+ <method name="setDateRange">
+ <parameter name="aStartDate"/>
+ <parameter name="aEndDate"/>
+ <body><![CDATA[
+ this.rangeStartDate = aStartDate;
+ this.rangeEndDate = aEndDate;
+
+ let viewStart = aStartDate.getInTimezone(this.mTimezone);
+ let viewEnd = aEndDate.getInTimezone(this.mTimezone);
+
+ viewStart.isDate = true;
+ viewStart.makeImmutable();
+ viewEnd.isDate = true;
+ viewEnd.makeImmutable();
+ this.mStartDate = viewStart;
+ this.mEndDate = viewEnd;
+
+ // goToDay are called when toggle the values below. The attempt to fix
+ // Bug 872063 has modified the behavior of setDateRange, which doesn't
+ // always refresh the view anymore. That is not the expected behavior
+ // by goToDay. Add checks here to determine if the view need to be
+ // refreshed.
+
+ // First, check values of tasksInView, workdaysOnly, showCompleted.
+ // Their status will determine the value of toggleStatus, which is
+ // saved to this.mToggleStatus during last call to relayout()
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ // Update the navigation bar only when changes are related to the current view.
+ if (this.isVisible()) {
+ cal.navigationBar.setDateRange(viewStart, viewEnd);
+ }
+
+ // Check whether view range has been changed since last call to
+ // relayout()
+ if (!this.mViewStart || !this.mViewEnd ||
+ this.mViewEnd.compare(viewEnd) != 0 ||
+ this.mViewStart.compare(viewStart) != 0 ||
+ this.mToggleStatus != toggleStatus) {
+ this.refresh();
+ }
+ ]]></body>
+ </method>
+
+ <method name="getDateList">
+ <parameter name="aCount"/>
+ <body><![CDATA[
+ let dates = [];
+ if (this.mStartDate && this.mEndDate) {
+ let date = this.mStartDate.clone();
+ while (date.compare(this.mEndDate) <= 0) {
+ dates.push(d.clone());
+ date.day += 1;
+ }
+ } else if (this.mDateList) {
+ for (let date of this.mDateList) {
+ dates.push(date.clone());
+ }
+ }
+
+ aCount.value = dates.length;
+ return dates;
+ ]]></body>
+ </method>
+
+ <property name="selectedDateTime">
+ <getter><![CDATA[
+ return this.mClickedTime;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mClickedTime = val;
+ ]]></setter>
+ </property>
+
+ <property name="selectedDay">
+ <getter><![CDATA[
+ let selected;
+ if (this.numVisibleDates == 1) {
+ selected = this.mDateColumns[0].date;
+ } else if (this.mSelectedDay) {
+ selected = this.mSelectedDay;
+ } else if (this.mSelectedDayCol) {
+ selected = this.mSelectedDayCol.date;
+ }
+
+ // TODO Make sure the selected day is valid
+ // TODO select now if it is in the range?
+ return selected;
+ ]]></getter>
+ <setter><![CDATA[
+ // ignore if just 1 visible, it's always selected,
+ // but we don't indicate it
+ if (this.numVisibleDates == 1) {
+ this.fireEvent("dayselect", val);
+ return val;
+ }
+
+ if (this.mSelectedDayCol) {
+ this.mSelectedDayCol.column.selected = false;
+ this.mSelectedDayCol.header.removeAttribute("selected");
+ }
+
+ if (val) {
+ this.mSelectedDayCol = this.findColumnForDate(val);
+ if (this.mSelectedDayCol) {
+ this.mSelectedDay = this.mSelectedDayCol.date;
+ this.mSelectedDayCol.column.selected = true;
+ this.mSelectedDayCol.header.setAttribute("selected", "true");
+ } else {
+ this.mSelectedDay = val;
+ }
+ }
+ this.fireEvent("dayselect", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="getSelectedItems">
+ <parameter name="aCount"/>
+ <body><![CDATA[
+ aCount.value = this.mSelectedItems.length;
+ return this.mSelectedItems;
+ ]]></body>
+ </method>
+ <method name="setSelectedItems">
+ <parameter name="aCount"/>
+ <parameter name="aItems"/>
+ <parameter name="aSuppressEvent"/>
+ <body><![CDATA[
+ if (this.mSelectedItems) {
+ for (let item of this.mSelectedItems) {
+ for (let occ of this.getItemOccurrencesInView(item)) {
+ let cols = this.findColumnsForItem(occ);
+ for (let col of cols) {
+ col.header.unselectOccurrence(occ);
+ col.column.unselectOccurrence(occ);
+ }
+ }
+ }
+ }
+ this.mSelectedItems = aItems || [];
+
+ for (let item of this.mSelectedItems) {
+ for (let occ of this.getItemOccurrencesInView(item)) {
+ let cols = this.findColumnsForItem(occ);
+ if (cols.length > 0) {
+ let start = item.startDate || item.entryDate || item.dueDate;
+ for (let col of cols) {
+ if (start.isDate) {
+ col.header.selectOccurrence(occ);
+ } else {
+ col.column.selectOccurrence(occ);
+ }
+ }
+ }
+ }
+ }
+
+ if (!aSuppressEvent) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+ ]]></body>
+ </method>
+
+ <method name="getItemOccurrencesInView">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ if (aItem.recurrenceInfo && aItem.recurrenceStartDate) {
+ // if selected a parent item, show occurrence(s) in view range
+ return aItem.getOccurrencesBetween(this.startDate, this.queryEndDate, {}, 0);
+ } else if (aItem.recurrenceStartDate) {
+ return [aItem];
+ } else { // undated todo
+ return [];
+ }
+ ]]></body>
+ </method>
+
+ <method name="centerSelectedItems">
+ <body><![CDATA[
+ let displayTZ = calendarDefaultTimezone();
+ let lowMinute = 24 * 60;
+ let highMinute = 0;
+
+ for (let item of this.mSelectedItems) {
+ let startDateProperty = calGetStartDateProp(item);
+ let endDateProperty = calGetEndDateProp(item);
+
+ let occs = [];
+ if (item.recurrenceInfo) {
+ // if selected a parent item, show occurrence(s) in view range
+ occs = item.getOccurrencesBetween(this.startDate, this.queryEndDate, {}, 0);
+ } else {
+ occs = [item];
+ }
+
+ for (let occ of occs) {
+ let occStart = occ[startDateProperty];
+ let occEnd = occ[endDateProperty];
+ // must have at least one of start or end
+ if (!occStart && !occEnd) {
+ continue; // task with no dates
+ }
+
+ // if just has single datetime, treat as zero duration item
+ // (such as task with due datetime or start datetime only)
+ occStart = occStart || occEnd;
+ occEnd = occEnd || occStart;
+ // Now both occStart and occEnd are datetimes.
+
+ // skip occurrence if all-day: it won't show in time view.
+ if (occStart.isDate || occEnd.isDate) {
+ continue;
+ }
+
+ // Trim dates to view. (Not mutated so just reuse view dates)
+ if (this.startDate.compare(occStart) > 0) {
+ occStart = this.startDate;
+ }
+ if (this.queryEndDate.compare(occEnd) < 0) {
+ occEnd = this.queryEndDate;
+ }
+
+ // Convert to display timezone if different
+ if (occStart.timezone != displayTZ) {
+ occStart = occStart.getInTimezone(displayTZ);
+ }
+ if (occEnd.timezone != displayTZ) {
+ occEnd = occEnd.getInTimezone(displayTZ);
+ }
+ // If crosses midnite in current TZ, set end just
+ // before midnite after start so start/title usually visible.
+ if (!sameDay(occStart, occEnd)) {
+ occEnd = occStart.clone();
+ occEnd.day = occStart.day;
+ occEnd.hour = 23;
+ occEnd.minute = 59;
+ }
+
+ // Ensure range shows occ
+ lowMinute = Math.min(occStart.hour * 60 + occStart.minute,
+ lowMinute);
+ highMinute = Math.max(occEnd.hour * 60 + occEnd.minute,
+ highMinute);
+ }
+ }
+
+ let displayDuration = highMinute - lowMinute;
+ if (this.mSelectedItems.length &&
+ displayDuration >= 0) {
+ let minute;
+ if (displayDuration <= this.mVisibleMinutes) {
+ minute = lowMinute + (displayDuration - this.mVisibleMinutes) / 2;
+ } else if (this.mSelectedItems.length == 1) {
+ // If the displayDuration doesn't fit into the visible
+ // minutes, but only one event is selected, then go ahead and
+ // center the event start.
+
+ minute = Math.max(0, lowMinute - (this.mVisibleMinutes / 2));
+ }
+ this.scrollToMinute(minute);
+ }
+ ]]></body>
+ </method>
+
+ <property name="pixelsPerMinute">
+ <getter><![CDATA[
+ return this.mPixPerMin;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mPixPerMin = val;
+
+ let timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
+ timebar.pixelsPerMinute = val;
+
+ if (!this.mDateColumns) {
+ return val;
+ }
+ for (let col of this.mDateColumns) {
+ col.column.pixelsPerMinute = val;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- private -->
+
+ <property name="numVisibleDates" readonly="true">
+ <getter><![CDATA[
+ if (this.mDateList) {
+ return this.mDateList.length;
+ }
+
+ let count = 0;
+
+ if (!this.mStartDate || !this.mEndDate) {
+ // The view has not been initialized, so there are 0 visible dates.
+ return count;
+ }
+
+ let date = this.mStartDate.clone();
+ while (date.compare(this.mEndDate) <= 0) {
+ count++;
+ date.day += 1;
+ }
+
+ return count;
+ ]]></getter>
+ </property>
+
+ <property name="orient">
+ <getter><![CDATA[
+ return this.getAttribute("orient") || "vertical";
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("orient", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="timeBarTimeIndicator" readonly="true">
+ <getter><![CDATA[
+ let timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
+ return document.getAnonymousElementByAttribute(timebar, "anonid", "timeIndicatorBoxTimeBar");
+ ]]></getter>
+ </property>
+
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ let needsreorient = false;
+ let needsrelayout = false;
+ if (aAttr == "orient") {
+ if (this.getAttribute("orient") != aVal) {
+ needsreorient = true;
+ }
+ }
+
+ if (aAttr == "context" || aAttr == "item-context") {
+ needsrelayout = true;
+ }
+
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+
+ if (needsrelayout && !needsreorient) {
+ this.relayout();
+ }
+
+ if (needsreorient) {
+ this.reorient();
+ }
+
+ return ret;
+ ]]></body>
+ </method>
+
+ <method name="reorient">
+ <body><![CDATA[
+ let orient = this.getAttribute("orient") || "horizontal";
+ let otherorient = (orient == "vertical" ? "horizontal" : "vertical");
+
+ if (orient == "horizontal") {
+ this.pixelsPerMinute = 1.5;
+ } else {
+ this.pixelsPerMinute = 0.6;
+ }
+
+ let normalelems = ["mainbox", "timebar"];
+ let otherelems = ["labelbox", "labeldaybox", "headertimespacer",
+ "headerbox", "headerdaybox", "scrollbox", "daybox"];
+
+ for (let id of normalelems) {
+ document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", orient);
+ }
+ for (let id of otherelems) {
+ document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", otherorient);
+ }
+
+ let scrollbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "scrollbox");
+ let mainbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "mainbox");
+
+ if (orient == "vertical") {
+ scrollbox.setAttribute(
+ "style", "overflow-x: hidden; overflow-y: auto;");
+ mainbox.setAttribute(
+ "style", "overflow-x: auto; overflow-y: hidden;");
+ } else {
+ scrollbox.setAttribute(
+ "style", "overflow-x: auto; overflow-y: hidden;");
+ mainbox.setAttribute(
+ "style", "overflow-x: hidden; overflow-y: auto;");
+ }
+
+ let boxes = ["daybox", "headerdaybox"];
+ for (let boxname of boxes) {
+ let box = document.getAnonymousElementByAttribute(this, "anonid", boxname);
+ setAttributeToChildren(box, "orient", orient);
+ }
+
+ setAttributeToChildren(this.labeldaybox, "orient", otherorient);
+
+ // Refresh
+ this.refresh();
+ ]]></body>
+ </method>
+
+ <method name="relayout">
+ <body><![CDATA[
+ if (!this.mStartDate || !this.mEndDate) {
+ return;
+ }
+
+ let orient = this.getAttribute("orient") || "horizontal";
+ let otherorient = getOtherOrientation(orient);
+
+ let computedDateList = [];
+ let startDate = this.mStartDate.clone();
+ while (startDate.compare(this.mEndDate) <= 0) {
+ let workday = startDate.clone();
+ workday.makeImmutable();
+ if (this.mDisplayDaysOff) {
+ computedDateList.push(workday);
+ } else if (!this.mDaysOffArray.includes(startDate.weekday)) {
+ computedDateList.push(workday);
+ }
+ startDate.day += 1;
+ }
+ this.mDateList = computedDateList;
+
+ // unselect previous selected event upon switch views, otherwise those
+ // events will stay selected forever, if select other events after
+ // change week view.
+ this.setSelectedItems(0, [], true);
+
+ let daybox = document.getAnonymousElementByAttribute(this, "anonid", "daybox");
+ let headerdaybox = document.getAnonymousElementByAttribute(this, "anonid", "headerdaybox");
+
+ let dayStartMin = this.mDayStartMin;
+ let dayEndMin = this.mDayEndMin;
+ let setUpDayEventsBox = (aDayBox, date) => {
+ aDayBox.setAttribute("class", "calendar-event-column-" + (counter % 2 == 0 ? "even" : "odd"));
+ aDayBox.setAttribute("context", this.getAttribute("context"));
+ aDayBox.setAttribute("item-context", this.getAttribute("item-context") || this.getAttribute("context"));
+ aDayBox.startLayoutBatchChange();
+ aDayBox.date = date;
+ aDayBox.setAttribute("orient", orient);
+ aDayBox.calendarView = this;
+ aDayBox.setDayStartEndMinutes(dayStartMin, dayEndMin);
+ };
+
+ let setUpDayHeaderBox = (aDayBox, date) => {
+ aDayBox.date = date;
+ aDayBox.calendarView = this;
+ aDayBox.setAttribute("orient", "vertical");
+ // Since the calendar-header-container boxes have the same vertical
+ // orientation for normal and rotated views, it needs an attribute
+ // "rotated" in order to have different css rules.
+ setBooleanAttribute(aDayBox, "rotated", orient == "horizontal");
+ };
+
+ this.mDateColumns = [];
+
+
+ // get today's date
+ let today = this.today();
+ let counter = 0;
+ let dayboxkids = daybox.childNodes;
+ let headerboxkids = headerdaybox.childNodes;
+ let labelboxkids = this.labeldaybox.childNodes;
+ let updateTimeIndicator = false;
+
+ for (let date of computedDateList) {
+ let dayEventsBox;
+ if (counter < dayboxkids.length) {
+ dayEventsBox = dayboxkids[counter];
+ dayEventsBox.removeAttribute("relation");
+ dayEventsBox.mEventInfos = [];
+ } else {
+ dayEventsBox = createXULElement("calendar-event-column");
+ dayEventsBox.setAttribute("flex", "1");
+ daybox.appendChild(dayEventsBox);
+ }
+ setUpDayEventsBox(dayEventsBox, date);
+
+ let dayHeaderBox;
+ if (counter < headerboxkids.length) {
+ dayHeaderBox = headerboxkids[counter];
+ // Delete backwards to make sure we get them all
+ // and delete until no more elements are left.
+ while (dayHeaderBox.mItemBoxes.length != 0) {
+ let num = dayHeaderBox.mItemBoxes.length;
+ dayHeaderBox.deleteEvent(dayHeaderBox.mItemBoxes[num-1].occurrence);
+ }
+ } else {
+ dayHeaderBox = createXULElement("calendar-header-container");
+ dayHeaderBox.setAttribute("flex", "1");
+ headerdaybox.appendChild(dayHeaderBox);
+ }
+ setUpDayHeaderBox(dayHeaderBox, date);
+
+ if (this.mDaysOffArray.indexOf(date.weekday) >= 0) {
+ dayEventsBox.dayOff = true;
+ dayHeaderBox.setAttribute("weekend", "true");
+ } else {
+ dayEventsBox.dayOff = false;
+ dayHeaderBox.removeAttribute("weekend");
+ }
+ let labelbox;
+ if (counter < labelboxkids.length) {
+ labelbox = labelboxkids[counter];
+ labelbox.date = date;
+ } else {
+ labelbox = createXULElement("calendar-day-label");
+ labelbox.setAttribute("orient", otherorient);
+ this.labeldaybox.appendChild(labelbox);
+ labelbox.date = date;
+ }
+ // Set attributes for date relations and for the time indicator.
+ let headerDayBox = document.getAnonymousElementByAttribute(
+ this, "anonid", "headerdaybox");
+ headerDayBox.removeAttribute("todaylastinview");
+ dayEventsBox.timeIndicatorBox.setAttribute("hidden", "true");
+ switch (date.compare(today)) {
+ case -1: {
+ dayHeaderBox.setAttribute("relation", "past");
+ dayEventsBox.setAttribute("relation", "past");
+ labelbox.setAttribute("relation", "past");
+ break;
+ }
+ case 0: {
+ let relation_ = this.numVisibleDates == 1 ? "today1day" : "today";
+ dayHeaderBox.setAttribute("relation", relation_);
+ dayEventsBox.setAttribute("relation", relation_);
+ labelbox.setAttribute("relation", relation_);
+ setBooleanAttribute(dayEventsBox.timeIndicatorBox, "hidden", this.mTimeIndicatorInterval == 0);
+ updateTimeIndicator = true;
+
+ // Due to equalsize=always being set on the dayboxes
+ // parent, there are a few issues showing the border of
+ // the last daybox correctly. To work around this, we're
+ // setting an attribute we can use in CSS. For more
+ // information about this hack, see bug 455045
+ if (dayHeaderBox == headerdaybox.childNodes[headerdaybox.childNodes.length - 1] &&
+ this.numVisibleDates > 1) {
+ headerDayBox.setAttribute("todaylastinview", "true");
+ }
+ break;
+ }
+ case 1: {
+ dayHeaderBox.setAttribute("relation", "future");
+ dayEventsBox.setAttribute("relation", "future");
+ labelbox.setAttribute("relation", "future");
+ break;
+ }
+ }
+ // We don't want to actually mess with our original dates, plus
+ // they're likely to be immutable.
+ let date2 = date.clone();
+ date2.isDate = true;
+ date2.makeImmutable();
+ this.mDateColumns.push({ date: date2, column: dayEventsBox, header: dayHeaderBox });
+ counter++;
+ }
+
+ // Remove any extra columns that may have been hanging around
+ function removeExtraKids(elem) {
+ while (counter < elem.childNodes.length) {
+ elem.childNodes[counter].remove();
+ }
+ }
+ removeExtraKids(daybox);
+ removeExtraKids(headerdaybox);
+ removeExtraKids(this.labeldaybox);
+
+ if (updateTimeIndicator) {
+ this.updateTimeIndicatorPosition();
+ }
+
+ // fix pixels-per-minute
+ this.onResize();
+ if (this.mDateColumns) {
+ for (let col of this.mDateColumns) {
+ col.column.endLayoutBatchChange();
+ }
+ }
+
+ // Adjust scrollbar spacers
+ this.adjustScrollBarSpacers();
+
+ // Store the start and end of current view. Next time when
+ // setDateRange is called, it will use mViewStart and mViewEnd to
+ // check if view range has been changed.
+ this.mViewStart = this.mStartDate;
+ this.mViewEnd = this.mEndDate;
+
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ this.mToggleStatus = toggleStatus;
+ ]]></body>
+ </method>
+
+ <method name="findColumnForDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ if (!this.mDateColumns) {
+ return null;
+ }
+ for (let col of this.mDateColumns) {
+ if (col.date.compare(aDate) == 0) {
+ return col;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="findDayBoxForDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ let col = this.findColumnForDate(aDate);
+ return (col && col.header);
+ ]]></body>
+ </method>
+
+ <method name="selectColumnHeader">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ let child = this.labeldaybox.firstChild;
+ while (child) {
+ if (child.date.compare(aDate) == 0) {
+ child.setAttribute("selected", "true");
+ } else {
+ child.removeAttribute("selected");
+ }
+ child = child.nextSibling;
+ }
+ ]]></body>
+ </method>
+
+ <method name="findColumnsForOccurrences">
+ <parameter name="aOccurrences"/>
+ <body><![CDATA[
+ if (!this.mDateColumns || !this.mDateColumns.length) {
+ return [];
+ }
+
+ let occMap = {};
+ for (let occ of aOccurrences) {
+ let startDate = occ[calGetStartDateProp(occ)]
+ .getInTimezone(this.mStartDate.timezone);
+ let endDate = occ[calGetEndDateProp(occ)]
+ .getInTimezone(this.mEndDate.timezone) || startDate;
+ if (startDate.compare(this.mStartDate) >= 0 &&
+ endDate.compare(this.mEndDate) <= 0) {
+ for (let i = startDate.day; i <= endDate.day; i++) {
+ occMap[i] = true;
+ }
+ }
+ }
+
+ return this.mDateColumns.filter(col => col.date.day in occMap);
+ ]]></body>
+ </method>
+
+ <method name="findColumnsForItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let columns = [];
+
+ if (!this.mDateColumns) {
+ return columns;
+ }
+
+ // Note that these may be dates or datetimes
+ let startDate = aItem.startDate || aItem.entryDate || aItem.dueDate;
+ if (!startDate) {
+ return columns;
+ }
+ let timezone = this.mDateColumns[0].date.timezone;
+ let targetDate = startDate.getInTimezone(timezone);
+ let finishDate = (aItem.endDate || aItem.dueDate || aItem.entryDate || startDate)
+ .getInTimezone(timezone);
+
+ if (!targetDate.isDate) {
+ // Set the time to 00:00 so that we get all the boxes
+ targetDate.hour = 0;
+ targetDate.minute = 0;
+ targetDate.second = 0;
+ }
+
+ if (targetDate.compare(finishDate) == 0) {
+ // We have also to handle zero length events in particular for
+ // tasks without entry or due date.
+ let col = this.findColumnForDate(targetDate);
+ if (col) {
+ columns.push(col);
+ }
+ }
+
+ while (targetDate.compare(finishDate) == -1) {
+ let col = this.findColumnForDate(targetDate);
+
+ // This might not exist if the event spans the view start or end
+ if (col) {
+ columns.push(col);
+ }
+ targetDate.day += 1;
+ }
+
+ return columns;
+ ]]></body>
+ </method>
+
+ <!-- for the given client-coord-system point, return
+ - the calendar-event-column that contains it. If
+ - no column contains it, return null.
+ -->
+ <method name="findColumnForClientPoint">
+ <parameter name="aClientX"/>
+ <parameter name="aClientY"/>
+ <body><![CDATA[
+ if (!this.mDateColumns) {
+ return null;
+ }
+ for (let col of this.mDateColumns) {
+ let boxObject = document.getAnonymousElementByAttribute(col.column, "anonid", "boxstack").boxObject;
+ if (aClientX >= boxObject.screenX &&
+ aClientX <= (boxObject.screenX + boxObject.width) &&
+ aClientY >= boxObject.screenY &&
+ aClientY <= (boxObject.screenY + boxObject.height)) {
+ return col.column;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="adjustScrollbarSpacersForAlldayEvents">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let startDate = aEvent[calGetStartDateProp(aEvent)];
+ let endDate = aEvent[calGetEndDateProp(aEvent)];
+ if ((startDate && startDate.isDate) ||
+ (endDate && endDate.isDate)) {
+ // If this is an all day event, then the header with allday
+ // events could possibly get a scrollbar. Readjust them.
+ this.adjustScrollBarSpacers();
+ }
+ ]]></body>
+ </method>
+
+ <method name="doAddItem">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let cols = this.findColumnsForItem(aEvent);
+ if (!cols.length) {
+ return;
+ }
+
+ for (let col of cols) {
+ let column = col.column;
+ let header = col.header;
+
+ let estart = aEvent.startDate || aEvent.entryDate || aEvent.dueDate;
+ if (estart.isDate) {
+ header.addEvent(aEvent);
+ } else {
+ column.addEvent(aEvent);
+ }
+ }
+
+ this.adjustScrollbarSpacersForAlldayEvents(aEvent);
+ ]]></body>
+ </method>
+
+ <method name="doDeleteItem">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let cols = this.findColumnsForItem(aEvent);
+ if (!cols.length) {
+ return;
+ }
+
+ let oldLength = this.mSelectedItems.length;
+ this.mSelectedItems = this.mSelectedItems.filter((item) => {
+ return item.hashId != aEvent.hashId;
+ });
+
+ for (let col of cols) {
+ let column = col.column;
+ let header = col.header;
+
+ let estart = aEvent.startDate || aEvent.entryDate || aEvent.dueDate;
+ if (estart.isDate) {
+ header.deleteEvent(aEvent);
+ } else {
+ column.deleteEvent(aEvent);
+ }
+ }
+
+ // If a deleted event was selected, we need to announce that the
+ // selection changed.
+ if (oldLength != this.mSelectedItems.length) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+
+ this.adjustScrollbarSpacersForAlldayEvents(aEvent);
+ ]]></body>
+ </method>
+
+ <method name="deleteItemsFromCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ if (!this.mDateColumns) {
+ return;
+ }
+ for (let col of this.mDateColumns) {
+ // get all-day events in column header and events within the column
+ let colEvents = col.header.mItemBoxes.map(box => box.occurrence)
+ .concat(col.column.mEventInfos.map(info => info.event));
+
+ for (let event of colEvents) {
+ if (event.calendar.id == aCalendar.id) {
+ this.doDeleteItem(event);
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="adjustScrollBarSpacers">
+ <body><![CDATA[
+ // get the view's orientation
+ let propertyName;
+ if (this.getAttribute("orient") == "vertical") {
+ propertyName = "width";
+ } else {
+ propertyName = "height";
+ }
+
+ // get the width/height of the scrollbox scrollbar
+ let scrollbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "scrollbox");
+ let propertyValue = scrollbox.boxObject.firstChild.boxObject[propertyName];
+ // Check if we need to show the headerScrollbarSpacer at all
+ let headerPropertyValue = propertyValue;
+ let headerDayBox = document.getAnonymousElementByAttribute(
+ this, "anonid", "headerdaybox");
+ if (headerDayBox) {
+ // Only do this when there are multiple days
+ let headerDayBoxMaxHeight = parseInt(document.defaultView.getComputedStyle(headerDayBox, null)
+ .getPropertyValue("max-height"), 10);
+ if (this.getAttribute("orient") == "vertical" &&
+ headerDayBox.boxObject.height >= headerDayBoxMaxHeight) {
+ // If the headerDayBox is just as high as the max-height, then
+ // there is already a scrollbar and we don't need to show the
+ // headerScrollbarSpacer. This is only valid for the non-rotated
+ // view.
+ headerPropertyValue = 0;
+ }
+ }
+
+ // set the same width/height for the label and header box spacers
+ let headerScrollBarSpacer = document.getAnonymousElementByAttribute(
+ this, "anonid", "headerscrollbarspacer");
+ headerScrollBarSpacer.setAttribute(propertyName, headerPropertyValue);
+ let labelScrollBarSpacer = document.getAnonymousElementByAttribute(
+ this, "anonid", "labelscrollbarspacer");
+ labelScrollBarSpacer.setAttribute(propertyName, propertyValue);
+ ]]></body>
+ </method>
+
+ <field name="mFirstVisibleMinute">0</field>
+ <method name="scrollToMinute">
+ <parameter name="aMinute"/>
+ <body><![CDATA[
+ let scrollbox = document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
+ let scrollBoxObject = scrollbox.boxObject;
+ // 'aMinute' will be the first minute showed in the view, so it must
+ // belong to the range 0 <-> (24*60 - minutes_showed_in_the_view) but
+ // we consider 25 hours instead of 24 to let the view scroll until
+ // showing events that start just before 0.00
+ let maxFirstMin = 25 * 60 - Math.round(scrollBoxObject.height / this.mPixPerMin);
+ aMinute = Math.min(maxFirstMin, Math.max(0, aMinute));
+
+ if (scrollBoxObject && scrollbox.scrollHeight > 0) {
+ let x = {}, y = {};
+ scrollBoxObject.getPosition(x, y);
+ let pos = Math.round(aMinute * this.mPixPerMin);
+ if (scrollbox.getAttribute("orient") == "horizontal") {
+ scrollBoxObject.scrollTo(x.value, pos);
+ } else {
+ scrollBoxObject.scrollTo(pos, y.value);
+ }
+ }
+
+ // Set the first visible minute in any case, we want to move to the
+ // right minute as soon as possible if we couldn't do so above.
+ this.mFirstVisibleMinute = aMinute;
+ ]]></body>
+ </method>
+
+ <method name="setDayStartEndMinutes">
+ <parameter name="aDayStartMin"/>
+ <parameter name="aDayEndMin"/>
+ <body><![CDATA[
+ if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
+ aDayEndMin > this.mEndMin) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.mDayStartMin != aDayStartMin ||
+ this.mDayEndMin != aDayEndMin) {
+ this.mDayStartMin = aDayStartMin;
+ this.mDayEndMin = aDayEndMin;
+
+ // Also update on the time-bar
+ document.getAnonymousElementByAttribute(this, "anonid", "timebar")
+ .setDayStartEndHours(this.mDayStartMin / 60,
+ this.mDayEndMin / 60);
+ }
+
+ ]]></body>
+ </method>
+
+ <method name="setVisibleMinutes">
+ <parameter name="aVisibleMinutes"/>
+ <body><![CDATA[
+ if (aVisibleMinutes <= 0 ||
+ aVisibleMinutes > (this.mEndMin - this.mStartMin)) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.mVisibleMinutes != aVisibleMinutes) {
+ this.mVisibleMinutes = aVisibleMinutes;
+ }
+ return this.mVisibleMinutes;
+ ]]></body>
+ </method>
+
+ <method name="zoomIn">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ let visibleHours = Preferences.get("calendar.view.visiblehours", 9);
+ visibleHours += (aLevel || 1);
+
+ Preferences.set("calendar.view.visiblehours", Math.min(visibleHours, 24));
+ ]]></body>
+ </method>
+ <method name="zoomOut">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ let visibleHours = Preferences.get("calendar.view.visiblehours", 9);
+ visibleHours -= (aLevel || 1);
+
+ Preferences.set("calendar.view.visiblehours", Math.max(1, visibleHours));
+ ]]></body>
+ </method>
+ <method name="zoomReset">
+ <body><![CDATA[
+ Preferences.set("calendar.view.visiblehours", 9);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="click" button="2"><![CDATA[
+ this.selectedDateTime = null;
+ ]]></handler>
+ <handler event="wheel" phase="bubbling"><![CDATA[
+ if (!event.ctrlKey && !event.shiftKey &&
+ !event.altKey && !event.metaKey) {
+ // Only shift hours if no modifier is pressed.
+
+ let minute = this.mFirstVisibleMinute;
+ if (event.deltaMode == event.DOM_DELTA_LINE) {
+ if (this.rotated && event.deltaX != 0) {
+ minute += event.deltaX < 0 ? -60 : 60;
+ } else if (!this.rotated && event.deltaY != 0) {
+ minute += event.deltaY < 0 ? -60 : 60;
+ }
+ } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
+ if (this.rotated && event.deltaX != 0) {
+ minute += Math.ceil(event.deltaX / this.mPixPerMin);
+ } else if (!this.rotated && event.deltaY != 0) {
+ minute += Math.ceil(event.deltaY / this.mPixPerMin);
+ }
+ }
+ this.scrollToMinute(minute);
+ }
+
+ // We are taking care of scrolling, so prevent the default
+ // action in any case.
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="scroll" phase="bubbling"><![CDATA[
+ let scrollbox = document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
+ let scrollBoxObject = scrollbox.boxObject;
+ if (scrollBoxObject && scrollbox.scrollHeight > 0) {
+ // We need to update the first visible minute, but only if the
+ // scrollbox has been sized.
+ let x = {}, y = {};
+ scrollBoxObject.getPosition(x, y);
+ if (scrollbox.getAttribute("orient") == "horizontal") {
+ this.mFirstVisibleMinute = Math.round(y.value / this.mPixPerMin);
+ } else {
+ this.mFirstVisibleMinute = Math.round(x.value / this.mPixPerMin);
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-statusbar.js b/calendar/base/content/calendar-statusbar.js
new file mode 100644
index 000000000..360afc5cb
--- /dev/null
+++ b/calendar/base/content/calendar-statusbar.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/* exported gCalendarStatusFeedback */
+
+/**
+ * This code might change soon if we support Thunderbird's activity manager.
+ * NOTE: The naming "Meteors" is historical.
+ */
+var gCalendarStatusFeedback = {
+ mCalendarStep: 0,
+ mCalendarCount: 0,
+ mWindow: null,
+ mStatusText: null,
+ mStatusBar: null,
+ mStatusProgressPanel: null,
+ mThrobber: null,
+ mProgressMode: Components.interfaces.calIStatusObserver.NO_PROGRESS,
+ mCurIndex: 0,
+ mInitialized: false,
+ mCalendars: {},
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIStatusObserver]),
+
+ initialize: function(aWindow) {
+ if (!this.mInitialized) {
+ this.mWindow = aWindow;
+ this.mStatusText = this.mWindow.document.getElementById("statusText");
+ this.mStatusBar = this.mWindow.document.getElementById("statusbar-icon");
+ this.mStatusProgressPanel = this.mWindow.document.getElementById("statusbar-progresspanel");
+ this.mThrobber = this.mWindow.document.getElementById("navigator-throbber");
+ this.mInitialized = true;
+ }
+ },
+
+ showStatusString: function(status) {
+ this.mStatusText.setAttribute("label", status);
+ },
+
+ get spinning() {
+ return this.mProgressMode;
+ },
+
+ startMeteors: function(aProgressMode, aCalendarCount) {
+ if (aProgressMode != Components.interfaces.calIStatusObserver.NO_PROGRESS) {
+ if (!this.mInitialized) {
+ Components.utils.reportError("StatusObserver has not been initialized!");
+ return;
+ }
+ this.mCalendars = {};
+ this.mCurIndex = 0;
+ if (aCalendarCount) {
+ this.mCalendarCount = this.mCalendarCount + aCalendarCount;
+ this.mCalendarStep = Math.trunc(100 / this.mCalendarCount);
+ }
+ this.mProgressMode = aProgressMode;
+ this.mStatusProgressPanel.removeAttribute("collapsed");
+ if (this.mProgressMode == Components.interfaces.calIStatusObserver.DETERMINED_PROGRESS) {
+ this.mStatusBar.removeAttribute("collapsed");
+ this.mStatusBar.setAttribute("mode", "determined");
+ this.mStatusBar.value = 0;
+ let commonStatus = calGetString("calendar", "gettingCalendarInfoCommon");
+ this.showStatusString(commonStatus);
+ }
+ if (this.mThrobber) {
+ this.mThrobber.setAttribute("busy", true);
+ }
+ }
+ },
+
+ stopMeteors: function() {
+ if (!this.mInitialized) {
+ return;
+ }
+ if (this.spinning != Components.interfaces.calIStatusObserver.NO_PROGRESS) {
+ this.mProgressMode = Components.interfaces.calIStatusObserver.NO_PROGRESS;
+ this.mStatusProgressPanel.collapsed = true;
+ this.mStatusBar.setAttribute("mode", "normal");
+ this.mStatusBar.value = 0;
+ this.mCalendarCount = 0;
+ this.showStatusString("");
+ if (this.mThrobber) {
+ this.mThrobber.setAttribute("busy", false);
+ }
+ }
+ },
+
+ calendarCompleted: function(aCalendar) {
+ if (!this.mInitialized) {
+ return;
+ }
+ if (this.spinning != Components.interfaces.calIStatusObserver.NO_PROGRESS) {
+ if (this.spinning == Components.interfaces.calIStatusObserver.DETERMINED_PROGRESS) {
+ if (!this.mCalendars[aCalendar.id] || this.mCalendars[aCalendar.id] === undefined) {
+ this.mCalendars[aCalendar.id] = true;
+ this.mStatusBar.value = parseInt(this.mStatusBar.value, 10) + this.mCalendarStep;
+ this.mCurIndex++;
+ let curStatus = calGetString("calendar", "gettingCalendarInfoDetail",
+ [this.mCurIndex, this.mCalendarCount]);
+ this.showStatusString(curStatus);
+ }
+ }
+ if (this.mThrobber) {
+ this.mThrobber.setAttribute("busy", true);
+ }
+ }
+ }
+};
diff --git a/calendar/base/content/calendar-task-editing.js b/calendar/base/content/calendar-task-editing.js
new file mode 100644
index 000000000..a2e84cb62
--- /dev/null
+++ b/calendar/base/content/calendar-task-editing.js
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Used by the "quick add" feature for tasks, for example in the task view or
+ * the uniinder-todo.
+ *
+ * NOTE: many of the following methods are called without taskEdit being the
+ * |this| object.
+ */
+
+var taskEdit = {
+ /**
+ * Get the currently observed calendar.
+ */
+ mObservedCalendar: null,
+ get observedCalendar() {
+ return this.mObservedCalendar;
+ },
+
+ /**
+ * Set the currently observed calendar, removing listeners to any old
+ * calendar set and adding listeners to the new one.
+ */
+ set observedCalendar(aCalendar) {
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.removeObserver(this.calendarObserver);
+ }
+
+ this.mObservedCalendar = aCalendar;
+
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.addObserver(this.calendarObserver);
+ }
+ return this.mObservedCalendar;
+ },
+
+ /**
+ * Helper function to set readonly and aria-disabled states and the value
+ * for a given target.
+ *
+ * @param aTarget The ID or XUL node to set the value
+ * @param aDisable A boolean if the target should be disabled.
+ * @param aValue The value that should be set on the target.
+ */
+ setupTaskField: function(aTarget, aDisable, aValue) {
+ aTarget.value = aValue;
+ setElementValue(aTarget, aDisable && "true", "readonly");
+ setElementValue(aTarget, aDisable && "true", "aria-disabled");
+ },
+
+ /**
+ * Handler function to call when the quick-add textbox gains focus.
+ *
+ * @param aEvent The DOM focus event
+ */
+ onFocus: function(aEvent) {
+ let edit = aEvent.target;
+ if (edit.localName == "input") {
+ // For some reason, we only receive an onfocus event for the textbox
+ // when debugging with venkman.
+ edit = edit.parentNode.parentNode;
+ }
+
+ let calendar = getSelectedCalendar();
+ edit.showsInstructions = true;
+
+ if (calendar.getProperty("capabilities.tasks.supported") === false) {
+ taskEdit.setupTaskField(edit,
+ true,
+ calGetString("calendar", "taskEditInstructionsCapability"));
+ } else if (isCalendarWritable(calendar)) {
+ edit.showsInstructions = false;
+ taskEdit.setupTaskField(edit, false, edit.savedValue || "");
+ } else {
+ taskEdit.setupTaskField(edit,
+ true,
+ calGetString("calendar", "taskEditInstructionsReadonly"));
+ }
+ },
+
+ /**
+ * Handler function to call when the quick-add textbox loses focus.
+ *
+ * @param aEvent The DOM blur event
+ */
+ onBlur: function(aEvent) {
+ let edit = aEvent.target;
+ if (edit.localName == "input") {
+ // For some reason, we only receive the blur event for the input
+ // element. There are no targets that point to the textbox. Go up
+ // the parent chain until we reach the textbox.
+ edit = edit.parentNode.parentNode;
+ }
+
+ let calendar = getSelectedCalendar();
+ if (!calendar) {
+ // this must be a first run, we don't have a calendar yet
+ return;
+ }
+
+ if (calendar.getProperty("capabilities.tasks.supported") === false) {
+ taskEdit.setupTaskField(edit,
+ true,
+ calGetString("calendar", "taskEditInstructionsCapability"));
+ } else if (isCalendarWritable(calendar)) {
+ if (!edit.showsInstructions) {
+ edit.savedValue = edit.value || "";
+ }
+ taskEdit.setupTaskField(edit,
+ false,
+ calGetString("calendar", "taskEditInstructions"));
+ } else {
+ taskEdit.setupTaskField(edit,
+ true,
+ calGetString("calendar", "taskEditInstructionsReadonly"));
+ }
+ edit.showsInstructions = true;
+ },
+
+ /**
+ * Handler function to call on keypress for the quick-add textbox.
+ *
+ * @param aEvent The DOM keypress event
+ */
+ onKeyPress: function(aEvent) {
+ if (aEvent.keyCode == Components.interfaces.nsIDOMKeyEvent.DOM_VK_RETURN) {
+ let edit = aEvent.target;
+ if (edit.value && edit.value.length > 0) {
+ let item = cal.createTodo();
+ setDefaultItemValues(item);
+ item.title = edit.value;
+
+ edit.value = "";
+ doTransaction("add", item, item.calendar, null, null);
+ }
+ }
+ },
+
+ /**
+ * Window load function to set up all quick-add textboxes. The texbox must
+ * have the class "task-edit-field".
+ */
+ onLoad: function(aEvent) {
+ window.removeEventListener("load", taskEdit.onLoad, false);
+ // TODO use getElementsByClassName
+ let taskEditFields = document.getElementsByAttribute("class", "task-edit-field");
+ for (let i = 0; i < taskEditFields.length; i++) {
+ taskEdit.onBlur({ target: taskEditFields[i] });
+ }
+
+ getCompositeCalendar().addObserver(taskEdit.compositeObserver);
+ taskEdit.observedCalendar = getSelectedCalendar();
+ },
+
+ /**
+ * Window load function to clean up all quick-add fields.
+ */
+ onUnload: function() {
+ getCompositeCalendar().removeObserver(taskEdit.compositeObserver);
+ taskEdit.observedCalendar = null;
+ },
+
+ /**
+ * Observer to watch for readonly, disabled and capability changes of the
+ * observed calendar.
+ *
+ * @see calIObserver
+ */
+ calendarObserver: {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIObserver]),
+
+ // calIObserver:
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onLoad: function(aCalendar) {},
+ onAddItem: function(aItem) {},
+ onModifyItem: function(aNewItem, aOldItem) {},
+ onDeleteItem: function(aDeletedItem) {},
+ onError: function(aCalendar, aErrNo, aMessage) {},
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ if (aCalendar.id != getSelectedCalendar().id) {
+ // Optimization: if the given calendar isn't the default calendar,
+ // then we don't need to change any readonly/disabled states.
+ return;
+ }
+ switch (aName) {
+ case "readOnly":
+ case "disabled": {
+ let taskEditFields = document.getElementsByAttribute("class", "task-edit-field");
+ for (let i = 0; i < taskEditFields.length; i++) {
+ taskEdit.onBlur({ target: taskEditFields[i] });
+ }
+ break;
+ }
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ // Since the old value is not used directly in onPropertyChanged,
+ // but should not be the same as the value, set it to a different
+ // value.
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ }
+ },
+
+ /**
+ * Observer to watch for changes to the selected calendar.
+ *
+ * XXX I think we don't need to implement calIObserver here.
+ *
+ * @see calICompositeObserver
+ */
+ compositeObserver: {
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calIObserver,
+ Components.interfaces.calICompositeObserver
+ ]),
+
+ // calIObserver:
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onLoad: function(aCalendar) {},
+ onAddItem: function(aItem) {},
+ onModifyItem: function(aNewItem, aOldItem) {},
+ onDeleteItem: function(aDeletedItem) {},
+ onError: function(aCalendar, aErrNo, aMessage) {},
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {},
+ onPropertyDeleting: function(aCalendar, aName) {},
+
+ // calICompositeObserver:
+ onCalendarAdded: function(aCalendar) {},
+ onCalendarRemoved: function(aCalendar) {},
+ onDefaultCalendarChanged: function(aNewDefault) {
+ let taskEditFields = document.getElementsByAttribute("class", "task-edit-field");
+ for (let i = 0; i < taskEditFields.length; i++) {
+ taskEdit.onBlur({ target: taskEditFields[i] });
+ }
+ taskEdit.observedCalendar = aNewDefault;
+ }
+ }
+};
+
+window.addEventListener("load", taskEdit.onLoad, false);
+window.addEventListener("unload", taskEdit.onUnload, false);
diff --git a/calendar/base/content/calendar-task-tree.js b/calendar/base/content/calendar-task-tree.js
new file mode 100644
index 000000000..fbfc89d7b
--- /dev/null
+++ b/calendar/base/content/calendar-task-tree.js
@@ -0,0 +1,312 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported addCalendarNames, calendars, changeContextMenuForTask,
+ * contextChangeTaskCalendar, contextChangeTaskPriority,
+ * contextPostponeTask, modifyTaskFromContext, deleteToDoCommand,
+ * tasksToMail, tasksToEvents, toggleCompleted,
+ */
+
+/**
+ * Add registered calendars to the given menupopup. Removes all previous
+ * children.
+ *
+ * XXX Either replace the existing items using replaceNode, or use helper
+ * functions (cal.removeChildren).
+ *
+ * @param aEvent The popupshowing event of the opening menu
+ */
+function addCalendarNames(aEvent) {
+ let calendarMenuPopup = aEvent.target;
+ while (calendarMenuPopup.hasChildNodes()) {
+ calendarMenuPopup.lastChild.remove();
+ }
+ let tasks = getSelectedTasks(aEvent);
+ let tasksSelected = (tasks.length > 0);
+ if (tasksSelected) {
+ let selIndex = appendCalendarItems(tasks[0], calendarMenuPopup, null, "contextChangeTaskCalendar(event);");
+ if (isPropertyValueSame(tasks, "calendar") && (selIndex > -1)) {
+ calendarMenuPopup.childNodes[selIndex].setAttribute("checked", "true");
+ }
+ }
+}
+
+/**
+ * Change the opening context menu for the selected tasks.
+ *
+ * @param aEvent The popupshowing event of the opening menu.
+ */
+function changeContextMenuForTask(aEvent) {
+ handleTaskContextMenuStateChange(aEvent);
+
+ let idnode = document.popupNode.id;
+ let items = getSelectedTasks(aEvent);
+ document.getElementById("task-context-menu-new").hidden =
+ (idnode == "unifinder-todo-tree");
+ document.getElementById("task-context-menu-modify").hidden =
+ (idnode == "unifinder-todo-tree");
+ document.getElementById("task-context-menu-new-todaypane").hidden =
+ (idnode == "calendar-task-tree");
+ document.getElementById("task-context-menu-modify-todaypane").hidden =
+ (idnode == "calendar-task-tree");
+ document.getElementById("task-context-menu-filter-todaypane").hidden =
+ (idnode == "calendar-task-tree");
+ document.getElementById("task-context-menu-separator-filter").hidden =
+ (idnode == "calendar-task-tree");
+
+ let tasksSelected = (items.length > 0);
+ applyAttributeToMenuChildren(aEvent.target, "disabled", (!tasksSelected));
+ if (calendarController.isCommandEnabled("calendar_new_todo_command") &&
+ calendarController.isCommandEnabled("calendar_new_todo_todaypane_command")) {
+ document.getElementById("calendar_new_todo_command").removeAttribute("disabled");
+ document.getElementById("calendar_new_todo_todaypane_command").removeAttribute("disabled");
+ } else {
+ document.getElementById("calendar_new_todo_command").setAttribute("disabled", "true");
+ document.getElementById("calendar_new_todo_todaypane_command").setAttribute("disabled", "true");
+ }
+
+ // make sure the "Paste" and "Cut" menu items are enabled
+ goUpdateCommand("cmd_paste");
+ goUpdateCommand("cmd_cut");
+
+ // make sure the filter menu is enabled
+ document.getElementById("task-context-menu-filter-todaypane").removeAttribute("disabled");
+ applyAttributeToMenuChildren(document.getElementById("task-context-menu-filter-todaypane-popup"),
+ "disabled", false);
+
+ changeMenuForTask(aEvent);
+
+ let menu = document.getElementById("task-context-menu-attendance-menu");
+ setupAttendanceMenu(menu, items);
+}
+
+/**
+ * Notify the task tree that the context menu open state has changed.
+ *
+ * @param aEvent The popupshowing or popuphiding event of the menu.
+ */
+function handleTaskContextMenuStateChange(aEvent) {
+ let tree = document.popupNode;
+
+ if (tree) {
+ tree.updateFocus();
+ }
+}
+
+/**
+ * Change the opening menu for the selected tasks.
+ *
+ * @param aEvent The popupshowing event of the opening menu.
+ */
+function changeMenuForTask(aEvent) {
+ // Make sure to update the status of some commands.
+ ["calendar_delete_todo_command",
+ "calendar_toggle_completed_command",
+ "calendar_general-progress_command",
+ "calendar_general-priority_command",
+ "calendar_general-postpone_command"].forEach(goUpdateCommand);
+
+ let tasks = getSelectedTasks(aEvent);
+ let tasksSelected = (tasks.length > 0);
+ if (tasksSelected) {
+ let cmd = document.getElementById("calendar_toggle_completed_command");
+ if (isPropertyValueSame(tasks, "isCompleted")) {
+ setBooleanAttribute(cmd, "checked", tasks[0].isCompleted);
+ } else {
+ setBooleanAttribute(cmd, "checked", false);
+ }
+ }
+}
+
+/**
+ * Handler function to change the progress of all selected tasks, or of
+ * the task loaded in the current tab.
+ *
+ * @param {XULCommandEvent} aEvent The DOM event that triggered this command
+ * @param {short} aProgress The new progress percentage
+ */
+function contextChangeTaskProgress(aEvent, aProgress) {
+ if (gTabmail && gTabmail.currentTabInfo.mode.type == "calendarTask") {
+ editToDoStatus(aProgress);
+ } else {
+ startBatchTransaction();
+ let tasks = getSelectedTasks(aEvent);
+ for (let task of tasks) {
+ let newTask = task.clone().QueryInterface(Components.interfaces.calITodo);
+ newTask.percentComplete = aProgress;
+ switch (aProgress) {
+ case 0:
+ newTask.isCompleted = false;
+ break;
+ case 100:
+ newTask.isCompleted = true;
+ break;
+ default:
+ newTask.status = "IN-PROCESS";
+ newTask.completedDate = null;
+ break;
+ }
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ endBatchTransaction();
+ }
+}
+
+/**
+ * Handler function to change the calendar of the selected tasks. The targeted
+ * menuitem must have "calendar" property that implements calICalendar.
+ *
+ * @param aEvent The DOM event that triggered this command.
+ */
+function contextChangeTaskCalendar(aEvent) {
+ startBatchTransaction();
+ let tasks = getSelectedTasks(aEvent);
+ for (let task of tasks) {
+ let newTask = task.clone();
+ newTask.calendar = aEvent.target.calendar;
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ endBatchTransaction();
+}
+
+/**
+ * Handler function to change the priority of the selected tasks, or of
+ * the task loaded in the current tab.
+ *
+ * @param {XULCommandEvent} aEvent The DOM event that triggered this command
+ * @param {short} aPriority The priority to set on the task(s)
+ */
+function contextChangeTaskPriority(aEvent, aPriority) {
+ let tabType = gTabmail && gTabmail.currentTabInfo.mode.type;
+ if (tabType == "calendarTask" || tabType == "calendarEvent") {
+ editConfigState({ priority: aPriority });
+ } else {
+ startBatchTransaction();
+ let tasks = getSelectedTasks(aEvent);
+ for (let task of tasks) {
+ let newTask = task.clone().QueryInterface(Components.interfaces.calITodo);
+ newTask.priority = aPriority;
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ endBatchTransaction();
+ }
+}
+
+/**
+ * Handler function to postpone the start and due dates of the selected
+ * tasks, or of the task loaded in the current tab. ISO 8601 format:
+ * "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We use this
+ * format intentionally instead of a calIDuration object because those
+ * objects cannot be serialized for message passing with iframes.)
+ *
+ * @param {XULCommandEvent} aEvent The DOM event that triggered this command
+ * @param {string} aDuration The duration to postpone in ISO 8601 format
+ */
+function contextPostponeTask(aEvent, aDuration) {
+ let duration = cal.createDuration(aDuration);
+ if (!duration) {
+ cal.LOG("[calendar-task-tree] Postpone Task - Invalid duration " + aDuration);
+ return;
+ }
+
+ if (gTabmail && gTabmail.currentTabInfo.mode.type == "calendarTask") {
+ postponeTask(aDuration);
+ } else {
+ startBatchTransaction();
+ let tasks = getSelectedTasks(aEvent);
+
+ tasks.forEach((task) => {
+ if (task.entryDate || task.dueDate) {
+ let newTask = task.clone();
+ cal.shiftItem(newTask, duration);
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ });
+
+ endBatchTransaction();
+ }
+}
+
+/**
+ * Modifies the selected tasks with the event dialog
+ *
+ * @param aEvent The DOM event that triggered this command.
+ * @param initialDate (optional) The initial date for new task datepickers
+ */
+function modifyTaskFromContext(aEvent, initialDate) {
+ let tasks = getSelectedTasks(aEvent);
+ for (let task of tasks) {
+ modifyEventWithDialog(task, null, true, initialDate);
+ }
+}
+
+/**
+ * Delete the current selected item with focus from the task tree
+ *
+ * @param aEvent The DOM event that triggered this command.
+ * @param aDoNotConfirm If true, the user will not be asked to delete.
+ */
+function deleteToDoCommand(aEvent, aDoNotConfirm) {
+ let tasks = getSelectedTasks(aEvent);
+ calendarViewController.deleteOccurrences(tasks.length,
+ tasks,
+ false,
+ aDoNotConfirm);
+}
+
+/**
+ * Gets the currently visible task tree
+ *
+ * @return The XUL task tree element.
+ */
+function getTaskTree() {
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ if (currentMode == "task") {
+ return document.getElementById("calendar-task-tree");
+ } else {
+ return document.getElementById("unifinder-todo-tree");
+ }
+}
+
+/**
+ * Gets the tasks selected in the currently visible task tree.
+ *
+ * XXX Parameter aEvent is unused, needs to be removed here and in calling
+ * functions.
+ *
+ * @param aEvent Unused
+ */
+function getSelectedTasks(aEvent) {
+ let taskTree = getTaskTree();
+ return taskTree ? taskTree.selectedTasks : [];
+}
+
+/**
+ * Convert selected tasks to emails.
+ */
+function tasksToMail(aEvent) {
+ let tasks = getSelectedTasks(aEvent);
+ calendarMailButtonDNDObserver.onDropItems(tasks);
+}
+
+/**
+ * Convert selected tasks to events.
+ */
+function tasksToEvents(aEvent) {
+ let tasks = getSelectedTasks(aEvent);
+ calendarCalendarButtonDNDObserver.onDropItems(tasks);
+}
+
+/**
+ * Toggle the completed state on selected tasks.
+ *
+ * @param aEvent The originating event, can be null.
+ */
+function toggleCompleted(aEvent) {
+ if (aEvent.target.getAttribute("checked") == "true") {
+ contextChangeTaskProgress(aEvent, 0);
+ } else {
+ contextChangeTaskProgress(aEvent, 100);
+ }
+}
diff --git a/calendar/base/content/calendar-task-tree.xml b/calendar/base/content/calendar-task-tree.xml
new file mode 100644
index 000000000..cb879b422
--- /dev/null
+++ b/calendar/base/content/calendar-task-tree.xml
@@ -0,0 +1,1195 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+]>
+
+<bindings id="calendar-task-tree-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="calendar-task-tree">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/calendar-task-tree.css"/>
+ </resources>
+ <content>
+ <xul:tree anonid="calendar-task-tree"
+ class="calendar-task-tree"
+ flex="1"
+ enableColumnDrag="false"
+ keepcurrentinview="true">
+ <xul:treecols anonid="calendar-task-tree-cols">
+ <xul:treecol anonid="calendar-task-tree-col-completed"
+ class="calendar-task-tree-col-completed"
+ minwidth="19"
+ fixed="true"
+ cycler="true"
+ sortKey="completedDate"
+ itemproperty="completed"
+ label="&calendar.unifinder.tree.done.label;"
+ tooltiptext="&calendar.unifinder.tree.done.tooltip2;">
+ <xul:image anonid="checkboximg" />
+ </xul:treecol>
+ <xul:splitter class="tree-splitter" ordinal="2"/>
+ <xul:treecol anonid="calendar-task-tree-col-priority"
+ class="calendar-task-tree-col-priority"
+ minwidth="17"
+ fixed="true"
+ itemproperty="priority"
+ label="&calendar.unifinder.tree.priority.label;"
+ tooltiptext="&calendar.unifinder.tree.priority.tooltip2;">
+ <xul:image anonid="priorityimg"/>
+ </xul:treecol>
+ <xul:splitter class="tree-splitter" ordinal="4"/>
+ <xul:treecol anonid="calendar-task-tree-col-title"
+ flex="1"
+ itemproperty="title"
+ label="&calendar.unifinder.tree.title.label;"
+ tooltiptext="&calendar.unifinder.tree.title.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="6"/>
+ <xul:treecol anonid="calendar-task-tree-col-entrydate"
+ itemproperty="entryDate"
+ flex="1"
+ label="&calendar.unifinder.tree.startdate.label;"
+ tooltiptext="&calendar.unifinder.tree.startdate.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="8"/>
+ <xul:treecol anonid="calendar-task-tree-col-duedate"
+ itemproperty="dueDate"
+ flex="1"
+ label="&calendar.unifinder.tree.duedate.label;"
+ tooltiptext="&calendar.unifinder.tree.duedate.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="10"/>
+ <xul:treecol anonid="calendar-task-tree-col-duration"
+ sortKey="dueDate"
+ itemproperty="duration"
+ flex="1"
+ label="&calendar.unifinder.tree.duration.label;"
+ tooltiptext="&calendar.unifinder.tree.duration.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="12"/>
+ <xul:treecol anonid="calendar-task-tree-col-completeddate"
+ itemproperty="completedDate"
+ flex="1"
+ label="&calendar.unifinder.tree.completeddate.label;"
+ tooltiptext="&calendar.unifinder.tree.completeddate.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="14"/>
+ <xul:treecol anonid="calendar-task-tree-col-percentcomplete"
+ flex="1"
+ type="progressmeter"
+ minwidth="19"
+ itemproperty="percentComplete"
+ label="&calendar.unifinder.tree.percentcomplete.label;"
+ tooltiptext="&calendar.unifinder.tree.percentcomplete.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="16"/>
+ <xul:treecol anonid="calendar-task-tree-col-categories"
+ itemproperty="categories"
+ flex="1"
+ label="&calendar.unifinder.tree.categories.label;"
+ tooltiptext="&calendar.unifinder.tree.categories.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="18"/>
+ <xul:treecol anonid="calendar-task-tree-col-location"
+ itemproperty="location"
+ label="&calendar.unifinder.tree.location.label;"
+ tooltiptext="&calendar.unifinder.tree.location.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="20"/>
+ <xul:treecol anonid="calendar-task-tree-col-status"
+ flex="1"
+ itemproperty="status"
+ label="&calendar.unifinder.tree.status.label;"
+ tooltiptext="&calendar.unifinder.tree.status.tooltip2;"/>
+ <xul:splitter class="tree-splitter" ordinal="22"/>
+ <xul:treecol anonid="calendar-task-tree-col-calendarname"
+ flex="1"
+ itemproperty="calendar"
+ label="&calendar.unifinder.tree.calendarname.label;"
+ tooltiptext="&calendar.unifinder.tree.calendarname.tooltip2;"/>
+ </xul:treecols>
+ <xul:treechildren tooltip="taskTreeTooltip" ondblclick="mTreeView.onDoubleClick(event)"/>
+ </xul:tree>
+ </content>
+
+ <implementation implements="nsIObserver">
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/PluralForm.jsm");
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://calendar/modules/calItemUtils.jsm");
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ // set up the tree filter
+ this.mFilter = new calFilter();
+
+ // set up the custom tree view
+ let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
+ this.mTreeView.tree = tree;
+ tree.view = this.mTreeView;
+
+ // set up our calendar event observer
+ let composite = getCompositeCalendar();
+ composite.addObserver(this.mTaskTreeObserver);
+
+ // set up the preference observer
+ let branch = Services.prefs.getBranch("");
+ branch.addObserver("calendar.", this, false);
+
+
+ // we want to make several attributes on the column
+ // elements persistent, but unfortunately there's no
+ // relyable way with the 'persist' feature.
+ // that's why we need to store the necessary bits and
+ // pieces at the element this binding is attached to.
+ let names = this.getAttribute("visible-columns").split(" ");
+ let ordinals = this.getAttribute("ordinals").split(" ");
+ let widths = this.getAttribute("widths").split(" ");
+ let sorted = this.getAttribute("sort-active");
+ let sortDirection = this.getAttribute("sort-direction") || "ascending";
+ tree = document.getAnonymousNodes(this)[0];
+ let treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
+ for (let i = 0; i < treecols.length; i++) {
+ let content = treecols[i].getAttribute("itemproperty");
+ if (names.some(element => element == content)) {
+ treecols[i].removeAttribute("hidden");
+ } else {
+ treecols[i].setAttribute("hidden", "true");
+ }
+ if (ordinals && ordinals.length > 0) {
+ treecols[i].ordinal = Number(ordinals.shift());
+ }
+ if (widths && widths.length > 0) {
+ treecols[i].width = Number(widths.shift());
+ }
+ if (sorted && sorted.length > 0) {
+ if (sorted == content) {
+ this.mTreeView.sortDirection = sortDirection;
+ this.mTreeView.selectedColumn = treecols[i];
+ }
+ }
+ }
+ ]]></constructor>
+ <destructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ // remove composite calendar observer
+ let composite = getCompositeCalendar();
+ composite.removeObserver(this.mTaskTreeObserver);
+
+ // remove the preference observer
+ let branch = Services.prefs.getBranch("");
+ branch.removeObserver("calendar.", this, false);
+
+ let widths = "";
+ let ordinals = "";
+ let visible = "";
+ let sorted = this.mTreeView.selectedColumn;
+ let tree = document.getAnonymousNodes(this)[0];
+ let treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
+ for (let i = 0; i < treecols.length; i++) {
+ if (treecols[i].getAttribute("hidden") != "true") {
+ let content = treecols[i].getAttribute("itemproperty");
+ visible += visible.length > 0 ? " " + content : content;
+ }
+ if (ordinals.length > 0) {
+ ordinals += " ";
+ }
+ ordinals += treecols[i].ordinal;
+ if (widths.length > 0) {
+ widths += " ";
+ }
+ widths += treecols[i].width || 0;
+ }
+ this.setAttribute("visible-columns", visible);
+ this.setAttribute("ordinals", ordinals);
+ this.setAttribute("widths", widths);
+ if (sorted) {
+ this.setAttribute("sort-active", sorted.getAttribute("itemproperty"));
+ this.setAttribute("sort-direction", this.mTreeView.sortDirection);
+ } else {
+ this.removeAttribute("sort-active");
+ this.removeAttribute("sort-direction");
+ }
+ ]]></destructor>
+
+ <field name="mTaskArray">[]</field>
+ <field name="mHash2Index"><![CDATA[({})]]></field>
+ <field name="mPendingRefreshJobs"><![CDATA[({})]]></field>
+ <field name="mShowCompletedTasks">true</field>
+ <field name="mFilter">null</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mDateRangeFilter">null</field>
+ <field name="mTextFilterField">null</field>
+
+ <property name="currentIndex">
+ <getter><![CDATA[
+ let tree = document.getAnonymousElementByAttribute(
+ this, "anonid", "calendar-task-tree");
+ return tree.currentIndex;
+ ]]></getter>
+ </property>
+
+ <property name="currentTask">
+ <getter><![CDATA[
+ let tree = document.getAnonymousElementByAttribute(
+ this, "anonid", "calendar-task-tree");
+ let index = tree.currentIndex;
+ if (tree.view && tree.view.selection) {
+ // If the current index is not selected, then ignore
+ index = (tree.view.selection.isSelected(index) ? index : -1);
+ }
+ return index < 0 ? null : this.mTaskArray[index];
+ ]]></getter>
+ </property>
+
+ <property name="selectedTasks" readonly="true">
+ <getter><![CDATA[
+ let tasks = [];
+ let start = {};
+ let end = {};
+ if (!this.mTreeView.selection) {
+ return tasks;
+ }
+
+ let rangeCount = this.mTreeView.selection.getRangeCount();
+ for (let range = 0; range < rangeCount; range++) {
+ this.mTreeView.selection.getRangeAt(range, start, end);
+ for (let i = start.value; i <= end.value; i++) {
+ let task = this.getTaskAtRow(i);
+ if (task) {
+ tasks.push(this.getTaskAtRow(i));
+ }
+ }
+ }
+ return tasks;
+ ]]></getter>
+ </property>
+
+ <property name="showCompleted">
+ <getter><![CDATA[
+ return this.mShowCompletedTasks;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mShowCompletedTasks = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="textFilterField">
+ <getter><![CDATA[
+ return this.mTextFilterField;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mTextFilterField = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="duration">
+ <parameter name="aTask"/>
+ <body><![CDATA[
+ if (aTask && aTask.dueDate && aTask.dueDate.isValid) {
+ let dur = aTask.dueDate.subtractDate(cal.now());
+ if (!dur.isNegative) {
+ let minutes = Math.ceil(dur.inSeconds / 60);
+ if (minutes >= 1440) { // 1 day or more
+ let dueIn = PluralForm.get(dur.days, calGetString("calendar", "dueInDays"));
+ return dueIn.replace("#1", dur.days);
+ } else if (minutes >= 60) { // 1 hour or more
+ let dueIn = PluralForm.get(dur.hours, calGetString("calendar", "dueInHours"));
+ return dueIn.replace("#1", dur.hours);
+ } else {
+ // Less than one hour
+ return calGetString("calendar", "dueInLessThanOneHour");
+ }
+ } else if (!aTask.completedDate || !aTask.completedDate.isValid) {
+ // Overdue task
+ let minutes = Math.ceil(-dur.inSeconds / 60);
+ if (minutes >= 1440) { // 1 day or more
+ let dueIn = PluralForm.get(dur.days, calGetString("calendar", "dueInDays"));
+ return "-" + dueIn.replace("#1", dur.days);
+ } else if (minutes >= 60) { // 1 hour or more
+ let dueIn = PluralForm.get(dur.hours, calGetString("calendar", "dueInHours"));
+ return "-" + dueIn.replace("#1", dur.hours);
+ } else {
+ // Less than one hour
+ return calGetString("calendar", "dueInLessThanOneHour");
+ }
+ }
+ }
+ // No due date specified
+ return null;
+ ]]></body>
+ </method>
+ <method name="getTaskAtRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return (aRow > -1 ? this.mTaskArray[aRow] : null);
+ ]]></body>
+ </method>
+
+ <method name="getTaskFromEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ return this.mTreeView._getItemFromEvent(aEvent);
+ ]]></body>
+ </method>
+
+ <field name="mTreeView"><![CDATA[
+ ({
+ /**
+ * Attributes
+ */
+
+ // back reference to the binding
+ binding: this,
+ tree: null,
+ treebox: null,
+ mSelectedColumn: null,
+ sortDirection: null,
+
+ get selectedColumn() {
+ return this.mSelectedColumn;
+ },
+
+ set selectedColumn(aCol) {
+ let tree = document.getAnonymousNodes(this.binding)[0];
+ let treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
+ for (let i = 0; i < treecols.length; i++) {
+ let col = treecols[i];
+ if (col.getAttribute("sortActive")) {
+ col.removeAttribute("sortActive");
+ col.removeAttribute("sortDirection");
+ }
+ if (aCol.getAttribute("itemproperty") == col.getAttribute("itemproperty")) {
+ col.setAttribute("sortActive", "true");
+ col.setAttribute("sortDirection", this.sortDirection);
+ }
+ }
+ return (this.mSelectedColumn = aCol);
+ },
+
+ /**
+ * High-level task tree manipulation
+ */
+
+ // Adds an array of items to the list if they match the currently applied filter.
+ addItems: function(aItems, aDontSort) {
+ this.modifyItems(aItems, [], aDontSort, true);
+ },
+
+ // Removes an array of items from the list.
+ removeItems: function(aItems) {
+ this.modifyItems([], aItems, true, false);
+ },
+
+ // Removes an array of old items from the list, and adds an array of new items if
+ // they match the currently applied filter.
+ modifyItems: function(aNewItems, aOldItems, aDontSort, aSelectNew) {
+ let selItem = this.binding.currentTask;
+ let selIndex = this.tree.currentIndex;
+ let firstHash = null;
+ let remIndexes = [];
+ aNewItems = aNewItems || [];
+ aOldItems = aOldItems || [];
+
+ this.treebox.beginUpdateBatch();
+
+ let idiff = new itemDiff();
+ idiff.load(aOldItems);
+ idiff.difference(aNewItems);
+ idiff.complete();
+ let delItems = idiff.deletedItems;
+ let addItems = idiff.addedItems;
+ let modItems = idiff.modifiedItems;
+
+ // find the indexes of the old items that need to be removed
+ for (let item of delItems.mArray) {
+ if (item.hashId in this.binding.mHash2Index) {
+ // the old item needs to be removed
+ remIndexes.push(this.binding.mHash2Index[item.hashId]);
+ delete this.binding.mHash2Index[item.hashId];
+ }
+ }
+
+ // modified items need to be updated
+ for (let item of modItems.mArray) {
+ if (item.hashId in this.binding.mHash2Index) {
+ // make sure we're using the new version of a modified item
+ this.binding.mTaskArray[this.binding.mHash2Index[item.hashId]] = item;
+ }
+ }
+
+ // remove the old items working backward from the end so the indexes stay valid
+ remIndexes.sort((a, b) => b - a).forEach((index) => {
+ this.binding.mTaskArray.splice(index, 1);
+ this.treebox.rowCountChanged(index, -1);
+ });
+
+ // add the new items
+ for (let item of addItems.mArray) {
+ if (!(item.hashId in this.binding.mHash2Index)) {
+ let index = this.binding.mTaskArray.length;
+ this.binding.mTaskArray.push(item);
+ this.binding.mHash2Index[item.hashId] = index;
+ this.treebox.rowCountChanged(index, 1);
+ firstHash = firstHash || item.hashId;
+ }
+ }
+
+ if (aDontSort) {
+ this.binding.recreateHashTable();
+ } else {
+ this.binding.sortItems();
+ }
+
+ if (aSelectNew && firstHash && firstHash in this.binding.mHash2Index) {
+ // select the first item added into the list
+ selIndex = this.binding.mHash2Index[firstHash];
+ } else if (selItem && selItem.hashId in this.binding.mHash2Index) {
+ // select the previously selected item
+ selIndex = this.binding.mHash2Index[selItem.hashId];
+ } else if (selIndex >= this.binding.mTaskArray.length) {
+ // make sure the previously selected index is valid
+ selIndex = this.binding.mTaskArray.length - 1;
+ }
+
+ if (selIndex > -1) {
+ this.tree.view.selection.select(selIndex);
+ this.treebox.ensureRowIsVisible(selIndex);
+ }
+
+ this.treebox.endUpdateBatch();
+ },
+
+ clear: function() {
+ let count = this.binding.mTaskArray.length;
+ if (count > 0) {
+ this.binding.mTaskArray = [];
+ this.binding.mHash2Index = {};
+ this.treebox.rowCountChanged(0, -count);
+ this.tree.view.selection.clearSelection();
+ }
+ },
+
+ updateItem: function(aItem) {
+ let index = this.binding.mHash2Index[aItem.hashId];
+ if (index) {
+ this.treebox.invalidateRow(index);
+ }
+ },
+
+ /**
+ * nsITreeView methods and properties
+ */
+
+ get rowCount() {
+ return this.binding.mTaskArray.length;
+ },
+
+ // TODO this code is currently identical to the unifinder. We should
+ // create an itemTreeView that these tree views can inherit, that
+ // contains this code, and possibly other code related to sorting and
+ // storing items. See bug 432582 for more details.
+ getCellProperties: function(aRow, aCol) {
+ let rowProps = this.getRowProperties(aRow);
+ let colProps = this.getColumnProperties(aCol);
+ return rowProps + (rowProps && colProps ? " " : "") + colProps;
+ },
+
+ // Called to get properties to paint a column background.
+ // For shading the sort column, etc.
+ getColumnProperties: function(aCol) {
+ return aCol.element.getAttribute("anonid") || "";
+ },
+
+ getRowProperties: function(aRow) {
+ let properties = [];
+ let item = this.binding.mTaskArray[aRow];
+ if (item.priority > 0 && item.priority < 5) {
+ properties.push("highpriority");
+ } else if (item.priority > 5 && item.priority < 10) {
+ properties.push("lowpriority");
+ }
+ properties.push(getProgressAtom(item));
+
+ // Add calendar name and id atom
+ properties.push("calendar-" + formatStringForCSSRule(item.calendar.name));
+ properties.push("calendarid-" + formatStringForCSSRule(item.calendar.id));
+
+ // Add item status atom
+ if (item.status) {
+ properties.push("status-" + item.status.toLowerCase());
+ }
+
+ // Alarm status atom
+ if (item.getAlarms({}).length) {
+ properties.push("alarm");
+ }
+
+ // Task categories
+ properties = properties.concat(item.getCategories({})
+ .map(formatStringForCSSRule));
+
+ return properties.join(" ");
+ },
+
+ // Called on the view when a cell in a non-selectable cycling
+ // column (e.g., unread/flag/etc.) is clicked.
+ cycleCell: function(aRow, aCol) {
+ let task = this.binding.mTaskArray[aRow];
+
+ // prevent toggling completed status for parent items of
+ // repeating tasks or when the calendar is read-only.
+ if (!task || task.recurrenceInfo || task.calendar.readOnly) {
+ return;
+ }
+ if (aCol != null) {
+ let content = aCol.element.getAttribute("itemproperty");
+ if (content == "completed") {
+ let newTask = task.clone().QueryInterface(Components.interfaces.calITodo);
+ newTask.isCompleted = !task.completedDate;
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ }
+ },
+
+ // Called on the view when a header is clicked.
+ cycleHeader: function(aCol) {
+ if (!this.selectedColumn) {
+ this.sortDirection = "ascending";
+ } else if (!this.sortDirection || this.sortDirection == "descending") {
+ this.sortDirection = "ascending";
+ } else {
+ this.sortDirection = "descending";
+ }
+ this.selectedColumn = aCol.element;
+ let selectedItems = this.binding.selectedTasks;
+ this.binding.sortItems();
+ if (selectedItems != undefined) {
+ this.tree.view.selection.clearSelection();
+ for (let item of selectedItems) {
+ let index = this.binding.mHash2Index[item.hashId];
+ this.tree.view.selection.toggleSelect(index);
+ }
+ }
+ },
+
+ // The text for a given cell. If a column consists only of an
+ // image, then the empty string is returned.
+ getCellText: function(aRow, aCol) {
+ let task = this.binding.mTaskArray[aRow];
+ if (!task) {
+ return false;
+ }
+
+ switch (aCol.element.getAttribute("itemproperty")) {
+ case "title":
+ // return title, or "Untitled" if empty/null
+ return (task.title ? task.title.replace(/\n/g, " ") : calGetString("calendar", "eventUntitled"));
+ case "entryDate":
+ return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.entryDate);
+ case "dueDate":
+ return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.dueDate);
+ case "completedDate":
+ return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.completedDate);
+ case "percentComplete":
+ return (task.percentComplete > 0 ? task.percentComplete + "%" : "");
+ case "categories":
+ return task.getCategories({}).join(", "); // TODO l10n-unfriendly
+ case "location":
+ return task.getProperty("LOCATION");
+ case "status":
+ return getToDoStatusString(task);
+ case "calendar":
+ return task.calendar.name;
+ case "duration":
+ return this.binding.duration(task);
+ case "completed":
+ case "priority":
+ default:
+ return "";
+ }
+ },
+
+ // This method is only called for columns of type other than text.
+ getCellValue: function(aRow, aCol) {
+ let task = this.binding.mTaskArray[aRow];
+ if (!task) {
+ return null;
+ }
+ switch (aCol.element.getAttribute("itemproperty")) {
+ case "percentComplete":
+ return task.percentComplete;
+ }
+ return null;
+ },
+
+ // SetCellValue is called when the value of the cell has been set by the user.
+ // This method is only called for columns of type other than text.
+ setCellValue: function(aRow, aCol, aValue) {
+ return null;
+ },
+
+ // The image path for a given cell. For defining an icon for a cell.
+ // If the empty string is returned, the :moz-tree-image pseudoelement will be used.
+ getImageSrc: function(aRow, aCol) {
+ // Return the empty string in order
+ // to use moz-tree-image pseudoelement :
+ // it is mandatory to return "" and not false :-(
+ return "";
+ },
+
+ // IsEditable is called to ask the view if the cell contents are editable.
+ // A value of true will result in the tree popping up a text field when the user
+ // tries to inline edit the cell.
+ isEditable: function(aRow, aCol) {
+ return true;
+ },
+
+ // Called during initialization to link the view to the front end box object.
+ setTree: function(aTreeBox) {
+ this.treebox = aTreeBox;
+ },
+
+ // Methods that can be used to test whether or not a twisty should
+ // be drawn, and if so, whether an open or closed twisty should be used.
+ isContainer: function(aRow) {
+ return false;
+ },
+ isContainerOpen: function(aRow) {
+ return false;
+ },
+ isContainerEmpty: function(aRow) {
+ return false;
+ },
+
+ // IsSeparator is used to determine if the row at index is a separator.
+ // A value of true will result in the tree drawing a horizontal separator.
+ // The tree uses the ::moz-tree-separator pseudoclass to draw the separator.
+ isSeparator: function(aRow) {
+ return false;
+ },
+
+ // Specifies if there is currently a sort on any column.
+ // Used mostly by drag'n'drop to affect drop feedback.
+ isSorted: function(aRow) {
+ return false;
+ },
+
+ canDrop: function() { return false; },
+
+ drop: function(aRow, aOrientation) {},
+
+ getParentIndex: function(aRow) {
+ return -1;
+ },
+
+ // The level is an integer value that represents the level of indentation.
+ // It is multiplied by the width specified in the :moz-tree-indentation
+ // pseudoelement to compute the exact indendation.
+ getLevel: function(aRow) {
+ return 0;
+ },
+
+ // The image path for a given cell. For defining an icon for a cell.
+ // If the empty string is returned, the :moz-tree-image pseudoelement
+ // will be used.
+ getImgSrc: function(aRow, aCol) {
+ return null;
+ },
+
+ // The progress mode for a given cell. This method is only called for
+ // columns of type |progressmeter|.
+ getProgressMode: function(aRow, aCol) {
+ switch (aCol.element.getAttribute("itemproperty")) {
+ case "percentComplete": {
+ let task = this.binding.mTaskArray[aRow];
+ if (aCol.element.boxObject.width > 75 &&
+ task.percentComplete > 0) {
+ // XXX Would be nice if we could use relative widths,
+ // i.e "15ex", but there is no scriptable interface.
+ return Components.interfaces.nsITreeView.PROGRESS_NORMAL;
+ }
+ break;
+ }
+ }
+
+ return Components.interfaces.nsITreeView.PROGRESS_NONE;
+ },
+
+ /**
+ * Task Tree Events
+ */
+ onSelect: function(event) {},
+
+ onDoubleClick: function(event) {
+ if (event.button == 0) {
+ let initialDate = getDefaultStartDate(this.binding.getInitialDate());
+ let col = {};
+ let item = this._getItemFromEvent(event, col);
+ if (item) {
+ let colAnonId = col.value.element.getAttribute("itemproperty");
+ if (colAnonId == "completed") {
+ // item holds checkbox state toggled by first click,
+ // so don't call modifyEventWithDialog
+ // to make sure user notices state changed.
+ } else {
+ modifyEventWithDialog(item, null, true, initialDate);
+ }
+ } else {
+ createTodoWithDialog(null, null, null, null, initialDate);
+ }
+ }
+ },
+
+ onKeyPress: function(event) {
+ switch (event.key) {
+ case "Delete": {
+ document.popupNode = this.binding;
+ document.getElementById("calendar_delete_todo_command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ break;
+ }
+ case " ": {
+ if (this.tree.currentIndex > -1) {
+ let col = document.getAnonymousElementByAttribute(
+ this.binding, "itemproperty", "completed");
+ this.cycleCell(
+ this.tree.currentIndex,
+ { element: col });
+ }
+ break;
+ }
+ case "Enter": {
+ let index = this.tree.currentIndex;
+ if (index > -1) {
+ modifyEventWithDialog(this.binding.mTaskArray[index]);
+ }
+ break;
+ }
+ }
+ },
+
+ // Set the context menu on mousedown to change it before it is opened
+ onMouseDown: function(event) {
+ let tree = document.getAnonymousElementByAttribute(this.binding,
+ "anonid",
+ "calendar-task-tree");
+
+ if (!this._getItemFromEvent(event)) {
+ tree.view.selection.invalidateSelection();
+ }
+ },
+
+ /**
+ * Private methods and attributes
+ */
+
+ _getItemFromEvent: function(event, aCol, aRow) {
+ aRow = aRow || {};
+ let childElt = {};
+ this.treebox.getCellAt(event.clientX, event.clientY, aRow, aCol || {}, childElt);
+ if (!childElt.value) {
+ return false;
+ }
+ return aRow && aRow.value > -1 && this.binding.mTaskArray[aRow.value];
+ },
+
+ // Helper function to display datetimes
+ _formatDateTime: function(aDateTime) {
+ let dateFormatter = Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+
+ // datetime is from todo object, it is not a javascript date
+ if (aDateTime && aDateTime.isValid) {
+ let dateTime = aDateTime.getInTimezone(calendarDefaultTimezone());
+ return dateFormatter.formatDateTime(dateTime);
+ }
+ return "";
+ }
+ })
+ ]]></field>
+
+ <!--
+ Observer for the calendar event data source. This keeps the unifinder
+ display up to date when the calendar event data is changed
+ -->
+ <field name="mTaskTreeObserver"><![CDATA[
+ ({
+ binding: this,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calICompositeObserver,
+ Components.interfaces.calIObserver
+ ]),
+
+ /**
+ * calIObserver methods and properties
+ */
+ onStartBatch: function() {
+ },
+
+ onEndBatch: function() {
+ },
+
+ onLoad: function() {
+ this.binding.refresh();
+ },
+
+ onAddItem: function(aItem) {
+ if (cal.isToDo(aItem)) {
+ this.binding.mTreeView.addItems(this.binding.mFilter.getOccurrences(aItem));
+ }
+ },
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (cal.isToDo(aNewItem) || cal.isToDo(aOldItem)) {
+ this.binding.mTreeView.modifyItems(this.binding.mFilter.getOccurrences(aNewItem),
+ this.binding.mFilter.getOccurrences(aOldItem));
+
+ // we also need to notify potential listeners.
+ let event = document.createEvent("Events");
+ event.initEvent("select", true, false);
+ this.binding.dispatchEvent(event);
+ }
+ },
+
+ onDeleteItem: function(aDeletedItem) {
+ if (cal.isToDo(aDeletedItem)) {
+ this.binding.mTreeView.removeItems(this.binding.mFilter.getOccurrences(aDeletedItem));
+ }
+ },
+
+ onError: function(aCalendar, aErrNo, aMessage) {},
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "disabled":
+ if (aValue) {
+ this.binding.onCalendarRemoved(aCalendar);
+ } else {
+ this.binding.onCalendarAdded(aCalendar);
+ }
+ break;
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ },
+
+ /**
+ * calICompositeObserver methods and properties
+ */
+ onCalendarAdded: function(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.binding.onCalendarAdded(aCalendar);
+ }
+ },
+
+ onCalendarRemoved: function(aCalendar) {
+ this.binding.onCalendarRemoved(aCalendar);
+ },
+
+ onDefaultCalendarChanged: function(aNewDefaultCalendar) {}
+ })
+ ]]></field>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aPrefName"/>
+ <body><![CDATA[
+ switch (aPrefName) {
+ case "calendar.date.format":
+ case "calendar.timezone.local":
+ this.refresh();
+ break;
+ }
+
+ ]]></body>
+ </method>
+
+ <method name="refreshFromCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let refreshJob = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ binding: this,
+ calendar: null,
+ items: null,
+ operation: null,
+
+ onOperationComplete: function(aOpCalendar, aStatus, aOperationType, aId, aDateTime) {
+ if (aOpCalendar.id in this.binding.mPendingRefreshJobs) {
+ delete this.binding.mPendingRefreshJobs[aOpCalendar.id];
+ }
+
+ let oldItems = this.binding.mTaskArray.filter(item => item.calendar.id == aOpCalendar.id);
+ this.binding.mTreeView.modifyItems(this.items, oldItems);
+ },
+
+ onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ this.items = this.items.concat(aItems);
+ },
+
+ cancel: function() {
+ if (this.operation && this.operation.isPending) {
+ this.operation.cancel();
+ this.operation = null;
+ this.items = [];
+ }
+ },
+
+ execute: function() {
+ if (aCalendar.id in this.binding.mPendingRefreshJobs) {
+ this.binding.mPendingRefreshJobs[aCalendar.id].cancel();
+ }
+ this.calendar = aCalendar;
+ this.items = [];
+
+ let operation = this.binding.mFilter.getItems(aCalendar,
+ aCalendar.ITEM_FILTER_TYPE_TODO,
+ this);
+ if (operation && operation.isPending) {
+ this.operation = operation;
+ this.binding.mPendingRefreshJobs[aCalendar.id] = this;
+ }
+ }
+ };
+
+ refreshJob.execute();
+ ]]></body>
+ </method>
+
+ <method name="selectAll">
+ <body><![CDATA[
+ if (this.mTreeView.selection) {
+ this.mTreeView.selection.selectAll();
+ }
+ ]]></body>
+ </method>
+
+ <!-- Called by event observers to update the display -->
+ <method name="refresh">
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ let cals = getCompositeCalendar().getCalendars({}) || [];
+ for (let calendar of cals) {
+ if (!calendar.getProperty("disabled")) {
+ this.refreshFromCalendar(calendar, aFilter);
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="onCalendarAdded">
+ <parameter name="aCalendar"/>
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ if (!aCalendar.getProperty("disabled")) {
+ this.refreshFromCalendar(aCalendar, aFilter);
+ }
+ ]]></body>
+ </method>
+
+ <method name="onCalendarRemoved">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let tasks = this.mTaskArray.filter(task => task.calendar.id == aCalendar.id);
+ this.mTreeView.removeItems(tasks);
+ ]]></body>
+ </method>
+
+ <method name="sortItems">
+ <body><![CDATA[
+ if (this.mTreeView.selectedColumn) {
+ let modifier = (this.mTreeView.sortDirection == "descending" ? -1 : 1);
+ let column = this.mTreeView.selectedColumn;
+ cal.sortEntry.mSortKey = column.getAttribute("sortKey")
+ ? column.getAttribute("sortKey")
+ : column.getAttribute("itemproperty");
+ let sortType = cal.getSortTypeForSortKey(cal.sortEntry.mSortKey);
+
+ // sort (key,item) entries
+ cal.sortEntry.mSortStartedDate = now();
+ let entries = this.mTaskArray.map(cal.sortEntry, cal.sortEntry);
+ entries.sort(cal.sortEntryComparer(sortType, modifier));
+ this.mTaskArray = entries.map(cal.sortEntryItem);
+ }
+
+ this.recreateHashTable();
+ ]]></body>
+ </method>
+
+ <method name="recreateHashTable">
+ <body><![CDATA[
+ this.mHash2Index = {};
+ for (let i = 0; i < this.mTaskArray.length; i++) {
+ let item = this.mTaskArray[i];
+ this.mHash2Index[item.hashId] = i;
+ }
+ if (this.mTreeView.treebox) {
+ this.mTreeView.treebox.invalidate();
+ }
+ ]]></body>
+ </method>
+
+ <method name="getInitialDate">
+ <body><![CDATA[
+ let initialDate = currentView().selectedDay;
+ return initialDate ? initialDate : now();
+ ]]></body>
+ </method>
+
+ <method name="doUpdateFilter">
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ let needsRefresh = false;
+ let oldStart = this.mFilter.mStartDate;
+ let oldEnd = this.mFilter.mEndDate;
+ let filterText = this.mFilter.filterText || "";
+
+ if (aFilter) {
+ let props = this.mFilter.filterProperties;
+ this.mFilter.applyFilter(aFilter);
+ needsRefresh = !props || !props.equals(this.mFilter.filterProperties);
+ } else {
+ this.mFilter.updateFilterDates();
+ }
+
+ if (this.mTextFilterField) {
+ let field = document.getElementById(this.mTextFilterField);
+ if (field) {
+ this.mFilter.filterText = field.value;
+ needsRefresh = needsRefresh || filterText.toLowerCase() != this.mFilter.filterText.toLowerCase();
+ }
+ }
+
+ // we only need to refresh the tree if the filter properties or date range changed
+ if (needsRefresh ||
+ !((!oldStart && !this.mFilter.mStartDate) ||
+ (oldStart && this.mFilter.mStartDate && oldStart.compare(this.mFilter.mStartDate) == 0)) ||
+ !((!oldEnd && !this.mFilter.mEndDate) ||
+ (oldEnd && this.mFilter.mEndDate && oldEnd.compare(this.mFilter.mEndDate) == 0))) {
+ this.refresh();
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateFilter">
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ this.doUpdateFilter(aFilter);
+ ]]></body>
+ </method>
+
+ <method name="updateFocus">
+ <body><![CDATA[
+ let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
+ let menuOpen = false;
+
+ // we need to consider the tree focused if the context menu is open.
+ if (this.hasAttribute("context")) {
+ let context = document.getElementById(this.getAttribute("context"));
+ if (context && context.state) {
+ menuOpen = (context.state == "open") || (context.state == "showing");
+ }
+ }
+
+ let focused = (document.activeElement == tree) || menuOpen;
+
+ calendarController.onSelectionChanged({ detail: focused ? this.selectedTasks : [] });
+ calendarController.todo_tasktree_focused = focused;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="select"><![CDATA[
+ this.mTreeView.onSelect(event);
+ if (calendarController.todo_tasktree_focused) {
+ calendarController.onSelectionChanged({ detail: this.selectedTasks });
+ }
+ ]]></handler>
+ <handler event="focus"><![CDATA[
+ this.updateFocus();
+ ]]></handler>
+ <handler event="blur"><![CDATA[
+ this.updateFocus();
+ ]]></handler>
+ <handler event="keypress"><![CDATA[
+ this.mTreeView.onKeyPress(event);
+ ]]></handler>
+ <handler event="mousedown"><![CDATA[
+ this.mTreeView.onMouseDown(event);
+ ]]></handler>
+ <handler event="dragstart"><![CDATA[
+ if (event.originalTarget.localName != "treechildren") {
+ // We should only drag treechildren, not for example the scrollbar.
+ return;
+ }
+ let item = this.mTreeView._getItemFromEvent(event);
+ if (!item || item.calendar.readOnly) {
+ return;
+ }
+
+ let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
+
+ // let's build the drag region
+ let region = null;
+ try {
+ region = Components.classes["@mozilla.org/gfx/region;1"].createInstance(Components.interfaces.nsIScriptableRegion);
+ region.init();
+ let treeBox = tree.treeBox;
+ let bodyBox = treeBox.treeBody.boxObject;
+ let sel = tree.view.selection;
+
+ let rowX = bodyBox.x;
+ let rowY = bodyBox.y;
+ let rowHeight = treeBox.rowHeight;
+ let rowWidth = bodyBox.width;
+
+ // add a rectangle for each visible selected row
+ for (let i = treeBox.getFirstVisibleRow(); i <= treeBox.getLastVisibleRow(); i++) {
+ if (sel.isSelected(i)) {
+ region.unionRect(rowX, rowY, rowWidth, rowHeight);
+ }
+ rowY = rowY + rowHeight;
+ }
+
+ // and finally, clip the result to be sure we don't spill over...
+ if (!region.isEmpty()) {
+ region.intersectRect(bodyBox.x, bodyBox.y, bodyBox.width, bodyBox.height);
+ }
+ } catch (ex) {
+ ASSERT(false, "Error while building selection region: " + ex + "\n");
+ region = null;
+ }
+ invokeEventDragSession(item, event.target);
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="calendar-task-tree-todaypane" extends="chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree">
+ <implementation>
+ <method name="getInitialDate">
+ <body><![CDATA[
+ let initialDate = agendaListbox.today ? agendaListbox.today.start : now();
+ return initialDate ? initialDate : now();
+ ]]></body>
+ </method>
+ <method name="updateFilter">
+ <parameter name="aFilter"/>
+ <body><![CDATA[
+ this.mFilter.selectedDate = agendaListbox.today && agendaListbox.today.start ?
+ agendaListbox.today.start : now();
+ this.doUpdateFilter(aFilter);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-task-view.js b/calendar/base/content/calendar-task-view.js
new file mode 100644
index 000000000..988ba043a
--- /dev/null
+++ b/calendar/base/content/calendar-task-view.js
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported taskDetailsView, sendMailToOrganizer, taskViewCopyLink */
+
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+var taskDetailsView = {
+
+ /**
+ * Task Details Events
+ *
+ * XXXberend Please document this function, possibly also consolidate since
+ * its the only function in taskDetailsView.
+ */
+ onSelect: function(event) {
+ function displayElement(id, flag) {
+ setBooleanAttribute(id, "hidden", !flag);
+ return flag;
+ }
+
+ let dateFormatter =
+ Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+
+ let item = document.getElementById("calendar-task-tree").currentTask;
+ if (displayElement("calendar-task-details-container", item != null) &&
+ displayElement("calendar-task-view-splitter", item != null)) {
+ displayElement("calendar-task-details-title-row", true);
+ document.getElementById("calendar-task-details-title").textContent =
+ (item.title ? item.title.replace(/\n/g, " ") : "");
+
+ let organizer = item.organizer;
+ if (displayElement("calendar-task-details-organizer-row", organizer != null)) {
+ let name = organizer.commonName;
+ if (!name || name.length <= 0) {
+ if (organizer.id && organizer.id.length) {
+ name = organizer.id;
+ let re = new RegExp("^mailto:(.*)", "i");
+ let matches = re.exec(name);
+ if (matches) {
+ name = matches[1];
+ }
+ }
+ }
+ if (displayElement("calendar-task-details-organizer-row", name && name.length)) {
+ document.getElementById("calendar-task-details-organizer").value = name;
+ }
+ }
+
+ let priority = 0;
+ if (item.calendar.getProperty("capabilities.priority.supported") != false) {
+ priority = parseInt(item.priority, 10);
+ }
+ displayElement("calendar-task-details-priority-label", priority > 0);
+ displayElement("calendar-task-details-priority-low", priority >= 6 && priority <= 9);
+ displayElement("calendar-task-details-priority-normal", priority == 5);
+ displayElement("calendar-task-details-priority-high", priority >= 1 && priority <= 4);
+
+ let status = item.getProperty("STATUS");
+ if (displayElement("calendar-task-details-status-row", status && status.length > 0)) {
+ let statusDetails = document.getElementById("calendar-task-details-status");
+ switch (status) {
+ case "NEEDS-ACTION": {
+ statusDetails.value = calGetString(
+ "calendar",
+ "taskDetailsStatusNeedsAction");
+ break;
+ }
+ case "IN-PROCESS": {
+ let percent = 0;
+ let property = item.getProperty("PERCENT-COMPLETE");
+ if (property != null) {
+ percent = parseInt(property, 10);
+ }
+ statusDetails.value = calGetString(
+ "calendar",
+ "taskDetailsStatusInProgress", [percent]);
+ break;
+ }
+ case "COMPLETED": {
+ if (item.completedDate) {
+ let completedDate = item.completedDate.getInTimezone(
+ calendarDefaultTimezone());
+ statusDetails.value = calGetString(
+ "calendar",
+ "taskDetailsStatusCompletedOn",
+ [dateFormatter.formatDateTime(completedDate)]);
+ }
+ break;
+ }
+ case "CANCELLED": {
+ statusDetails.value = calGetString(
+ "calendar",
+ "taskDetailsStatusCancelled");
+ break;
+ }
+ default: {
+ displayElement("calendar-task-details-status-row", false);
+ break;
+ }
+ }
+ }
+ let categories = item.getCategories({});
+ if (displayElement("calendar-task-details-category-row", categories.length > 0)) {
+ document.getElementById("calendar-task-details-category").value = categories.join(", ");
+ }
+ document.getElementById("task-start-row").Item = item;
+ document.getElementById("task-due-row").Item = item;
+ let parentItem = item;
+ if (parentItem.parentItem != parentItem) {
+ // XXXdbo Didn't we want to get rid of these checks?
+ parentItem = parentItem.parentItem;
+ }
+ let recurrenceInfo = parentItem.recurrenceInfo;
+ let recurStart = parentItem.recurrenceStartDate;
+ if (displayElement("calendar-task-details-repeat-row", recurrenceInfo && recurStart)) {
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let startDate = recurStart.getInTimezone(kDefaultTimezone);
+ let endDate = item.dueDate ? item.dueDate.getInTimezone(kDefaultTimezone) : null;
+ let detailsString = recurrenceRule2String(recurrenceInfo, startDate, endDate, startDate.isDate);
+ if (detailsString) {
+ let rpv = document.getElementById("calendar-task-details-repeat");
+ rpv.value = detailsString.split("\n").join(" ");
+ }
+ }
+ let textbox = document.getElementById("calendar-task-details-description");
+ let description = item.hasProperty("DESCRIPTION") ? item.getProperty("DESCRIPTION") : null;
+ textbox.value = description;
+ textbox.inputField.readOnly = true;
+ let attachmentRows = document.getElementById("calendar-task-details-attachment-rows");
+ removeChildren(attachmentRows);
+ let attachments = item.getAttachments({});
+ if (displayElement("calendar-task-details-attachment-row", attachments.length > 0)) {
+ displayElement("calendar-task-details-attachment-rows", true);
+ for (let attachment of attachments) {
+ let url = attachment.calIAttachment.uri.spec;
+ let urlLabel = createXULElement("label");
+ urlLabel.setAttribute("value", url);
+ urlLabel.setAttribute("tooltiptext", url);
+ urlLabel.setAttribute("class", "text-link");
+ urlLabel.setAttribute("crop", "end");
+ urlLabel.setAttribute("onclick",
+ "if (event.button != 2) launchBrowser(this.value);");
+ urlLabel.setAttribute("context", "taskview-link-context-menu");
+ attachmentRows.appendChild(urlLabel);
+ }
+ }
+ }
+ },
+
+ loadCategories: function(event) {
+ let panel = event.target;
+ let item = document.getElementById("calendar-task-tree").currentTask;
+ panel.loadItem(item);
+ },
+
+ saveCategories: function(event) {
+ let panel = event.target;
+ let item = document.getElementById("calendar-task-tree").currentTask;
+ let categoriesMap = {};
+
+ for (let cat of item.getCategories({})) {
+ categoriesMap[cat] = true;
+ }
+
+ for (let cat of panel.categories) {
+ if (cat in categoriesMap) {
+ delete categoriesMap[cat];
+ } else {
+ categoriesMap[cat] = false;
+ }
+ }
+
+ if (categoriesMap.toSource() != "({})") {
+ let newItem = item.clone();
+ newItem.setCategories(panel.categories.length, panel.categories);
+
+ doTransaction("modify", newItem, newItem.calendar, item, null);
+ }
+ }
+};
+
+
+/**
+ * Updates the currently applied filter for the task view and refreshes the task
+ * tree.
+ *
+ * @param aFilter The filter name to set.
+ */
+function taskViewUpdate(aFilter) {
+ let tree = document.getElementById("calendar-task-tree");
+ let broadcaster = document.getElementById("filterBroadcaster");
+ let oldFilter = broadcaster.getAttribute("value");
+ let filter = oldFilter;
+
+ if (aFilter && !(aFilter instanceof Event)) {
+ filter = aFilter;
+ }
+
+ if (filter && (filter != oldFilter)) {
+ broadcaster.setAttribute("value", filter);
+ }
+
+ // update the filter
+ tree.updateFilter(filter || "all");
+}
+
+/**
+ * Prepares a dialog to send an email to the organizer of the currently selected
+ * task in the task view.
+ *
+ * XXX We already have a function with this name in the event dialog. Either
+ * consolidate or make name more clear.
+ */
+function sendMailToOrganizer() {
+ let item = document.getElementById("calendar-task-tree").currentTask;
+ if (item != null) {
+ let organizer = item.organizer;
+ let email = cal.getAttendeeEmail(organizer, true);
+ let emailSubject = cal.calGetString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+ let identity = item.calendar.getProperty("imip.identity");
+ sendMailTo(email, emailSubject, null, identity);
+ }
+}
+
+/**
+ * Handler function to observe changing of the calendar display deck. Updates
+ * the task tree if the task view was selected.
+ *
+ * TODO Consolidate this function and anything connected, its still from times
+ * before we had view tabs.
+ */
+function taskViewObserveDisplayDeckChange(event) {
+ let deck = event.target;
+
+ // Bug 309505: The 'select' event also fires when we change the selected
+ // panel of calendar-view-box. Workaround with this check.
+ if (deck.id != "calendarDisplayDeck") {
+ return;
+ }
+
+ // In case we find that the task view has been made visible, we refresh the view.
+ if (deck.selectedPanel && deck.selectedPanel.id == "calendar-task-box") {
+ let taskFilterGroup = document.getElementById("task-tree-filtergroup");
+ taskViewUpdate(taskFilterGroup.value || "all");
+ }
+}
+
+// Install event listeners for the display deck change and connect task tree to filter field
+function taskViewOnLoad() {
+ let deck = document.getElementById("calendarDisplayDeck");
+ let tree = document.getElementById("calendar-task-tree");
+
+ if (deck && tree) {
+ deck.addEventListener("select", taskViewObserveDisplayDeckChange, true);
+ tree.textFilterField = "task-text-filter-field";
+
+ // setup the platform-dependent placeholder for the text filter field
+ let textFilter = document.getElementById("task-text-filter-field");
+ if (textFilter) {
+ let base = textFilter.getAttribute("emptytextbase");
+ let keyLabel = textFilter.getAttribute(AppConstants.platform == "macosx" ?
+ "keyLabelMac" : "keyLabelNonMac");
+
+ textFilter.setAttribute("placeholder", base.replace("#1", keyLabel));
+ textFilter.value = "";
+ }
+ }
+
+ // Setup customizeDone handler for the task action toolbox.
+ let toolbox = document.getElementById("task-actions-toolbox");
+ toolbox.customizeDone = function(aEvent) {
+ MailToolboxCustomizeDone(aEvent, "CustomizeTaskActionsToolbar");
+ };
+
+ let toolbarset = document.getElementById("customToolbars");
+ toolbox.toolbarset = toolbarset;
+
+ Services.obs.notifyObservers(window, "calendar-taskview-startup-done", false);
+}
+
+/**
+ * Copy the value of the given link node to the clipboard
+ *
+ * @param linkNode The node containing the value to copy to the clipboard
+ */
+function taskViewCopyLink(linkNode) {
+ if (linkNode) {
+ let linkAddress = linkNode.value;
+ let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Components.interfaces.nsIClipboardHelper);
+ clipboard.copyString(linkAddress);
+ }
+}
+
+window.addEventListener("load", taskViewOnLoad, false);
diff --git a/calendar/base/content/calendar-task-view.xul b/calendar/base/content/calendar-task-view.xul
new file mode 100644
index 000000000..7aacd1d28
--- /dev/null
+++ b/calendar/base/content/calendar-task-view.xul
@@ -0,0 +1,244 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-task-view.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-bindings.css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %dtd3;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://calendar/content/calendar-task-tree.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-task-view.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calApplicationUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calFilter.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <vbox id="calendarDisplayDeck">
+ <vbox id="calendar-task-box" flex="1"
+ onselect="taskDetailsView.onSelect(event);">
+ <hbox id="task-addition-box" align="center">
+ <box align="center" flex="1">
+ <toolbarbutton id="calendar-add-task-button"
+ label="&calendar.newtask.button.label;"
+ tooltiptext="&calendar.newtask.button.tooltip;"
+ observes="calendar_new_todo_command"/>
+ <textbox id="view-task-edit-field"
+ flex="1"
+ class="task-edit-field"
+ onfocus="taskEdit.onFocus(event)"
+ onblur="taskEdit.onBlur(event)"
+ onkeypress="taskEdit.onKeyPress(event)"/>
+ </box>
+ <box align="center" flex="1">
+ <textbox id="task-text-filter-field"
+ class="searchBox"
+ type="search"
+ flex="1"
+ placeholder=""
+ emptytextbase="&calendar.task.text-filter.textbox.emptytext.base1;"
+ keyLabelNonMac="&calendar.task.text-filter.textbox.emptytext.keylabel.nonmac;"
+ keyLabelMac="&calendar.task.text-filter.textbox.emptytext.keylabel.mac;"
+ oncommand="taskViewUpdate();"/>
+ </box>
+ </hbox>
+ <vbox flex="1">
+ <calendar-task-tree id="calendar-task-tree" flex="1"
+ visible-columns="completed priority title startdate duedate"
+ persist="visible-columns ordinals widths sort-active sort-direction height"
+ context="taskitem-context-menu"/>
+ <splitter id="calendar-task-view-splitter" collapse="none" persist="state" class="calendar-splitter"/>
+ <vbox id="calendar-task-details-container"
+ class="main-header-area"
+ flex="1"
+ persist="height"
+ context="task-actions-toolbar-context-menu"
+ hidden="true">
+ <hbox id="calendar-task-details">
+ <grid id="calendar-task-details-grid" flex="1">
+ <columns id="calendar-task-details-columns">
+ <column id="calendar-header-name-column"/>
+ <column id="calendar-header-value-column" flex="1"/>
+ </columns>
+
+ <rows id="calendar-task-details-rows">
+ <row id="calendar-task-details-priority-row"
+ align="end">
+ <hbox pack="end">
+ <label id="calendar-task-details-priority-label"
+ value="&calendar.task-details.priority.label;"
+ class="task-details-name"
+ hidden="true"/>
+ </hbox>
+ <hbox flex="1" align="end" >
+ <label id="calendar-task-details-priority-low"
+ value="&calendar.task-details.priority.low.label;"
+ class="task-details-value"
+ crop="end"
+ flex="1"
+ hidden="true"/>
+ <label id="calendar-task-details-priority-normal"
+ value="&calendar.task-details.priority.normal.label;"
+ class="task-details-value"
+ crop="end"
+ flex="1"
+ hidden="true"/>
+ <label id="calendar-task-details-priority-high"
+ value="&calendar.task-details.priority.high.label;"
+ class="task-details-value"
+ crop="end"
+ flex="1"
+ hidden="true"/>
+ <hbox id="other-actions-box" align="end" flex="1" pack="end">
+ <menupopup id="task-actions-toolbar-context-menu">
+ <menuitem id="CustomizeTaskActionsToolbar"
+ oncommand="CustomizeMailToolbar('task-actions-toolbox', 'CustomizeTaskActionsToolbar')"
+ label="&calendar.menu.customize.label;"
+ accesskey="&calendar.menu.customize.accesskey;"/>
+ </menupopup>
+
+ <toolbox id="task-actions-toolbox"
+ minwidth="50px"
+ mode="full"
+ iconsize="small"
+ labelalign="end"
+ defaultmode="full"
+ defaulticonsize="small"
+ defaultlabelalign="end">
+ <toolbarpalette id="task-actions-toolbar-palette">
+ <toolbarbutton id="task-actions-category"
+ type="menu"
+ label="&calendar.unifinder.tree.categories.label;"
+ tooltiptext="&calendar.task.category.button.tooltip;"
+ command="calendar_task_category_command"
+ observes="calendar_task_category_command"
+ class="toolbarbutton-1 msgHeaderView-button">
+ <panel id="task-actions-category-panel"
+ type="category-panel"
+ onpopupshowing="taskDetailsView.loadCategories(event)"
+ onpopuphiding="taskDetailsView.saveCategories(event)"/>
+ </toolbarbutton>
+ <toolbarbutton id="task-actions-markcompleted"
+ type="menu-button"
+ label="&calendar.context.markcompleted.label;"
+ tooltiptext="&calendar.task.complete.button.tooltip;"
+ command="calendar_toggle_completed_command"
+ observes="calendar_toggle_completed_command"
+ class="toolbarbutton-1 msgHeaderView-button">
+ <menupopup id="task-actions-markcompleted-menupopup" type="task-progress"/>
+ </toolbarbutton>
+ <toolbarbutton id="task-actions-priority"
+ type="menu"
+ label="&calendar.context.priority.label;"
+ tooltiptext="&calendar.task.priority.button.tooltip;"
+ command="calendar_general-priority_command"
+ observes="calendar_general-priority_command"
+ class="toolbarbutton-1 msgHeaderView-button">
+ <menupopup id="task-actions-priority-menupopup" type="task-priority"/>
+ </toolbarbutton>
+ <toolbarbutton id="calendar-delete-task-button"
+ class="toolbarbutton-1 msgHeaderView-button"
+ label="&calendar.taskview.delete.label;"
+ tooltiptext="&calendar.context.deletetask.label;"
+ observes="calendar_delete_todo_command"/>
+ </toolbarpalette>
+
+ <toolbar id="task-actions-toolbar" align="start"
+ class="inline-toolbar"
+ customizable="true"
+ mode="full"
+ iconsize="small"
+ labelalign="end"
+ defaultmode="full"
+ defaulticonsize="small"
+ context="task-actions-toolbar-context-menu"
+ defaultset="task-actions-category,task-actions-markcompleted,task-actions-priority,calendar-delete-task-button"/>
+ </toolbox>
+ </hbox>
+ </hbox>
+ </row>
+ <row id="calendar-task-details-title-row"
+ align="top"
+ hidden="true">
+ <label value="&calendar.task-details.title.label;"
+ class="task-details-name"/>
+ <label id="calendar-task-details-title"
+ class="task-details-value"/>
+ </row>
+ <row id="calendar-task-details-organizer-row"
+ align="top"
+ hidden="true">
+ <label value="&calendar.task-details.organizer.label;"
+ class="task-details-name"/>
+ <label id="calendar-task-details-organizer"
+ class="task-details-value text-link"
+ crop="end"
+ onclick="sendMailToOrganizer()"/>
+ </row>
+ <row id="calendar-task-details-status-row"
+ align="top"
+ hidden="true">
+ <label value="&calendar.task-details.status.label;"
+ class="task-details-name"/>
+ <label id="calendar-task-details-status"
+ crop="end"
+ class="task-details-value"/>
+ </row>
+ <row id="calendar-task-details-category-row"
+ align="top"
+ hidden="true">
+ <label value="&calendar.task-details.category.label;"
+ class="task-details-name"/>
+ <label id="calendar-task-details-category"
+ crop="end"
+ class="task-details-value"/>
+ </row>
+ <row class="item-date-row"
+ id="task-start-row"
+ mode="start"
+ taskStartLabel="&calendar.task-details.start.label;"
+ align="end"/>
+ <row class="item-date-row"
+ id="task-due-row"
+ mode="end"
+ taskDueLabel="&calendar.task-details.due.label;"
+ align="end"/>
+ <row id="calendar-task-details-repeat-row"
+ align="top"
+ hidden="true">
+ <label value="&calendar.task-details.repeat.label;"
+ class="task-details-name"/>
+ <label id="calendar-task-details-repeat"
+ crop="end"
+ class="task-details-value"/>
+ </row>
+ </rows>
+ </grid>
+ </hbox>
+ <textbox id="calendar-task-details-description" multiline="true" flex="1"/>
+ <hbox id="calendar-task-details-attachment-row"
+ align="top"
+ hidden="true">
+ <hbox pack="end">
+ <label value="&calendar.task-details.attachments.label;"
+ class="task-details-name"/>
+ </hbox>
+ <vbox id="calendar-task-details-attachment-rows"
+ align="top"
+ flex="1"
+ style="overflow: auto;"
+ hidden="true">
+ </vbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+
+</overlay>
diff --git a/calendar/base/content/calendar-ui-utils.js b/calendar/base/content/calendar-ui-utils.js
new file mode 100644
index 000000000..3cc85d25e
--- /dev/null
+++ b/calendar/base/content/calendar-ui-utils.js
@@ -0,0 +1,654 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported getElementValue, setBooleanAttribute, showElement, hideElement,
+ * uncollapseElement, collapseElement, disableElementWithLock,
+ * enableElementWithLock, uncheckChildNodes, removeChildren,
+ * appendCalendarItems, setAttributeToChildren, checkRadioControl,
+ * processEnableCheckbox, updateListboxDeleteButton,
+ * updateUnitLabelPlural, updateMenuLabelsPlural, menuListSelectItem,
+ * getOptimalMinimumWidth, getOptimalMinimumHeight,
+ * getOtherOrientation, updateSelectedLabel, setupAttendanceMenu
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+
+/**
+ * Helper function for filling the form,
+ * Set the value of a property of a XUL element
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ * @param aNewValue value to set property to ( if undefined no change is made )
+ * @param aPropertyName OPTIONAL name of property to set, default is "value",
+ * use "checked" for radios & checkboxes, "data" for
+ * drop-downs
+ */
+function setElementValue(aElement, aNewValue, aPropertyName) {
+ cal.ASSERT(aElement, "aElement");
+
+ if (aNewValue !== undefined) {
+ if (typeof aElement == "string") {
+ aElement = document.getElementById(aElement);
+ cal.ASSERT(aElement, "aElement");
+ }
+
+ if (!aElement) { return; }
+
+ if (aNewValue === false) {
+ try {
+ aElement.removeAttribute(aPropertyName);
+ } catch (e) {
+ cal.ERROR("setElementValue: aElement.removeAttribute couldn't remove " +
+ aPropertyName + " from " + (aElement && aElement.localName) + " e: " + e + "\n");
+ }
+ } else if (aPropertyName) {
+ try {
+ aElement.setAttribute(aPropertyName, aNewValue);
+ } catch (e) {
+ cal.ERROR("setElementValue: aElement.setAttribute couldn't set " +
+ aPropertyName + " from " + (aElement && aElement.localName) + " to " + aNewValue +
+ " e: " + e + "\n");
+ }
+ } else {
+ aElement.value = aNewValue;
+ }
+ }
+}
+
+/**
+ * Helper function for getting data from the form,
+ * Get the value of a property of a XUL element
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ * @param propertyName OPTIONAL name of property to set, default is "value",
+ * use "checked" for radios & checkboxes, "data" for
+ * drop-downs
+ * @return newValue Value of property
+ *
+ */
+function getElementValue(aElement, aPropertyName) {
+ if (typeof aElement == "string") {
+ aElement = document.getElementById(aElement);
+ }
+ return aElement[aPropertyName || "value"];
+}
+
+/**
+ * Sets the value of a boolean attribute by either setting the value or
+ * removing the attribute
+ *
+ * @param aXulElement The XUL element/string ID the attribute is applied to.
+ * @param aAttribute The name of the attribute
+ * @param aValue The boolean value
+ * @return Returns aValue (for chaining)
+ */
+function setBooleanAttribute(aXulElement, aAttribute, aValue) {
+ setElementValue(aXulElement, aValue ? "true" : false, aAttribute);
+ return aValue;
+}
+
+/**
+ * Unconditionally show the element (hidden attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function showElement(aElement) {
+ setElementValue(aElement, false, "hidden");
+}
+
+/**
+ * Unconditionally hide the element (hidden attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function hideElement(aElement) {
+ setElementValue(aElement, "true", "hidden");
+}
+
+/**
+ * Unconditionally show the element (collapsed attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function uncollapseElement(aElement) {
+ setElementValue(aElement, false, "collapsed");
+}
+
+/**
+ * Unconditionally hide the element (collapsed attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function collapseElement(aElement) {
+ setElementValue(aElement, "true", "collapsed");
+}
+
+/**
+ * Unconditionally enable the element (hidden attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function enableElement(aElement) {
+ setElementValue(aElement, false, "disabled");
+}
+
+/**
+ * Unconditionally disable the element (hidden attribute)
+ *
+ * @param aElement ID of XUL element to set, or the element node itself
+ */
+function disableElement(aElement) {
+ setElementValue(aElement, "true", "disabled");
+}
+
+/**
+ * This function unconditionally disables the element for
+ * which the id has been passed as argument. Furthermore, it
+ * remembers who was responsible for this action by using
+ * the given key (lockId). In case the control should be
+ * enabled again the lock gets removed, but the control only
+ * gets enabled if *all* possibly held locks have been removed.
+ *
+ * @param elementId The element ID of the element to disable.
+ * @param lockId The ID of the lock to set.
+ */
+function disableElementWithLock(elementId, lockId) {
+ // unconditionally disable the element.
+ disableElement(elementId);
+
+ // remember that this element has been locked with
+ // the key passed as argument. we keep a primitive
+ // form of ref-count in the attribute 'lock'.
+ let element = document.getElementById(elementId);
+ if (element) {
+ if (!element.hasAttribute(lockId)) {
+ element.setAttribute(lockId, "true");
+ let n = parseInt(element.getAttribute("lock") || 0, 10);
+ element.setAttribute("lock", n + 1);
+ }
+ }
+}
+
+/**
+ * This function is intended to be used in tandem with the
+ * above defined function 'disableElementWithLock()'.
+ * See the respective comment for further details.
+ *
+ * @see disableElementWithLock
+ * @param elementId The element ID of the element to enable.
+ * @param lockId The ID of the lock to set.
+ */
+function enableElementWithLock(elementId, lockId) {
+ let element = document.getElementById(elementId);
+ if (!element) {
+ dump("unable to find " + elementId + "\n");
+ return;
+ }
+
+ if (element.hasAttribute(lockId)) {
+ element.removeAttribute(lockId);
+ let n = parseInt(element.getAttribute("lock") || 0, 10) - 1;
+ if (n > 0) {
+ element.setAttribute("lock", n);
+ } else {
+ element.removeAttribute("lock");
+ }
+ if (n <= 0) {
+ enableElement(elementId);
+ }
+ }
+}
+
+/**
+ * Unchecks the commands of the child elements of a DOM-tree-node i.e of a menu
+ *
+ * @param aEvent The event from which the target is taken to retrieve the
+ * child elements
+ */
+function uncheckChildNodes(aEvent) {
+ let liveList = aEvent.target.getElementsByAttribute("checked", "true");
+ for (let i = liveList.length - 1; i >= 0; i--) {
+ let commandName = liveList.item(i).getAttribute("command");
+ let command = document.getElementById(commandName);
+ if (command) {
+ command.setAttribute("checked", "false");
+ }
+ }
+}
+
+/**
+ * Removes all child nodes of the given node
+ *
+ * @param aElement The Node (or its id) to remove children from
+ */
+function removeChildren(aElement) {
+ if (typeof aElement == "string") {
+ aElement = document.getElementById(aElement);
+ }
+
+ while (aElement.firstChild) {
+ aElement.lastChild.remove();
+ }
+}
+
+/**
+ * Sorts a sorted array of calendars by pref |calendar.list.sortOrder|.
+ * Repairs that pref if dangling entries exist.
+ *
+ * @param calendars An array of calendars to sort.
+ */
+function sortCalendarArray(calendars) {
+ let ret = calendars.concat([]);
+ let sortOrder = {};
+ let sortOrderPref = Preferences.get("calendar.list.sortOrder", "").split(" ");
+ for (let i = 0; i < sortOrderPref.length; ++i) {
+ sortOrder[sortOrderPref[i]] = i;
+ }
+ function sortFunc(cal1, cal2) {
+ let orderIdx1 = sortOrder[cal1.id] || -1;
+ let orderIdx2 = sortOrder[cal2.id] || -1;
+ if (orderIdx1 < orderIdx2) {
+ return -1;
+ }
+ if (orderIdx1 > orderIdx2) {
+ return 1;
+ }
+ return 0;
+ }
+ ret.sort(sortFunc);
+
+ // check and repair pref:
+ let sortOrderString = Preferences.get("calendar.list.sortOrder", "");
+ let wantedOrderString = ret.map(calendar => calendar.id).join(" ");
+ if (wantedOrderString != sortOrderString) {
+ Preferences.set("calendar.list.sortOrder", wantedOrderString);
+ }
+
+ return ret;
+}
+
+/**
+* Fills up a menu - either a menupopup or a menulist - with menuitems that refer
+* to calendars.
+*
+* @param aItem The event or task
+* @param aCalendarMenuParent The direct parent of the menuitems - either a
+* menupopup or a menulist
+* @param aCalendarToUse The default-calendar
+* @param aOnCommand A string that is applied to the "oncommand"
+* attribute of each menuitem
+* @return The index of the calendar that matches the
+* default-calendar. By default 0 is returned.
+*/
+function appendCalendarItems(aItem, aCalendarMenuParent, aCalendarToUse, aOnCommand) {
+ let calendarToUse = aCalendarToUse || aItem.calendar;
+ let calendars = sortCalendarArray(cal.getCalendarManager().getCalendars({}));
+ let indexToSelect = 0;
+ let index = -1;
+ for (let i = 0; i < calendars.length; ++i) {
+ let calendar = calendars[i];
+ if (calendar.id == calendarToUse.id ||
+ (calendar &&
+ isCalendarWritable(calendar) &&
+ (userCanAddItemsToCalendar(calendar) ||
+ (calendar == aItem.calendar && userCanModifyItem(aItem))) &&
+ isItemSupported(aItem, calendar))) {
+ let menuitem = addMenuItem(aCalendarMenuParent, calendar.name, calendar.name);
+ menuitem.calendar = calendar;
+ index++;
+ if (aOnCommand) {
+ menuitem.setAttribute("oncommand", aOnCommand);
+ }
+ if (aCalendarMenuParent.localName == "menupopup") {
+ menuitem.setAttribute("type", "checkbox");
+ }
+ if (calendarToUse && calendarToUse.id == calendar.id) {
+ indexToSelect = index;
+ }
+ }
+ }
+ return indexToSelect;
+}
+
+/**
+ * Helper function to add a menuitem to a menulist or similar.
+ *
+ * @param aParent The XUL node to add the menuitem to.
+ * @param aLabel The label string of the menuitem.
+ * @param aValue The value attribute of the menuitem.
+ * @param aCommand The oncommand attribute of the menuitem.
+ * @return The newly created menuitem
+ */
+function addMenuItem(aParent, aLabel, aValue, aCommand) {
+ let item = null;
+ if (aParent.localName == "menupopup") {
+ item = createXULElement("menuitem");
+ item.setAttribute("label", aLabel);
+ if (aValue) {
+ item.setAttribute("value", aValue);
+ }
+ if (aCommand) {
+ item.command = aCommand;
+ }
+ aParent.appendChild(item);
+ } else if (aParent.localName == "menulist") {
+ item = aParent.appendItem(aLabel, aValue);
+ }
+ return item;
+}
+
+/**
+ * Sets a given attribute value on the children of a passed node
+ *
+ * @param aParent The parent node.
+ * @param aAttribute The name of the attribute to be set.
+ * @param aValue The value of the attribute.
+ * @param aFilterAttribute (optional) The name of an attribute that the child nodes carry
+ * and that is used to filter the childnodes.
+ * @param aFilterValue (optional) The value of the filterattribute. If set only those
+ * childnodes are modified that have an attribute
+ * 'aFilterAttribute' with the given value
+ * 'aFilterValue' set.
+ */
+function setAttributeToChildren(aParent, aAttribute, aValue, aFilterAttribute, aFilterValue) {
+ for (let i = 0; i < aParent.childNodes.length; i++) {
+ let element = aParent.childNodes[i];
+ if (aFilterAttribute == null) {
+ setElementValue(element, aValue, aAttribute);
+ } else if (element.hasAttribute(aFilterAttribute)) {
+ let compValue = element.getAttribute(aFilterAttribute);
+ if (compValue === aFilterValue) {
+ setElementValue(element, aValue, aAttribute);
+ }
+ }
+ }
+}
+
+/**
+ * Checks a radio control or a radio-menuitem.
+ *
+ * @param aParent The parent node of the 'radio controls', either radios
+ * or menuitems of the type 'radio'.
+ * @param avalue The value of the radio control bound to be checked.
+ * @return True or false depending on if the a 'radio control' with the
+ * given value could be checked.
+ */
+function checkRadioControl(aParent, aValue) {
+ for (let i = 0; i < aParent.childNodes.length; i++) {
+ let element = aParent.childNodes[i];
+ if (element.hasAttribute("value")) {
+ let compValue = element.getAttribute("value");
+ if (compValue == aValue) {
+ if (element.localName == "menuitem") {
+ if (element.getAttribute("type") == "radio") {
+ element.setAttribute("checked", "true");
+ return true;
+ }
+ } else if (element.localName == "radio") {
+ element.radioGroup.selectedItem = element;
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Enables or disables the given element depending on the checkbox state.
+ *
+ * @param checkboxId The ID of the XUL checkbox element.
+ * @param elementId The element to change the disabled state on.
+ */
+function processEnableCheckbox(checkboxId, elementId) {
+ let checked = document.getElementById(checkboxId).checked;
+ setElementValue(elementId, !checked && "true", "disabled");
+}
+
+/**
+ * Enable/disable button if there are children in a listbox
+ *
+ * XXX This function needs renaming, it can do more than just buttons.
+ *
+ * @param listboxId The ID of the listbox to check.
+ * @param buttonId The element to change the disabled state on.
+ */
+function updateListboxDeleteButton(listboxId, buttonId) {
+ let rowCount = document.getElementById(listboxId).getRowCount();
+ setElementValue(buttonId, rowCount < 1 && "true", "disabled");
+}
+
+/**
+ * Gets the correct plural form of a given unit.
+ *
+ * @param aLength The number to use to determine the plural form
+ * @param aUnit The unit to find the plural form of
+ * @param aIncludeLength (optional) If true, the length will be included in the
+ * result. If false, only the pluralized unit is returned.
+ * @return A string containg the pluralized version of the unit
+ */
+function unitPluralForm(aLength, aUnit, aIncludeLength=true) {
+ let unitProp = {
+ minutes: "unitMinutes",
+ hours: "unitHours",
+ days: "unitDays",
+ weeks: "unitWeeks"
+ }[aUnit] || "unitMinutes";
+
+ return PluralForm.get(aLength, cal.calGetString("calendar", unitProp))
+ .replace("#1", aIncludeLength ? aLength : "").trim();
+}
+
+/**
+ * Update the given unit label to show the correct plural form.
+ *
+ * @param aLengthFieldId The ID of the element containing the number
+ * @param aLabelId The ID of the label to update.
+ * @param aUnit The unit to use for the label.
+ */
+function updateUnitLabelPlural(aLengthFieldId, aLabelId, aUnit) {
+ let label = document.getElementById(aLabelId);
+ let length = Number(document.getElementById(aLengthFieldId).value);
+
+ label.value = unitPluralForm(length, aUnit, false);
+}
+
+/**
+ * Update the given menu to show the correct plural form in the list.
+ *
+ * @param aLengthFieldId The ID of the element containing the number
+ * @param aMenuId The menu to update labels in.
+ */
+function updateMenuLabelsPlural(aLengthFieldId, aMenuId) {
+ let menu = document.getElementById(aMenuId);
+ let length = Number(document.getElementById(aLengthFieldId).value);
+
+ // update the menu items
+ let items = menu.getElementsByTagName("menuitem");
+ for (let menuItem of items) {
+ menuItem.label = unitPluralForm(length, menuItem.value, false);
+ }
+
+ // force the menu selection to redraw
+ let saveSelectedIndex = menu.selectedIndex;
+ menu.selectedIndex = -1;
+ menu.selectedIndex = saveSelectedIndex;
+}
+
+/**
+ * Select value in menuList. Throws string if no such value.
+ *
+ * XXX Isn't it enough to just do menuList.value = value ?
+ *
+ * @param menuListId The ID of the menulist to check.
+ * @param value The value to set.
+ * @throws String error if value not found.
+ */
+function menuListSelectItem(menuListId, value) {
+ let menuList = document.getElementById(menuListId);
+ let index = menuListIndexOf(menuList, value);
+ if (index == -1) {
+ throw "menuListSelectItem: No such Element: " + value;
+ } else {
+ menuList.selectedIndex = index;
+ }
+}
+
+/**
+ * Find index of menuitem with the given value, or return -1 if not found.
+ *
+ * @param menuListId The XUL menulist node to check.
+ * @param value The value to look for.
+ * @return The child index of the node that matches, or -1.
+ */
+function menuListIndexOf(menuList, value) {
+ let items = menuList.menupopup.childNodes;
+ let index = -1;
+ for (let i = 0; i < items.length; i++) {
+ let element = items[i];
+ if (element.nodeName == "menuitem") {
+ index++;
+ }
+ if (element.getAttribute("value") == value) {
+ return index;
+ }
+ }
+ return -1; // not found
+}
+
+/**
+ * Creates the given element in the XUL namespace.
+ *
+ * @param elem The local name of the element to create.
+ * @return The XUL element requested.
+ */
+function createXULElement(elem) {
+ return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", elem);
+}
+
+/**
+ * A helper function to calculate and add up certain css-values of a box.
+ * It is required, that all css values can be converted to integers
+ * see also
+ * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSview-getComputedStyle
+ * @param aXULElement The xul element to be inspected.
+ * @param aStyleProps The css style properties for which values are to be retrieved
+ * e.g. 'font-size', 'min-width" etc.
+ * @return An integer value denoting the optimal minimum width
+ */
+function getSummarizedStyleValues(aXULElement, aStyleProps) {
+ let retValue = 0;
+ let cssStyleDeclares = document.defaultView.getComputedStyle(aXULElement, null);
+ for (let prop of aStyleProps) {
+ retValue += parseInt(cssStyleDeclares.getPropertyValue(prop), 10);
+ }
+ return retValue;
+}
+
+/**
+ * Calculates the optimal minimum width based on the set css style-rules
+ * by considering the css rules for the min-width, padding, border, margin
+ * and border of the box.
+ *
+ * @param aXULElement The xul element to be inspected.
+ * @return An integer value denoting the optimal minimum width
+ */
+function getOptimalMinimumWidth(aXULElement) {
+ return getSummarizedStyleValues(aXULElement, ["min-width",
+ "padding-left", "padding-right",
+ "margin-left", "margin-top",
+ "border-left-width", "border-right-width"]);
+}
+
+/**
+ * Calculates the optimal minimum height based on the set css style-rules
+ * by considering the css rules for the font-size, padding, border, margin
+ * and border of the box. In its current state the line-height is considered
+ * by assuming that it's size is about one third of the size of the font-size
+ *
+ * @param aXULElement The xul-element to be inspected.
+ * @return An integer value denoting the optimal minimum height
+ */
+function getOptimalMinimumHeight(aXULElement) {
+ // the following line of code presumes that the line-height is set to "normal"
+ // which is supposed to be a "reasonable distance" between the lines
+ let firstEntity = parseInt(1.35 * getSummarizedStyleValues(aXULElement, ["font-size"]), 10);
+ let secondEntity = getSummarizedStyleValues(aXULElement,
+ ["padding-bottom", "padding-top",
+ "margin-bottom", "margin-top",
+ "border-bottom-width", "border-top-width"]);
+ return (firstEntity + secondEntity);
+}
+
+/**
+ * Gets the "other" orientation value, i.e if "horizontal" is passed, "vertical"
+ * is returned and vice versa.
+ *
+ * @param aOrientation The orientation value to turn around.
+ * @return The opposite orientation value.
+ */
+function getOtherOrientation(aOrientation) {
+ return (aOrientation == "horizontal" ? "vertical" : "horizontal");
+}
+
+/**
+ * Setting labels on a menuitem doesn't update the label that is shown when the
+ * menuitem is selected. This function takes care by reselecting the item
+ *
+ * @param aElement The element to update, or its id as a string
+ */
+function updateSelectedLabel(aElement) {
+ if (typeof aElement == "string") {
+ aElement = document.getElementById(aElement);
+ }
+ let selectedIndex = aElement.selectedIndex;
+ aElement.selectedIndex = -1;
+ aElement.selectedIndex = selectedIndex;
+}
+
+/**
+ * Sets up the attendance context menu, based on the given items
+ *
+ * @param aMenu The DOM Node of the menupopup to set up
+ * @param aItems The array of items to consider
+ */
+function setupAttendanceMenu(aMenu, aItems) {
+ function getInvStat(item) {
+ let attendee = null;
+ if (cal.isInvitation(item)) {
+ attendee = cal.getInvitedAttendee(item);
+ } else if (item.organizer) {
+ let calOrgId = item.calendar.getProperty("organizerId");
+ if (calOrgId == item.organizer.id && item.getAttendees({}).length) {
+ attendee = item.organizer;
+ }
+ }
+ return attendee && attendee.participationStatus;
+ }
+
+ goUpdateCommand("calendar_attendance_command");
+
+ let allSingle = aItems.every(x => !x.recurrenceId);
+ setElementValue(aMenu, allSingle ? "single" : "recurring", "itemType");
+
+ let firstStatusOccurrences = aItems.length && getInvStat(aItems[0]);
+ let firstStatusParents = aItems.length && getInvStat(aItems[0].parentItem);
+ let sameStatusOccurrences = aItems.every(x => getInvStat(x) == firstStatusOccurrences);
+ let sameStatusParents = aItems.every(x => getInvStat(x.parentItem) == firstStatusParents);
+
+ let occurrenceChildren = aMenu.getElementsByAttribute("value", firstStatusOccurrences);
+ let parentsChildren = aMenu.getElementsByAttribute("value", firstStatusParents);
+
+ if (sameStatusOccurrences && occurrenceChildren[0]) {
+ occurrenceChildren[0].setAttribute("checked", "true");
+ }
+
+ if (sameStatusParents && parentsChildren[1]) {
+ parentsChildren[1].setAttribute("checked", "true");
+ }
+
+ return true;
+}
diff --git a/calendar/base/content/calendar-unifinder-todo.js b/calendar/base/content/calendar-unifinder-todo.js
new file mode 100644
index 000000000..9c90c5cc6
--- /dev/null
+++ b/calendar/base/content/calendar-unifinder-todo.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported prepareCalendarToDoUnifinder, finishCalendarToDoUnifinder */
+
+/**
+ * Called when the window is loaded to set up the unifinder-todo.
+ */
+function prepareCalendarToDoUnifinder() {
+ // add listener to update the date filters
+ getViewDeck().addEventListener("dayselect", updateCalendarToDoUnifinder, false);
+
+ updateCalendarToDoUnifinder();
+}
+
+/**
+ * Updates the applied filter and show completed view of the unifinder todo.
+ *
+ * @param aFilter The filter name to set.
+ */
+function updateCalendarToDoUnifinder(aFilter) {
+ // Set up hiding completed tasks for the unifinder-todo tree
+ let showCompleted = document.getElementById("show-completed-checkbox").checked;
+ let tree = document.getElementById("unifinder-todo-tree");
+ let oldFilter = document.getElementById("unifinder-todo-filter-broadcaster").getAttribute("value");
+ let filter = oldFilter;
+
+ // This function acts as an event listener, in which case we get the Event as the
+ // parameter instead of a filter.
+ if (aFilter && !(aFilter instanceof Event)) {
+ filter = aFilter;
+ }
+
+ if (filter && (filter != oldFilter)) {
+ document.getElementById("unifinder-todo-filter-broadcaster").setAttribute("value", aFilter);
+ }
+
+ if (filter && !showCompleted) {
+ let filterProps = tree.mFilter.getDefinedFilterProperties(filter);
+ if (filterProps) {
+ filterProps.status = (filterProps.status || filterProps.FILTER_STATUS_ALL) &
+ (filterProps.FILTER_STATUS_INCOMPLETE |
+ filterProps.FILTER_STATUS_IN_PROGRESS);
+ filter = filterProps;
+ }
+ }
+
+ // update the filter
+ tree.showCompleted = showCompleted;
+ tree.updateFilter(filter);
+}
+
+/**
+ * Called when the window is unloaded to clean up the unifinder-todo.
+ */
+function finishCalendarToDoUnifinder() {
+ // remove listeners
+ getViewDeck().removeEventListener("dayselect", updateCalendarToDoUnifinder, false);
+}
diff --git a/calendar/base/content/calendar-unifinder-todo.xul b/calendar/base/content/calendar-unifinder-todo.xul
new file mode 100644
index 000000000..b01ebe667
--- /dev/null
+++ b/calendar/base/content/calendar-unifinder-todo.xul
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd3;
+]>
+
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-task-view.css"?>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-task-tree.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calFilter.js"/> <script type="application/javascript" src="chrome://calendar/content/calendar-unifinder-todo.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <vbox id="todo-tab-panel" persist="height,collapsed" flex="1">
+ <box id="todo-label" align="left" collapsed="true">
+ <label flex="1" crop="end" style="font-weight: bold" value="&calendar.unifinder.todoitems.label;" control="unifinder-todo-tree"/>
+ </box>
+ <box align="center">
+ <checkbox id="show-completed-checkbox"
+ label="&calendar.unifinder.showcompletedtodos.label;"
+ flex="1"
+ crop="end"
+ oncommand="updateCalendarToDoUnifinder()"
+ persist="checked"/>
+ </box>
+ <vbox id="calendar-task-tree-detail" flex="1">
+ <calendar-task-tree id="unifinder-todo-tree" flex="1"
+ visible-columns="completed priority title"
+ persist="visible-columns ordinals widths sort-active sort-direction"
+ context="taskitem-context-menu"/>
+ <textbox id="unifinder-task-edit-field"
+ class="task-edit-field"
+ onfocus="taskEdit.onFocus(event)"
+ onblur="taskEdit.onBlur(event)"
+ onkeypress="taskEdit.onKeyPress(event)"/>
+ </vbox>
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/calendar-unifinder.js b/calendar/base/content/calendar-unifinder.js
new file mode 100644
index 000000000..45e250a1c
--- /dev/null
+++ b/calendar/base/content/calendar-unifinder.js
@@ -0,0 +1,959 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gCalendarEventTreeClicked, unifinderDoubleClick, unifinderKeyPress,
+ * focusSearch, toggleUnifinder
+ */
+
+/**
+ * U N I F I N D E R
+ *
+ * This is a hacked in interface to the unifinder. We will need to
+ * improve this to make it usable in general.
+ *
+ * NOTE: Including this file will cause a load handler to be added to the
+ * window.
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Set this to true when the calendar event tree is clicked to allow for
+// multiple selection
+var gCalendarEventTreeClicked = false;
+
+// Store the start and enddate, because the providers can't be trusted when
+// dealing with all-day events. So we need to filter later. See bug 306157
+
+var kDefaultTimezone;
+var gUnifinderNeedsRefresh = true;
+
+/**
+ * Checks if the unifinder is hidden
+ *
+ * @return Returns true if the unifinder is hidden.
+ */
+function isUnifinderHidden() {
+ return document.getElementById("bottom-events-box").hidden;
+}
+
+/**
+ * Returns the current filter applied to the unifinder.
+ *
+ * @return The string name of the applied filter.
+ */
+function getCurrentUnifinderFilter() {
+ return document.getElementById("event-filter-menulist").selectedItem.value;
+}
+
+/**
+ * Observer for the calendar event data source. This keeps the unifinder
+ * display up to date when the calendar event data is changed
+ *
+ * @see calIObserver
+ * @see calICompositeObserver
+ */
+var unifinderObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calICompositeObserver,
+ Components.interfaces.nsIObserver,
+ Components.interfaces.calIObserver
+ ]),
+
+ // calIObserver:
+ onStartBatch: function() {
+ },
+
+ onEndBatch: function() {
+ refreshEventTree();
+ },
+
+ onLoad: function() {
+ if (isUnifinderHidden() && !gUnifinderNeedsRefresh) {
+ // If the unifinder is hidden, all further item operations might
+ // produce invalid entries in the unifinder. From now on, ignore
+ // those operations and refresh as soon as the unifinder is shown
+ // again.
+ gUnifinderNeedsRefresh = true;
+ unifinderTreeView.clearItems();
+ }
+ },
+
+ onAddItem: function(aItem) {
+ if (isEvent(aItem) &&
+ !gUnifinderNeedsRefresh &&
+ unifinderTreeView.mFilter.isItemInFilters(aItem)
+ ) {
+ this.addItemToTree(aItem);
+ }
+ },
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ this.onDeleteItem(aOldItem);
+ this.onAddItem(aNewItem);
+ },
+
+ onDeleteItem: function(aDeletedItem) {
+ if (isEvent(aDeletedItem) && !gUnifinderNeedsRefresh) {
+ this.removeItemFromTree(aDeletedItem);
+ }
+ },
+
+ onError: function(aCalendar, aErrNo, aMessage) {},
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "disabled":
+ refreshEventTree();
+ break;
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ },
+
+ // calICompositeObserver:
+ onCalendarAdded: function(aAddedCalendar) {
+ if (!aAddedCalendar.getProperty("disabled")) {
+ addItemsFromCalendar(aAddedCalendar,
+ addItemsFromSingleCalendarInternal);
+ }
+ },
+
+ onCalendarRemoved: function(aDeletedCalendar) {
+ if (!aDeletedCalendar.getProperty("disabled")) {
+ deleteItemsFromCalendar(aDeletedCalendar);
+ }
+ },
+
+ onDefaultCalendarChanged: function(aNewDefaultCalendar) {},
+
+ /**
+ * Add an unifinder item to the tree. It is safe to call these for any
+ * event. The functions will determine whether or not anything actually
+ * needs to be done to the tree.
+ *
+ * @return aItem The item to add to the tree.
+ */
+ addItemToTree: function(aItem) {
+ let items;
+ let filter = unifinderTreeView.mFilter;
+
+ if (filter.startDate && filter.endDate) {
+ items = aItem.getOccurrencesBetween(filter.startDate, filter.endDate, {});
+ } else {
+ items = [aItem];
+ }
+ unifinderTreeView.addItems(items.filter(filter.isItemInFilters, filter));
+ },
+
+ /**
+ * Remove an item from the unifinder tree. It is safe to call these for any
+ * event. The functions will determine whether or not anything actually
+ * needs to be done to the tree.
+ *
+ * @return aItem The item to remove from the tree.
+ */
+ removeItemFromTree: function(aItem) {
+ let items;
+ let filter = unifinderTreeView.mFilter;
+ if (filter.startDate && filter.endDate && (aItem.parentItem == aItem)) {
+ items = aItem.getOccurrencesBetween(filter.startDate, filter.endDate, {});
+ } else {
+ items = [aItem];
+ }
+ // XXX: do we really still need this, we are always checking it in the refreshInternal
+ unifinderTreeView.removeItems(items.filter(filter.isItemInFilters, filter));
+ },
+
+ observe: function(aSubject, aTopic, aPrefName) {
+ switch (aPrefName) {
+ case "calendar.date.format":
+ case "calendar.timezone.local":
+ refreshEventTree();
+ break;
+ }
+ }
+};
+
+/**
+ * Called when the window is loaded to prepare the unifinder. This function is
+ * used to add observers, event listeners, etc.
+ */
+function prepareCalendarUnifinder() {
+ // Only load once
+ window.removeEventListener("load", prepareCalendarUnifinder, false);
+ let unifinderTree = document.getElementById("unifinder-search-results-tree");
+
+ // Add pref observer
+ let branch = Services.prefs.getBranch("");
+ branch.addObserver("calendar.", unifinderObserver, false);
+
+ // Check if this is not the hidden window, which has no UI elements
+ if (unifinderTree) {
+ // set up our calendar event observer
+ let ccalendar = getCompositeCalendar();
+ ccalendar.addObserver(unifinderObserver);
+
+ kDefaultTimezone = calendarDefaultTimezone();
+
+ // Set up the filter
+ unifinderTreeView.mFilter = new calFilter();
+
+ // Set up the unifinder views.
+ unifinderTreeView.treeElement = unifinderTree;
+ unifinderTree.view = unifinderTreeView;
+
+ // Listen for changes in the selected day, so we can update if need be
+ let viewDeck = getViewDeck();
+ viewDeck.addEventListener("dayselect", unifinderDaySelect, false);
+ viewDeck.addEventListener("itemselect", unifinderItemSelect, true);
+
+ // Set up sortDirection and sortActive, in case it persisted
+ let sorted = unifinderTree.getAttribute("sort-active");
+ let sortDirection = unifinderTree.getAttribute("sort-direction");
+ if (!sortDirection || sortDirection == "undefined") {
+ sortDirection = "ascending";
+ }
+ let tree = document.getElementById("unifinder-search-results-tree");
+ let treecols = tree.getElementsByTagName("treecol");
+ for (let i = 0; i < treecols.length; i++) {
+ let col = treecols[i];
+ let content = col.getAttribute("itemproperty");
+ if (sorted && sorted.length > 0) {
+ if (sorted == content) {
+ unifinderTreeView.sortDirection = sortDirection;
+ unifinderTreeView.selectedColumn = col;
+ }
+ }
+ }
+ // Display something upon first load. onLoad doesn't work properly for
+ // observers
+ if (!isUnifinderHidden()) {
+ gUnifinderNeedsRefresh = false;
+ refreshEventTree();
+ }
+ }
+}
+
+/**
+ * Called when the window is unloaded to clean up any observers and listeners
+ * added.
+ */
+function finishCalendarUnifinder() {
+ let ccalendar = getCompositeCalendar();
+ ccalendar.removeObserver(unifinderObserver);
+
+ // Remove pref observer
+ let branch = Services.prefs.getBranch("");
+ branch.removeObserver("calendar.", unifinderObserver, false);
+
+ let viewDeck = getViewDeck();
+ if (viewDeck) {
+ viewDeck.removeEventListener("dayselect", unifinderDaySelect, false);
+ viewDeck.removeEventListener("itemselect", unifinderItemSelect, true);
+ }
+
+ // Persist the sort
+ let unifinderTree = document.getElementById("unifinder-search-results-tree");
+ let sorted = unifinderTreeView.selectedColumn;
+ if (sorted) {
+ unifinderTree.setAttribute("sort-active", sorted.getAttribute("itemproperty"));
+ unifinderTree.setAttribute("sort-direction", unifinderTreeView.sortDirection);
+ } else {
+ unifinderTree.removeAttribute("sort-active");
+ unifinderTree.removeAttribute("sort-direction");
+ }
+}
+
+/**
+ * Event listener for the view deck's dayselect event.
+ */
+function unifinderDaySelect() {
+ let filter = getCurrentUnifinderFilter();
+ if (filter == "current" || filter == "currentview") {
+ refreshEventTree();
+ }
+}
+
+/**
+ * Event listener for the view deck's itemselect event.
+ */
+function unifinderItemSelect(aEvent) {
+ unifinderTreeView.setSelectedItems(aEvent.detail);
+}
+
+/**
+ * Helper function to display event datetimes in the unifinder.
+ *
+ * @param aDatetime A calIDateTime object to format.
+ * @return The passed date's formatted in the default timezone.
+ */
+function formatUnifinderEventDateTime(aDatetime) {
+ return cal.getDateFormatter().formatDateTime(aDatetime.getInTimezone(kDefaultTimezone));
+}
+
+/**
+ * Handler function for double clicking the unifinder.
+ *
+ * @param event The DOM doubleclick event.
+ */
+function unifinderDoubleClick(event) {
+ // We only care about button 0 (left click) events
+ if (event.button != 0) {
+ return;
+ }
+
+ // find event by id
+ let calendarEvent = unifinderTreeView.getItemFromEvent(event);
+
+ if (calendarEvent) {
+ modifyEventWithDialog(calendarEvent, null, true);
+ } else {
+ createEventWithDialog();
+ }
+}
+
+/**
+ * Handler function for selection in the unifinder.
+ *
+ * @param event The DOM selection event.
+ */
+function unifinderSelect(event) {
+ let tree = unifinderTreeView.treeElement;
+ if (!tree.view.selection || tree.view.selection.getRangeCount() == 0) {
+ return;
+ }
+
+ let selectedItems = [];
+ gCalendarEventTreeClicked = true;
+
+ // Get the selected events from the tree
+ let start = {};
+ let end = {};
+ let numRanges = tree.view.selection.getRangeCount();
+
+ for (let range = 0; range < numRanges; range++) {
+ tree.view.selection.getRangeAt(range, start, end);
+
+ for (let i = start.value; i <= end.value; i++) {
+ try {
+ selectedItems.push(unifinderTreeView.getItemAt(i));
+ } catch (e) {
+ WARN("Error getting Event from row: " + e + "\n");
+ }
+ }
+ }
+
+ if (selectedItems.length == 1) {
+ // Go to the day of the selected item in the current view.
+ currentView().goToDay(selectedItems[0].startDate);
+ }
+
+ // Set up the selected items in the view. Pass in true, so we don't end
+ // up in a circular loop
+ currentView().setSelectedItems(selectedItems.length, selectedItems, true);
+ currentView().centerSelectedItems();
+ calendarController.onSelectionChanged({ detail: selectedItems });
+ document.getElementById("unifinder-search-results-tree").focus();
+}
+
+/**
+ * Handler function for keypress in the unifinder.
+ *
+ * @param aEvent The DOM Key event.
+ */
+function unifinderKeyPress(aEvent) {
+ const kKE = Components.interfaces.nsIDOMKeyEvent;
+ switch (aEvent.keyCode) {
+ case 13:
+ // Enter, edit the event
+ editSelectedEvents();
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ case kKE.DOM_VK_BACK_SPACE:
+ case kKE.DOM_VK_DELETE:
+ deleteSelectedEvents();
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+}
+
+/**
+ * Tree controller for unifinder search results
+ */
+var unifinderTreeView = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsITreeView]),
+
+ // Provide a default tree that holds all the functions used here to avoid
+ // cludgy if (this.tree) { this.tree.rowCountChanged(...); } constructs.
+ tree: {
+ rowCountChanged: function() {},
+ beginUpdateBatch: function() {},
+ endUpdateBatch: function() {},
+ invalidate: function() {}
+ },
+
+ treeElement: null,
+ doingSelection: false,
+ mFilter: null,
+ mSelectedColumn: null,
+ sortDirection: null,
+
+ /**
+ * Returns the currently selected column in the unifinder (used for sorting).
+ */
+ get selectedColumn() {
+ return this.mSelectedColumn;
+ },
+
+ /**
+ * Sets the currently selected column in the unifinder (used for sorting).
+ */
+ set selectedColumn(aCol) {
+ let tree = document.getElementById("unifinder-search-results-tree");
+ let treecols = tree.getElementsByTagName("treecol");
+ for (let col of treecols) {
+ if (col.getAttribute("sortActive")) {
+ col.removeAttribute("sortActive");
+ col.removeAttribute("sortDirection");
+ }
+ if (aCol.getAttribute("itemproperty") == col.getAttribute("itemproperty")) {
+ col.setAttribute("sortActive", "true");
+ col.setAttribute("sortDirection", this.sortDirection);
+ }
+ }
+ return (this.mSelectedColumn = aCol);
+ },
+
+ /**
+ * Event functions
+ */
+
+ eventArray: [],
+ eventIndexMap: {},
+
+ /**
+ * Add an item to the unifinder tree.
+ *
+ * @param aItemArray An array of items to add.
+ * @param aDontSort If true, the items will only be appended.
+ */
+ addItems: function(aItemArray, aDontSort) {
+ this.eventArray = this.eventArray.concat(aItemArray);
+ let newCount = (this.eventArray.length - aItemArray.length - 1);
+ this.tree.rowCountChanged(newCount, aItemArray.length);
+
+ if (aDontSort) {
+ this.calculateIndexMap();
+ } else {
+ this.sortItems();
+ }
+ },
+
+ /**
+ * Remove items from the unifinder tree.
+ *
+ * @param aItemArray An array of items to remove.
+ */
+ removeItems: function(aItemArray) {
+ let indexesToRemove = [];
+ // Removing items is a bit tricky. Our getItemRow function takes the
+ // index from a cached map, so removing an item from the array will
+ // remove the wrong indexes. We don't want to just invalidate the map,
+ // since this will cause O(n^2) behavior. Instead, we keep a sorted
+ // array of the indexes to remove:
+ for (let item of aItemArray) {
+ let row = this.getItemRow(item);
+ if (row > -1) {
+ if (!indexesToRemove.length || row <= indexesToRemove[0]) {
+ indexesToRemove.unshift(row);
+ } else {
+ indexesToRemove.push(row);
+ }
+ }
+ }
+
+ // Then we go through the indexes to remove, and remove then from the
+ // array. We subtract one delta for each removed index to make sure the
+ // correct element is removed from the array and the correct
+ // notification is sent.
+ this.tree.beginUpdateBatch();
+ for (let delta = 0; delta < indexesToRemove.length; delta++) {
+ let index = indexesToRemove[delta];
+ this.eventArray.splice(index - delta, 1);
+ this.tree.rowCountChanged(index - delta, -1);
+ }
+ this.tree.endUpdateBatch();
+
+ // Finally, we recalculate the index map once. This way we end up with
+ // (given that Array.unshift doesn't loop but just prepends or maps
+ // memory smartly) O(3n) behavior. Lets hope its worth it.
+ this.calculateIndexMap(true);
+ },
+
+ /**
+ * Clear all items from the unifinder.
+ */
+ clearItems: function() {
+ let oldCount = this.eventArray.length;
+ this.eventArray = [];
+ this.tree.rowCountChanged(0, -oldCount);
+ this.calculateIndexMap();
+ },
+
+ /**
+ * Sets the items that should be in the unifinder. This removes all items
+ * that were previously in the unifinder.
+ */
+ setItems: function(aItemArray, aDontSort) {
+ let oldCount = this.eventArray.length;
+ this.eventArray = aItemArray.slice(0);
+ this.tree.rowCountChanged(oldCount - 1, this.eventArray.length - oldCount);
+
+ if (aDontSort) {
+ this.calculateIndexMap();
+ } else {
+ this.sortItems();
+ }
+ },
+
+ /**
+ * Recalculate the index map that improves performance when accessing
+ * unifinder items. This is usually done automatically when adding/removing
+ * items.
+ *
+ * @param aDontInvalidate (optional) Don't invalidate the tree, i.e if
+ * you correctly issued rowCountChanged
+ * notices.
+ */
+ calculateIndexMap: function(aDontInvalidate) {
+ this.eventIndexMap = {};
+ for (let i = 0; i < this.eventArray.length; i++) {
+ this.eventIndexMap[this.eventArray[i].hashId] = i;
+ }
+
+ if (!aDontInvalidate) {
+ this.tree.invalidate();
+ }
+ },
+
+ /**
+ * Sort the items in the unifinder by the currently selected column.
+ */
+ sortItems: function() {
+ if (this.selectedColumn) {
+ let modifier = (this.sortDirection == "descending" ? -1 : 1);
+ let sortKey = unifinderTreeView.selectedColumn.getAttribute("itemproperty");
+ let sortType = cal.getSortTypeForSortKey(sortKey);
+ // sort (key,item) entries
+ cal.sortEntry.mSortKey = sortKey;
+ cal.sortEntry.mSortStartedDate = now();
+ let entries = this.eventArray.map(cal.sortEntry, cal.sortEntry);
+ entries.sort(cal.sortEntryComparer(sortType, modifier));
+ this.eventArray = entries.map(cal.sortEntryItem);
+ }
+ this.calculateIndexMap();
+ },
+
+ /**
+ * Get the index of the row associated with the passed item.
+ *
+ * @param item The item to search for.
+ * @return The row index of the passed item.
+ */
+ getItemRow: function(item) {
+ if (this.eventIndexMap[item.hashId] === undefined) {
+ return -1;
+ }
+ return this.eventIndexMap[item.hashId];
+ },
+
+ /**
+ * Get the item at the given row index.
+ *
+ * @param item The row index to get the item for.
+ * @return The item at the given row.
+ */
+ getItemAt: function(aRow) {
+ return this.eventArray[aRow];
+ },
+
+ /**
+ * Get the calendar item from the given DOM event
+ *
+ * @param event The DOM mouse event to get the item for.
+ * @return The item under the mouse position.
+ */
+ getItemFromEvent: function(event) {
+ let row = this.tree.getRowAt(event.clientX, event.clientY);
+
+ if (row > -1) {
+ return this.getItemAt(row);
+ }
+ return null;
+ },
+
+ /**
+ * Change the selection in the unifinder.
+ *
+ * @param aItemArray An array of items to select.
+ */
+ setSelectedItems: function(aItemArray) {
+ if (this.doingSelection || !this.tree || !this.tree.view) {
+ return;
+ }
+
+ this.doingSelection = true;
+
+ // If no items were passed, get the selected items from the view.
+ aItemArray = aItemArray || currentView().getSelectedItems({});
+
+ calendarUpdateDeleteCommand(aItemArray);
+
+ /**
+ * The following is a brutal hack, caused by
+ * http://lxr.mozilla.org/mozilla1.0/source/layout/xul/base/src/tree/src/nsTreeSelection.cpp#555
+ * and described in bug 168211
+ * http://bugzilla.mozilla.org/show_bug.cgi?id=168211
+ * Do NOT remove anything in the next 3 lines, or the selection in the tree will not work.
+ */
+ this.treeElement.onselect = null;
+ this.treeElement.removeEventListener("select", unifinderSelect, true);
+ this.tree.view.selection.selectEventsSuppressed = true;
+ this.tree.view.selection.clearSelection();
+
+ if (aItemArray && aItemArray.length == 1) {
+ // If only one item is selected, scroll to it
+ let rowToScrollTo = this.getItemRow(aItemArray[0]);
+ if (rowToScrollTo > -1) {
+ this.tree.ensureRowIsVisible(rowToScrollTo);
+ this.tree.view.selection.select(rowToScrollTo);
+ }
+ } else if (aItemArray && aItemArray.length > 1) {
+ // If there is more than one item, just select them all.
+ for (let item of aItemArray) {
+ let row = this.getItemRow(item);
+ this.tree.view.selection.rangedSelect(row, row, true);
+ }
+ }
+
+ // This needs to be in a setTimeout
+ setTimeout(() => unifinderTreeView.resetAllowSelection(), 1);
+ },
+
+ /**
+ * Due to a selection issue described in bug 168211 this method is needed to
+ * re-add the selection listeners selection listeners.
+ */
+ resetAllowSelection: function() {
+ if (!this.tree) {
+ return;
+ }
+ /**
+ * Do not change anything in the following lines, they are needed as
+ * described in the selection observer above
+ */
+ this.doingSelection = false;
+
+ this.tree.view.selection.selectEventsSuppressed = false;
+ this.treeElement.addEventListener("select", unifinderSelect, true);
+ },
+
+ /**
+ * Tree View Implementation
+ * @see nsITreeView
+ */
+ get rowCount() {
+ return this.eventArray.length;
+ },
+
+
+ // TODO this code is currently identical to the task tree. We should create
+ // an itemTreeView that these tree views can inherit, that contains this
+ // code, and possibly other code related to sorting and storing items. See
+ // bug 432582 for more details.
+ getCellProperties: function(aRow, aCol) {
+ let rowProps = this.getRowProperties(aRow);
+ let colProps = this.getColumnProperties(aCol);
+ return rowProps + (rowProps && colProps ? " " : "") + colProps;
+ },
+ getRowProperties: function(aRow) {
+ let properties = [];
+ let item = this.eventArray[aRow];
+ if (item.priority > 0 && item.priority < 5) {
+ properties.push("highpriority");
+ } else if (item.priority > 5 && item.priority < 10) {
+ properties.push("lowpriority");
+ }
+
+ // Add calendar name atom
+ properties.push("calendar-" + formatStringForCSSRule(item.calendar.name));
+
+ // Add item status atom
+ if (item.status) {
+ properties.push("status-" + item.status.toLowerCase());
+ }
+
+ // Alarm status atom
+ if (item.getAlarms({}).length) {
+ properties.push("alarm");
+ }
+
+ // Task categories
+ properties = properties.concat(item.getCategories({})
+ .map(formatStringForCSSRule));
+
+ return properties.join(" ");
+ },
+ getColumnProperties: function(aCol) { return ""; },
+
+ isContainer: function() {
+ return false;
+ },
+
+ isContainerOpen: function(aRow) {
+ return false;
+ },
+
+ isContainerEmpty: function(aRow) {
+ return false;
+ },
+
+ isSeparator: function(aRow) {
+ return false;
+ },
+
+ isSorted: function(aRow) {
+ return false;
+ },
+
+ canDrop: function(aRow, aOrientation) {
+ return false;
+ },
+
+ drop: function(aRow, aOrientation) {},
+
+ getParentIndex: function(aRow) {
+ return -1;
+ },
+
+ hasNextSibling: function(aRow, aAfterIndex) {},
+
+ getLevel: function(aRow) {
+ return 0;
+ },
+
+ getImageSrc: function(aRow, aOrientation) {},
+
+ getProgressMode: function(aRow, aCol) {},
+
+ getCellValue: function(aRow, aCol) {
+ return null;
+ },
+
+ getCellText: function(row, column) {
+ let calendarEvent = this.eventArray[row];
+
+ switch (column.element.getAttribute("itemproperty")) {
+ case "title": {
+ return (calendarEvent.title ? calendarEvent.title.replace(/\n/g, " ") : "");
+ }
+ case "startDate": {
+ return formatUnifinderEventDateTime(calendarEvent.startDate);
+ }
+ case "endDate": {
+ let eventEndDate = calendarEvent.endDate.clone();
+ // XXX reimplement
+ // let eventEndDate = getCurrentNextOrPreviousRecurrence(calendarEvent);
+ if (calendarEvent.startDate.isDate) {
+ // display enddate is ical enddate - 1
+ eventEndDate.day = eventEndDate.day - 1;
+ }
+ return formatUnifinderEventDateTime(eventEndDate);
+ }
+ case "categories": {
+ return calendarEvent.getCategories({}).join(", ");
+ }
+ case "location": {
+ return calendarEvent.getProperty("LOCATION");
+ }
+ case "status": {
+ return getEventStatusString(calendarEvent);
+ }
+ case "calendar": {
+ return calendarEvent.calendar.name;
+ }
+ default: {
+ return false;
+ }
+ }
+ },
+
+ setTree: function(tree) {
+ this.tree = tree;
+ },
+
+ toggleOpenState: function(aRow) {},
+
+ cycleHeader: function(col) {
+ if (!this.selectedColumn) {
+ this.sortDirection = "ascending";
+ } else if (!this.sortDirection || this.sortDirection == "descending") {
+ this.sortDirection = "ascending";
+ } else {
+ this.sortDirection = "descending";
+ }
+ this.selectedColumn = col.element;
+ this.sortItems();
+ },
+
+ isEditable: function(aRow, aCol) {
+ return false;
+ },
+
+ setCellValue: function(aRow, aCol, aValue) {},
+ setCellText: function(aRow, aCol, aValue) {},
+
+ performAction: function(aAction) {},
+
+ performActionOnRow: function(aAction, aRow) {},
+
+ performActionOnCell: function(aAction, aRow, aCol) {},
+
+ outParameter: {} // used to obtain dates during sort
+};
+
+/**
+ * Refresh the unifinder tree by getting items from the composite calendar and
+ * applying the current filter.
+ */
+function refreshEventTree() {
+ let field = document.getElementById("unifinder-search-field");
+ if (field) {
+ unifinderTreeView.mFilter.filterText = field.value;
+ }
+
+ addItemsFromCalendar(getCompositeCalendar(),
+ addItemsFromCompositeCalendarInternal);
+}
+
+/**
+ * EXTENSION_POINTS
+ * Filters the passed event array according to the currently applied filter.
+ * Afterwards, applies the items to the unifinder view.
+ *
+ * If you are implementing a new filter, you can overwrite this function and
+ * filter the items accordingly and afterwards call this function with the
+ * result.
+ *
+ * @param eventArray The array of items to be set in the unifinder.
+ */
+function addItemsFromCompositeCalendarInternal(eventArray) {
+ let newItems = eventArray.filter(unifinderTreeView.mFilter.isItemInFilters,
+ unifinderTreeView.mFilter);
+ unifinderTreeView.setItems(newItems);
+
+ // Select selected events in the tree. Not passing the argument gets the
+ // items from the view.
+ unifinderTreeView.setSelectedItems();
+}
+
+function addItemsFromSingleCalendarInternal(eventArray) {
+ let newItems = eventArray.filter(unifinderTreeView.mFilter.isItemInFilters,
+ unifinderTreeView.mFilter);
+ unifinderTreeView.setItems(unifinderTreeView.eventArray.concat(newItems));
+
+ // Select selected events in the tree. Not passing the argument gets the
+ // items from the view.
+ unifinderTreeView.setSelectedItems();
+}
+
+function addItemsFromCalendar(aCalendar, aAddItemsInternalFunc) {
+ if (isUnifinderHidden()) {
+ // If the unifinder is hidden, don't refresh the events to reduce needed
+ // getItems calls.
+ return;
+ }
+ let refreshListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ mEventArray: [],
+
+ onOperationComplete: function(aOpCalendar, aStatus, aOperationType, aId, aDateTime) {
+ let refreshTreeInternalFunc = function() {
+ aAddItemsInternalFunc(refreshListener.mEventArray);
+ };
+ setTimeout(refreshTreeInternalFunc, 0);
+ },
+
+ onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ refreshListener.mEventArray = refreshListener.mEventArray.concat(aItems);
+ }
+ };
+
+ let filter = 0;
+
+ filter |= aCalendar.ITEM_FILTER_TYPE_EVENT;
+
+ // Not all xul might be there yet...
+ if (!document.getElementById("unifinder-search-field")) {
+ return;
+ }
+ unifinderTreeView.mFilter.applyFilter(getCurrentUnifinderFilter());
+
+ if (unifinderTreeView.mFilter.startDate && unifinderTreeView.mFilter.endDate) {
+ filter |= aCalendar.ITEM_FILTER_CLASS_OCCURRENCES;
+ }
+
+ aCalendar.getItems(filter,
+ 0,
+ unifinderTreeView.mFilter.startDate,
+ unifinderTreeView.mFilter.endDate,
+ refreshListener);
+}
+
+function deleteItemsFromCalendar(aCalendar) {
+ let filter = unifinderTreeView.mFilter;
+ let items = unifinderTreeView.eventArray.filter(item => item.calendar.id == aCalendar.id);
+
+ unifinderTreeView.removeItems(items.filter(filter.isItemInFilters, filter));
+}
+
+/**
+ * Focuses the unifinder search field
+ */
+function focusSearch() {
+ document.getElementById("unifinder-search-field").focus();
+}
+
+/**
+ * Toggles the hidden state of the unifinder.
+ */
+function toggleUnifinder() {
+ // Toggle the elements
+ goToggleToolbar("bottom-events-box", "calendar_show_unifinder_command");
+ goToggleToolbar("calendar-view-splitter");
+
+ unifinderTreeView.treeElement.view = unifinderTreeView;
+
+ // When the unifinder is hidden, refreshEventTree is not called. Make sure
+ // the event tree is refreshed now.
+ if (!isUnifinderHidden() && gUnifinderNeedsRefresh) {
+ gUnifinderNeedsRefresh = false;
+ refreshEventTree();
+ }
+
+ // Make sure the selection is correct
+ if (unifinderTreeView.doingSelection) {
+ unifinderTreeView.resetAllowSelection();
+ }
+ unifinderTreeView.setSelectedItems();
+}
+
+window.addEventListener("load", prepareCalendarUnifinder, false);
+window.addEventListener("unload", finishCalendarUnifinder, false);
diff --git a/calendar/base/content/calendar-unifinder.xul b/calendar/base/content/calendar-unifinder.xul
new file mode 100644
index 000000000..e8756aadb
--- /dev/null
+++ b/calendar/base/content/calendar-unifinder.xul
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://calendar/skin/calendar-unifinder.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://calendar/content/calendar-unifinder.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <vbox id="calendar-view-box">
+ <vbox id="bottom-events-box" insertbefore="calendar-nav-control" persist="height">
+ <hbox id="unifinder-searchBox" persist="collapsed">
+ <box align="center">
+ <menulist id="event-filter-menulist" value="P7D" persist="value">
+ <menupopup id="event-filter-menupopup" oncommand="refreshEventTree()">
+ <menuitem id="event-filter-all"
+ label="&calendar.events.filter.all.label;"
+ value="all"/>
+ <menuitem id="event-filter-today"
+ label="&calendar.events.filter.today.label;"
+ value="today"/>
+ <menuitem id="event-filter-next7days"
+ label="&calendar.events.filter.next7Days.label;"
+ value="P7D"/>
+ <menuitem id="event-filter-next14Days"
+ label="&calendar.events.filter.next14Days.label;"
+ value="P14D"/>
+ <menuitem id="event-filter-next31Days"
+ label="&calendar.events.filter.next31Days.label;"
+ value="P31D"/>
+ <menuitem id="event-filter-thisCalendarMonth"
+ label="&calendar.events.filter.thisCalendarMonth.label;"
+ value="thisCalendarMonth"/>
+ <menuitem id="event-filter-future"
+ label="&calendar.events.filter.future.label;"
+ value="future"/>
+ <menuitem id="event-filter-current"
+ label="&calendar.events.filter.current.label;"
+ value="current"/>
+ <menuitem id="event-filter-currentview"
+ label="&calendar.events.filter.currentview.label;"
+ value="currentview"/>
+ </menupopup>
+ </menulist>
+ </box>
+ <box align="center" flex="1">
+ <label control="unifinder-search-field" value="&calendar.search.options.searchfor;"/>
+ <textbox id="unifinder-search-field"
+ class="searchBox"
+ type="search"
+ oncommand="refreshEventTree();"
+ flex="1"/>
+ </box>
+ <toolbarbutton id="unifinder-closer"
+ class="unifinder-closebutton"
+ command="calendar_show_unifinder_command"
+ tooltiptext="&calendar.unifinder.close.tooltip;"/>
+ </hbox>
+ <tree id="unifinder-search-results-tree" flex="1"
+ onselect="unifinderSelect(event); calendarController.onSelectionChanged()"
+ onkeypress="unifinderKeyPress(event)"
+ _selectDelay="500"
+ persist="sort-active sort-direction"
+ enableColumnDrag="true">
+ <treecols id="unifinder-search-results-tree-cols">
+ <treecol id="unifinder-search-results-tree-col-title"
+ persist="hidden ordinal width"
+ flex="1"
+ itemproperty="title"
+ label="&calendar.unifinder.tree.title.label;"
+ tooltiptext="&calendar.unifinder.tree.title.tooltip2;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unifinder-search-results-tree-col-startdate"
+ persist="hidden ordinal width"
+ flex="1"
+ itemproperty="startDate"
+ label="&calendar.unifinder.tree.startdate.label;"
+ tooltiptext="&calendar.unifinder.tree.startdate.tooltip2;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unifinder-search-results-tree-col-enddate"
+ persist="hidden ordinal width"
+ flex="1"
+ itemproperty="endDate"
+ label="&calendar.unifinder.tree.enddate.label;"
+ tooltiptext="&calendar.unifinder.tree.enddate.tooltip2;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unifinder-search-results-tree-col-categories"
+ persist="hidden ordinal width"
+ flex="1"
+ itemproperty="categories"
+ label="&calendar.unifinder.tree.categories.label;"
+ tooltiptext="&calendar.unifinder.tree.categories.tooltip2;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unifinder-search-results-tree-col-location"
+ persist="hidden ordinal width"
+ flex="1"
+ hidden="true"
+ itemproperty="location"
+ label="&calendar.unifinder.tree.location.label;"
+ tooltiptext="&calendar.unifinder.tree.location.tooltip2;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unifinder-search-results-tree-col-status"
+ persist="hidden ordinal width"
+ flex="1"
+ hidden="true"
+ itemproperty="status"
+ label="&calendar.unifinder.tree.status.label;"
+ tooltiptext="&calendar.unifinder.tree.status.tooltip2;"/>
+ <treecol id="unifinder-search-results-tree-col-calendarname"
+ persist="hidden ordinal width"
+ flex="1"
+ hidden="true"
+ itemproperty="calendar"
+ label="&calendar.unifinder.tree.calendarname.label;"
+ tooltiptext="&calendar.unifinder.tree.calendarname.tooltip2;"/>
+ </treecols>
+
+ <!-- on mousedown here happens before onclick above -->
+ <treechildren tooltip="eventTreeTooltip"
+ context="calendar-item-context-menu"
+ onkeypress="if (event.keyCode == 13) unifinderEditCommand();"
+ ondragenter="return false;"
+ ondblclick="unifinderDoubleClick(event)"
+ onfocus="focusFirstItemIfNoSelection( );"/>
+ </tree>
+ </vbox>
+ <splitter id="calendar-view-splitter"
+ insertbefore="calendar-nav-control"
+ resizebefore="closest"
+ resizeafter="farthest"
+ persist="state"
+ class="chromeclass-extrachrome sidebar-splitter calendar-splitter"
+ orient="vertical"
+ onmouseup="setTimeout(refreshEventTree, 10);"/>
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/calendar-view-bindings.css b/calendar/base/content/calendar-view-bindings.css
new file mode 100644
index 000000000..7cffd605d
--- /dev/null
+++ b/calendar/base/content/calendar-view-bindings.css
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+calendar-event-column {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-event-column);
+}
+
+calendar-event-box {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-event-box);
+}
+
+calendar-event-gripbar {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-event-gripbar);
+}
+
+calendar-time-bar {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-time-bar);
+}
+
+calendar-multiday-view {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-multiday-view);
+}
+
+calendar-day-label {
+ -moz-binding: url(chrome://calendar/content/calendar-base-view.xml#calendar-day-label);
+}
+
+nav-day-label {
+ -moz-binding: url(chrome://calendar/content/calendar-base-view.xml#nav-day-label);
+}
+
+calendar-base-view {
+ -moz-binding: url(chrome://calendar/content/calendar-base-view.xml#calendar-base-view);
+}
+
+calendar-header-container {
+ -moz-binding: url(chrome://calendar/content/calendar-multiday-view.xml#calendar-header-container);
+}
+
+/* Month View */
+calendar-month-base-view {
+ -moz-binding: url(chrome://calendar/content/calendar-month-view.xml#calendar-month-base-view);
+}
+
+calendar-base-view {
+ -moz-binding: url(chrome://calendar/content/calendar-base-view.xml#calendar-base-view);
+ -moz-user-focus: normal;
+}
+
+calendar-month-day-box {
+ -moz-binding: url(chrome://calendar/content/calendar-month-view.xml#calendar-month-day-box);
+}
+
+calendar-month-day-box-item {
+ -moz-binding: url(chrome://calendar/content/calendar-month-view.xml#calendar-month-day-box-item);
+}
+
+/* View core */
+calendar-editable-item {
+ -moz-binding: url(chrome://calendar/content/calendar-view-core.xml#calendar-editable-item);
+}
+
+calendar-category-box {
+ -moz-binding: url(chrome://calendar/content/calendar-view-core.xml#calendar-category-box);
+}
+
+ calendar-shadow-box {
+ -moz-binding: url(chrome://calendar/content/calendar-view-core.xml#calendar-shadow-box);
+ }
diff --git a/calendar/base/content/calendar-view-core.xml b/calendar/base/content/calendar-view-core.xml
new file mode 100644
index 000000000..35f70c676
--- /dev/null
+++ b/calendar/base/content/calendar-view-core.xml
@@ -0,0 +1,389 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Note that this file depends on helper functions in calUtils.js-->
+
+<bindings id="calendar-core-view-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="calendar-editable-item">
+ <content mousethrough="never"
+ tooltip="itemTooltip"
+ tabindex="-1">
+ <xul:vbox flex="1">
+ <xul:hbox>
+ <xul:box anonid="event-container"
+ class="calendar-color-box"
+ xbl:inherits="calendar-uri,calendar-id"
+ flex="1">
+ <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
+ <xul:stack anonid="eventbox"
+ class="calendar-event-box-container"
+ flex="1"
+ xbl:inherits="readonly,flashing,alarm,allday,priority,progress,status,calendar,categories">
+ <xul:hbox class="calendar-event-details">
+ <xul:vbox align="left"
+ flex="1"
+ xbl:inherits="context">
+ <xul:label anonid="event-name"
+ crop="end"
+ flex="1"
+ style="margin: 0;"/>
+ <xul:textbox anonid="event-name-textbox"
+ class="plain"
+ crop="end"
+ hidden="true"
+ style="background: transparent !important;"
+ wrap="true"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ <xul:stack>
+ <xul:calendar-category-box anonid="category-box"
+ xbl:inherits="categories"
+ pack="end"/>
+ <xul:hbox align="center">
+ <xul:hbox anonid="alarm-icons-box"
+ class="alarm-icons-box"
+ align="center"
+ xbl:inherits="flashing"/>
+ <xul:image anonid="item-classification-box"
+ class="item-classification-box"
+ pack="end"/>
+ </xul:hbox>
+ </xul:stack>
+ </xul:hbox>
+ </xul:stack>
+ </xul:box>
+ </xul:box>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.eventNameTextbox.onblur = () => {
+ this.stopEditing(true);
+ };
+ this.eventNameTextbox.onkeypress = (event) => {
+ // save on enter
+ if (event.keyCode == 13) {
+ this.stopEditing(true);
+ // abort on escape
+ } else if (event.keyCode == 27) {
+ this.stopEditing(false);
+ }
+ };
+ let stopPropagationIfEditing = (event) => {
+ if (this.mEditing) {
+ event.stopPropagation();
+ }
+ };
+ // while editing, single click positions cursor, so don't propagate.
+ this.eventNameTextbox.onclick = stopPropagationIfEditing;
+ // while editing, double click selects words, so don't propagate.
+ this.eventNameTextbox.ondblclick = stopPropagationIfEditing;
+ // while editing, don't propagate mousedown/up (selects calEvent).
+ this.eventNameTextbox.onmousedown = stopPropagationIfEditing;
+ this.eventNameTextbox.onmouseup = stopPropagationIfEditing;
+ ]]></constructor>
+
+ <field name="mOccurrence">null</field>
+ <field name="mSelected">false</field>
+ <field name="mCalendarView">null</field>
+
+ <property name="parentBox"
+ onget="return this.mParentBox;"
+ onset="this.mParentBox = val;"/>
+
+ <property name="selected">
+ <getter><![CDATA[
+ return this.mSelected;
+ ]]></getter>
+ <setter><![CDATA[
+ if (val && !this.mSelected) {
+ this.mSelected = true;
+ this.setAttribute("selected", "true");
+ } else if (!val && this.mSelected) {
+ this.mSelected = false;
+ this.removeAttribute("selected");
+ }
+ return val;
+ ]]></setter>
+ </property>
+ <property name="calendarView">
+ <getter><![CDATA[
+ return this.mCalendarView;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mCalendarView = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="occurrence">
+ <getter><![CDATA[
+ return this.mOccurrence;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mOccurrence = val;
+ this.setEditableLabel();
+ this.setCSSClasses();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="eventNameLabel" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'event-name');"/>
+ <property name="eventNameTextbox" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'event-name-textbox');"/>
+
+ <method name="setEditableLabel">
+ <body><![CDATA[
+ let evl = this.eventNameLabel;
+ let item = this.mOccurrence;
+ evl.value = (item.title ? item.title.replace(/\n/g, " ")
+ : cal.calGetString("calendar", "eventUntitled"));
+ ]]></body>
+ </method>
+
+ <method name="setCSSClasses">
+ <body><![CDATA[
+ let item = this.mOccurrence;
+ this.setAttribute("calendar-uri", item.calendar.uri.spec);
+ this.setAttribute("calendar-id", item.calendar.id);
+ let categoriesArray = item.getCategories({});
+ if (categoriesArray.length > 0) {
+ let cssClassesArray = categoriesArray.map(formatStringForCSSRule);
+ this.setAttribute("categories", cssClassesArray.join(" "));
+ }
+
+ // Add alarm icons as needed.
+ let alarms = item.getAlarms({});
+ if (alarms.length && Preferences.get("calendar.alarms.indicator.show", true)) {
+ let iconsBox = document.getAnonymousElementByAttribute(this, "anonid", "alarm-icons-box");
+ cal.alarms.addReminderImages(iconsBox, alarms);
+
+ // Set suppressed status on the icons box
+ setElementValue(iconsBox,
+ item.calendar.getProperty("suppressAlarms") || false,
+ "suppressed");
+ }
+
+ // Item classification / privacy
+ let classificationBox = document.getAnonymousElementByAttribute(this, "anonid", "item-classification-box");
+ if (classificationBox) {
+ classificationBox.setAttribute("classification", item.privacy || "PUBLIC");
+ }
+
+ // Set up event box attributes for use in css selectors. Note if
+ // something is added here, it should also be xbl:inherited correctly
+ // in the <content> section of this binding, and all that inherit it.
+
+ // Event type specific properties
+ if (cal.isEvent(item)) {
+ if (item.startDate.isDate) {
+ this.setAttribute("allday", "true");
+ }
+ this.setAttribute("itemType", "event");
+ } else if (cal.isToDo(item)) {
+ // progress attribute
+ this.setAttribute("progress", getProgressAtom(item));
+ // Attribute for tasks and tasks image.
+ this.setAttribute("itemType", "todo");
+ if (item.entryDate && !item.dueDate) {
+ this.setAttribute("todoType", "start");
+ } else if (!item.entryDate && item.dueDate) {
+ this.setAttribute("todoType", "end");
+ }
+ }
+
+ if (this.calendarView &&
+ item.hashId in this.calendarView.mFlashingEvents) {
+ this.setAttribute("flashing", "true");
+ }
+
+ if (alarms.length) {
+ this.setAttribute("alarm", "true");
+ }
+
+ // priority
+ if (item.priority > 0 && item.priority < 5) {
+ this.setAttribute("priority", "high");
+ } else if (item.priority > 5 && item.priority < 10) {
+ this.setAttribute("priority", "low");
+ }
+
+ // status attribute
+ if (item.status) {
+ this.setAttribute("status", item.status.toUpperCase());
+ }
+
+ // item class
+ if (item.hasProperty("CLASS")) {
+ this.setAttribute("itemclass", item.getProperty("CLASS"));
+ }
+
+ // calendar name
+ this.setAttribute("calendar", item.calendar.name.toLowerCase());
+
+ // Invitation
+ if (cal.isInvitation(item)) {
+ this.setAttribute("invitation-status", cal.getInvitedAttendee(item).participationStatus);
+ this.setAttribute("readonly", "true");
+ } else if (!isCalendarWritable(item.calendar)) {
+ this.setAttribute("readonly", "true");
+ }
+ ]]></body>
+ </method>
+
+ <method name="startEditing">
+ <body><![CDATA[
+ this.editingTimer = null;
+ this.mOriginalTextLabel = this.mOccurrence.title;
+
+ this.eventNameLabel.setAttribute("hidden", "true");
+
+ this.mEditing = true;
+
+ this.eventNameTextbox.value = this.mOriginalTextLabel;
+ this.eventNameTextbox.removeAttribute("hidden");
+ this.eventNameTextbox.select();
+ ]]></body>
+ </method>
+ <method name="select">
+ <parameter name="event"/>
+ <body><![CDATA[
+ if (!this.calendarView) {
+ return;
+ }
+ let items = this.calendarView.mSelectedItems.slice();
+ if (event.ctrlKey || event.metaKey) {
+ if (this.selected) {
+ let pos = items.indexOf(this.mOccurrence);
+ items.splice(pos, 1);
+ } else {
+ items.push(this.mOccurrence);
+ }
+ } else {
+ items = [this.mOccurrence];
+ }
+ this.calendarView.setSelectedItems(items.length, items);
+ ]]></body>
+ </method>
+ <method name="stopEditing">
+ <parameter name="saveChanges"/>
+ <body><![CDATA[
+ if (!this.mEditing) {
+ return;
+ }
+
+ this.mEditing = false;
+
+ if (saveChanges && (this.eventNameTextbox.value != this.mOriginalTextLabel)) {
+ this.calendarView.controller.modifyOccurrence(this.mOccurrence,
+ null, null,
+ this.eventNameTextbox.value);
+
+ // Note that as soon as we do the modifyItem, this element ceases to exist,
+ // so don't bother trying to modify anything further here! ('this' exists,
+ // because it's being kept alive, but our child content etc. is all gone)
+ return;
+ }
+
+ this.eventNameTextbox.setAttribute("hidden", "true");
+ this.eventNameLabel.removeAttribute("hidden");
+ return;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="contextmenu" phase="capturing"><![CDATA[
+ // If the middle/right button was used for click just select the item.
+ if (!this.selected) {
+ this.select(event);
+ }
+ ]]></handler>
+ <handler event="click" button="0"><![CDATA[
+ if (this.mEditing) {
+ return;
+ }
+
+ // If the left button was used and the item is already selected
+ // and there are no multiple items selected start
+ // the 'single click edit' timeout. Otherwise select the item too.
+ // Also, check if the calendar is readOnly or we are offline.
+
+ if (this.selected && !(event.ctrlKey || event.metaKey) &&
+ isCalendarWritable(this.mOccurrence.calendar)) {
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ }
+ this.editingTimer = setTimeout(() => this.startEditing(), 350);
+ } else {
+ this.select(event);
+ event.stopPropagation();
+ }
+ ]]></handler>
+
+ <handler event="dblclick" button="0"><![CDATA[
+ event.stopPropagation();
+
+ // stop 'single click edit' timeout (if started)
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ this.editingTimer = null;
+ }
+
+ if (this.calendarView && this.calendarView.controller) {
+ let item = event.ctrlKey ? this.mOccurrence.parentItem : this.mOccurrence;
+ this.calendarView.controller.modifyOccurrence(item);
+ }
+ ]]></handler>
+ <handler event="mouseover"><![CDATA[
+ if (this.calendarView && this.calendarView.controller) {
+ event.stopPropagation();
+ onMouseOverItem(event);
+ }
+ ]]></handler>
+ <handler event="dragstart"><![CDATA[
+ if (event.target.localName == "calendar-event-box") {
+ return;
+ }
+ let item = this.occurrence;
+ let isInvitation = item.calendar instanceof Components.interfaces.calISchedulingSupport && item.calendar.isInvitation(item);
+ if (!isCalendarWritable(item.calendar) || !userCanModifyItem(item) || isInvitation) {
+ return;
+ }
+ if (!this.selected) {
+ this.select(event);
+ }
+ invokeEventDragSession(item, this);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-category-box">
+ <!-- calendar-views.css makes this binding hide if the categories attribute
+ is not specified -->
+ <content>
+ <xul:vbox anonid="category-box"
+ class="category-color-box calendar-event-selection"
+ xbl:inherits="categories">
+ <xul:hbox flex="1">
+ <xul:image class="calendar-category-box-gradient" height="1"/>
+ </xul:hbox>
+ <xul:hbox height="1"/>
+ </xul:vbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-views.js b/calendar/base/content/calendar-views.js
new file mode 100644
index 000000000..337fd673e
--- /dev/null
+++ b/calendar/base/content/calendar-views.js
@@ -0,0 +1,723 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported switchToView, getSelectedDay, scheduleMidnightUpdate,
+ * updateStyleSheetForViews, observeViewDaySelect, toggleOrientation,
+ * toggleWorkdaysOnly, toggleTasksInView, toggleShowCompletedInView,
+ * goToDate, getLastCalendarView, deleteSelectedEvents,
+ * editSelectedEvents, selectAllEvents
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/**
+ * Controller for the views
+ * @see calIcalendarViewController
+ */
+var calendarViewController = {
+ QueryInterface: function(aIID) {
+ if (!aIID.equals(Components.interfaces.calICalendarViewController) &&
+ !aIID.equals(Components.interfaces.nsISupports)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+
+ return this;
+ },
+
+ /**
+ * Creates a new event
+ * @see calICalendarViewController
+ */
+ createNewEvent: function(aCalendar, aStartTime, aEndTime, aForceAllday) {
+ // if we're given both times, skip the dialog
+ if (aStartTime && aEndTime && !aStartTime.isDate && !aEndTime.isDate) {
+ let item = cal.createEvent();
+ setDefaultItemValues(item, aCalendar, aStartTime, aEndTime);
+ item.title = calGetString("calendar", "newEvent");
+ doTransaction("add", item, item.calendar, null, null);
+ } else {
+ createEventWithDialog(aCalendar, aStartTime, null, null, null, aForceAllday);
+ }
+ },
+
+ /**
+ * Modifies the given occurrence
+ * @see calICalendarViewController
+ */
+ modifyOccurrence: function(aOccurrence, aNewStartTime, aNewEndTime, aNewTitle) {
+ // if modifying this item directly (e.g. just dragged to new time),
+ // then do so; otherwise pop up the dialog
+ if (aNewStartTime || aNewEndTime || aNewTitle) {
+ let instance = aOccurrence.clone();
+
+ if (aNewTitle) {
+ instance.title = aNewTitle;
+ }
+
+ // When we made the executive decision (in bug 352862) that
+ // dragging an occurrence of a recurring event would _only_ act
+ // upon _that_ occurrence, we removed a bunch of code from this
+ // function. If we ever revert that decision, check CVS history
+ // here to get that code back.
+
+ if (aNewStartTime || aNewEndTime) {
+ // Yay for variable names that make this next line look silly
+ if (isEvent(instance)) {
+ if (aNewStartTime && instance.startDate) {
+ instance.startDate = aNewStartTime;
+ }
+ if (aNewEndTime && instance.endDate) {
+ instance.endDate = aNewEndTime;
+ }
+ } else {
+ if (aNewStartTime && instance.entryDate) {
+ instance.entryDate = aNewStartTime;
+ }
+ if (aNewEndTime && instance.dueDate) {
+ instance.dueDate = aNewEndTime;
+ }
+ }
+ }
+
+ doTransaction("modify", instance, instance.calendar, aOccurrence, null);
+ } else {
+ modifyEventWithDialog(aOccurrence, null, true);
+ }
+ },
+
+ /**
+ * Deletes the given occurrences
+ * @see calICalendarViewController
+ */
+ deleteOccurrences: function(aCount,
+ aOccurrences,
+ aUseParentItems,
+ aDoNotConfirm) {
+ startBatchTransaction();
+ let recurringItems = {};
+
+ let getSavedItem = function(aItemToDelete) {
+ // Get the parent item, saving it in our recurringItems object for
+ // later use.
+ let hashVal = aItemToDelete.parentItem.hashId;
+ if (!recurringItems[hashVal]) {
+ recurringItems[hashVal] = {
+ oldItem: aItemToDelete.parentItem,
+ newItem: aItemToDelete.parentItem.clone()
+ };
+ }
+ return recurringItems[hashVal];
+ };
+
+ // Make sure we are modifying a copy of aOccurrences, otherwise we will
+ // run into race conditions when the view's doDeleteItem removes the
+ // array elements while we are iterating through them. While we are at
+ // it, filter out any items that have readonly calendars, so that
+ // checking for one total item below also works out if all but one item
+ // are readonly.
+ let occurrences = aOccurrences.filter(item => isCalendarWritable(item.calendar));
+
+ for (let itemToDelete of occurrences) {
+ if (aUseParentItems) {
+ // Usually happens when ctrl-click is used. In that case we
+ // don't need to ask the user if he wants to delete an
+ // occurrence or not.
+ itemToDelete = itemToDelete.parentItem;
+ } else if (!aDoNotConfirm && occurrences.length == 1) {
+ // Only give the user the selection if only one occurrence is
+ // selected. Otherwise he will get a dialog for each occurrence
+ // he deletes.
+ let [targetItem, , response] = promptOccurrenceModification(itemToDelete, false, "delete");
+ if (!response) {
+ // The user canceled the dialog, bail out
+ break;
+ }
+
+ itemToDelete = targetItem;
+ }
+
+ // Now some dirty work: Make sure more than one occurrence can be
+ // deleted by saving the recurring items and removing occurrences as
+ // they come in. If this is not an occurrence, we can go ahead and
+ // delete the whole item.
+ if (itemToDelete.parentItem.hashId == itemToDelete.hashId) {
+ doTransaction("delete", itemToDelete, itemToDelete.calendar, null, null);
+ } else {
+ let savedItem = getSavedItem(itemToDelete);
+ savedItem.newItem.recurrenceInfo
+ .removeOccurrenceAt(itemToDelete.recurrenceId);
+ // Dont start the transaction yet. Do so later, in case the
+ // parent item gets modified more than once.
+ }
+ }
+
+ // Now handle recurring events. This makes sure that all occurrences
+ // that have been passed are deleted.
+ for (let hashVal in recurringItems) {
+ let ritem = recurringItems[hashVal];
+ doTransaction("modify",
+ ritem.newItem,
+ ritem.newItem.calendar,
+ ritem.oldItem,
+ null);
+ }
+ endBatchTransaction();
+ }
+};
+
+/**
+ * This function does the common steps to switch between views. Should be called
+ * from app-specific view switching functions
+ *
+ * @param aViewType The type of view to select.
+ */
+function switchToView(aViewType) {
+ let viewDeck = getViewDeck();
+ let selectedDay;
+ let currentSelection = [];
+
+ // Set up the view commands
+ let views = viewDeck.childNodes;
+ for (let i = 0; i < views.length; i++) {
+ let view = views[i];
+ let commandId = "calendar_" + view.id + "_command";
+ let command = document.getElementById(commandId);
+ if (view.id == aViewType + "-view") {
+ command.setAttribute("checked", "true");
+ } else {
+ command.removeAttribute("checked");
+ }
+ }
+
+ /**
+ * Sets up a node to use view specific attributes. If there is no view
+ * specific attribute, then <attr>-all is used instead.
+ *
+ * @param id The id of the node to set up.
+ * @param attr The view specific attribute to modify.
+ */
+ function setupViewNode(id, attr) {
+ let node = document.getElementById(id);
+ if (node) {
+ if (node.hasAttribute(attr + "-" + aViewType)) {
+ node.setAttribute(attr, node.getAttribute(attr + "-" + aViewType));
+ } else {
+ node.setAttribute(attr, node.getAttribute(attr + "-all"));
+ }
+ }
+ }
+
+ // Set up the labels and accesskeys for the context menu
+ ["calendar-view-context-menu-next",
+ "calendar-view-context-menu-previous",
+ "calendar-go-menu-next",
+ "calendar-go-menu-previous",
+ "appmenu_calendar-go-menu-next",
+ "appmenu_calendar-go-menu-previous"].forEach((x) => {
+ setupViewNode(x, "label");
+ setupViewNode(x, "accesskey");
+ });
+
+ // Set up the labels for the view navigation
+ ["previous-view-button",
+ "today-view-button",
+ "next-view-button"].forEach(x => setupViewNode(x, "tooltiptext"));
+
+ try {
+ selectedDay = viewDeck.selectedPanel.selectedDay;
+ currentSelection = viewDeck.selectedPanel.getSelectedItems({});
+ } catch (ex) {
+ // This dies if no view has even been chosen this session, but that's
+ // ok because we'll just use now() below.
+ }
+
+ if (!selectedDay) {
+ selectedDay = now();
+ }
+
+ // Anyone wanting to plug in a view needs to follow this naming scheme
+ let view = document.getElementById(aViewType + "-view");
+ viewDeck.selectedPanel = view;
+
+ // Select the corresponding tab
+ let viewTabs = document.getElementById("view-tabs");
+ viewTabs.selectedIndex = getViewDeck().selectedIndex;
+
+ let compositeCal = getCompositeCalendar();
+ if (view.displayCalendar != compositeCal) {
+ view.displayCalendar = compositeCal;
+ view.timezone = calendarDefaultTimezone();
+ view.controller = calendarViewController;
+ }
+
+ view.goToDay(selectedDay);
+ view.setSelectedItems(currentSelection.length, currentSelection);
+
+ onCalendarViewResize();
+}
+
+/**
+ * Returns the calendar view deck XUL element.
+ *
+ * @return The view-deck element.
+ */
+function getViewDeck() {
+ return document.getElementById("view-deck");
+}
+
+/**
+ * Returns the currently selected calendar view.
+ *
+ * @return The selected calendar view
+ */
+function currentView() {
+ return getViewDeck().selectedPanel;
+}
+
+/**
+ * Returns the selected day in the current view.
+ *
+ * @return The selected day
+ */
+function getSelectedDay() {
+ return currentView().selectedDay;
+}
+
+var gMidnightTimer;
+
+/**
+ * Creates a timer that will fire after midnight. Pass in a function as
+ * aRefreshCallback that should be called at that time.
+ *
+ * XXX This function is not very usable, since there is only one midnight timer.
+ * Better would be a function that uses the observer service to notify at
+ * midnight.
+ *
+ * @param aRefreshCallback A callback to be called at midnight.
+ */
+function scheduleMidnightUpdate(aRefreshCallback) {
+ let jsNow = new Date();
+ let tomorrow = new Date(jsNow.getFullYear(), jsNow.getMonth(), jsNow.getDate() + 1);
+ let msUntilTomorrow = tomorrow.getTime() - jsNow.getTime();
+
+ // Is an nsITimer/callback extreme overkill here? Yes, but it's necessary to
+ // workaround bug 291386. If we don't, we stand a decent chance of getting
+ // stuck in an infinite loop.
+ let udCallback = {
+ notify: function(timer) {
+ aRefreshCallback();
+ }
+ };
+
+ if (gMidnightTimer) {
+ gMidnightTimer.cancel();
+ } else {
+ // Observer for wake after sleep/hibernate/standby to create new timers and refresh UI
+ let wakeObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "wake_notification") {
+ // postpone refresh for another couple of seconds to get netwerk ready:
+ if (this.mTimer) {
+ this.mTimer.cancel();
+ } else {
+ this.mTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ }
+ this.mTimer.initWithCallback(udCallback, 10 * 1000,
+ Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ }
+ }
+ };
+
+ // Add observer
+ Services.obs.addObserver(wakeObserver, "wake_notification", false);
+
+ // Remove observer on unload
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(wakeObserver, "wake_notification");
+ }, false);
+ gMidnightTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ }
+ gMidnightTimer.initWithCallback(udCallback, msUntilTomorrow, gMidnightTimer.TYPE_ONE_SHOT);
+}
+
+/**
+ * Retuns a cached copy of the view stylesheet.
+ *
+ * @return The view stylesheet object.
+ */
+function getViewStyleSheet() {
+ if (!getViewStyleSheet.sheet) {
+ const cssUri = "chrome://calendar/content/calendar-view-bindings.css";
+ for (let sheet of document.styleSheets) {
+ if (sheet.href == cssUri) {
+ getViewStyleSheet.sheet = sheet;
+ break;
+ }
+ }
+ }
+ return getViewStyleSheet.sheet;
+}
+
+/**
+ * Updates the view stylesheet to contain rules that give all boxes with class
+ * .calendar-color-box and an attribute calendar-id="<id of the calendar>" the
+ * background color of the specified calendar.
+ *
+ * @param aCalendar The calendar to update the stylesheet for.
+ */
+function updateStyleSheetForViews(aCalendar) {
+ if (!updateStyleSheetForViews.ruleCache) {
+ updateStyleSheetForViews.ruleCache = {};
+ }
+ let ruleCache = updateStyleSheetForViews.ruleCache;
+
+ if (!(aCalendar.id in ruleCache)) {
+ // We haven't create a rule for this calendar yet, do so now.
+ let sheet = getViewStyleSheet();
+ let ruleString = '.calendar-color-box[calendar-id="' + aCalendar.id + '"] {} ';
+ let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
+
+ ruleCache[aCalendar.id] = sheet.cssRules[ruleIndex];
+ }
+
+ let color = aCalendar.getProperty("color") || "#A8C2E1";
+ ruleCache[aCalendar.id].style.backgroundColor = color;
+ ruleCache[aCalendar.id].style.color = cal.getContrastingTextColor(color);
+}
+
+/**
+ * Category preferences observer. Used to update the stylesheets for category
+ * colors.
+ *
+ * Note we need to keep the categoryPrefBranch variable outside of
+ * initCategories since branch observers only live as long as the branch object
+ * is alive, and out of categoryManagement to avoid cyclic references.
+ */
+var categoryPrefBranch;
+var categoryManagement = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
+
+ initCategories: function() {
+ categoryPrefBranch = Services.prefs.getBranch("calendar.category.color.");
+ let categories = categoryPrefBranch.getChildList("");
+
+ // Fix illegally formatted category prefs.
+ for (let i in categories) {
+ let category = categories[i];
+ if (category.search(/[^_0-9a-z-]/) != -1) {
+ let categoryFix = formatStringForCSSRule(category);
+ if (categoryPrefBranch.prefHasUserValue(categoryFix)) {
+ categories.splice(i, 1); // remove illegal name
+ } else {
+ let color = categoryPrefBranch.getCharPref(category);
+ categoryPrefBranch.setCharPref(categoryFix, color);
+ categoryPrefBranch.clearUserPref(category); // not usable
+ categories[i] = categoryFix; // replace illegal name
+ }
+ }
+ }
+
+ // Add color information to the stylesheets.
+ categories.forEach(categoryManagement.updateStyleSheetForCategory,
+ categoryManagement);
+ categoryPrefBranch.addObserver("", categoryManagement, false);
+ },
+
+ cleanupCategories: function() {
+ categoryPrefBranch = Services.prefs.getBranch("calendar.category.color.");
+ categoryPrefBranch.removeObserver("", categoryManagement);
+ },
+
+ observe: function(aSubject, aTopic, aPrefName) {
+ this.updateStyleSheetForCategory(aPrefName);
+ // TODO Currently, the only way to find out if categories are removed is
+ // to initially grab the calendar.categories.names preference and then
+ // observe changes to it. it would be better if we had hooks for this,
+ // so we could delete the rule from our style cache and also remove its
+ // color preference.
+ },
+
+ categoryStyleCache: {},
+
+ updateStyleSheetForCategory: function(aCatName) {
+ if (!(aCatName in this.categoryStyleCache)) {
+ // We haven't created a rule for this category yet, do so now.
+ let sheet = getViewStyleSheet();
+ let ruleString = '.category-color-box[categories~="' + aCatName + '"] {} ';
+ let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
+
+ this.categoryStyleCache[aCatName] = sheet.cssRules[ruleIndex];
+ }
+
+ let color = Preferences.get("calendar.category.color." + aCatName) || "";
+ this.categoryStyleCache[aCatName].style.backgroundColor = color;
+ }
+};
+
+/**
+ * Handler function to set the selected day in the minimonth to the currently
+ * selected day in the current view.
+ *
+ * @param event The "dayselect" event emitted from the views.
+ *
+ */
+function observeViewDaySelect(event) {
+ let date = event.detail;
+ let jsDate = new Date(date.year, date.month, date.day);
+
+ // for the month and multiweek view find the main month,
+ // which is the month with the most visible days in the view;
+ // note, that the main date is the first day of the main month
+ let jsMainDate;
+ if (!event.originalTarget.supportsDisjointDates) {
+ let mainDate = null;
+ let maxVisibleDays = 0;
+ let startDay = currentView().startDay;
+ let endDay = currentView().endDay;
+ let firstMonth = startDay.startOfMonth;
+ let lastMonth = endDay.startOfMonth;
+ for (let month = firstMonth.clone(); month.compare(lastMonth) <= 0; month.month += 1) {
+ let visibleDays = 0;
+ if (month.compare(firstMonth) == 0) {
+ visibleDays = startDay.endOfMonth.day - startDay.day + 1;
+ } else if (month.compare(lastMonth) == 0) {
+ visibleDays = endDay.day;
+ } else {
+ visibleDays = month.endOfMonth.day;
+ }
+ if (visibleDays > maxVisibleDays) {
+ mainDate = month.clone();
+ maxVisibleDays = visibleDays;
+ }
+ }
+ jsMainDate = new Date(mainDate.year, mainDate.month, mainDate.day);
+ }
+
+ getMinimonth().selectDate(jsDate, jsMainDate);
+ currentView().focus();
+}
+
+/**
+ * Provides a neutral way to get the minimonth, regardless of whether we're in
+ * Sunbird or Lightning.
+ *
+ * @return The XUL minimonth element.
+ */
+function getMinimonth() {
+ return document.getElementById("calMinimonth");
+}
+
+/**
+ * Update the view orientation based on the checked state of the command
+ */
+function toggleOrientation() {
+ let cmd = document.getElementById("calendar_toggle_orientation_command");
+ let newValue = (cmd.getAttribute("checked") == "true" ? "false" : "true");
+ cmd.setAttribute("checked", newValue);
+
+ let deck = getViewDeck();
+ for (let view of deck.childNodes) {
+ view.rotated = (newValue == "true");
+ }
+
+ // orientation refreshes automatically
+}
+
+/**
+ * Toggle the workdays only checkbox and refresh the current view
+ *
+ * XXX We shouldn't need to refresh the view just to toggle the workdays. This
+ * should happen automatically.
+ */
+function toggleWorkdaysOnly() {
+ let cmd = document.getElementById("calendar_toggle_workdays_only_command");
+ let newValue = (cmd.getAttribute("checked") == "true" ? "false" : "true");
+ cmd.setAttribute("checked", newValue);
+
+ let deck = getViewDeck();
+ for (let view of deck.childNodes) {
+ view.workdaysOnly = (newValue == "true");
+ }
+
+ // Refresh the current view
+ currentView().goToDay();
+}
+
+/**
+ * Toggle the tasks in view checkbox and refresh the current view
+ */
+function toggleTasksInView() {
+ let cmd = document.getElementById("calendar_toggle_tasks_in_view_command");
+ let newValue = (cmd.getAttribute("checked") == "true" ? "false" : "true");
+ cmd.setAttribute("checked", newValue);
+
+ let deck = getViewDeck();
+ for (let view of deck.childNodes) {
+ view.tasksInView = (newValue == "true");
+ }
+
+ // Refresh the current view
+ currentView().goToDay();
+}
+
+/**
+ * Toggle the show completed in view checkbox and refresh the current view
+ */
+function toggleShowCompletedInView() {
+ let cmd = document.getElementById("calendar_toggle_show_completed_in_view_command");
+ let newValue = (cmd.getAttribute("checked") == "true" ? "false" : "true");
+ cmd.setAttribute("checked", newValue);
+
+ let deck = getViewDeck();
+ for (let view of deck.childNodes) {
+ view.showCompleted = (newValue == "true");
+ }
+
+ // Refresh the current view
+ currentView().goToDay();
+}
+
+/**
+ * Provides a neutral way to go to the current day in the views and minimonth.
+ *
+ * @param aDate The date to go.
+ */
+function goToDate(aDate) {
+ getMinimonth().value = cal.dateTimeToJsDate(aDate);
+ currentView().goToDay(aDate);
+}
+
+/**
+ * Returns the calendar view that was selected before restart, or the current
+ * calendar view if it has already been set in this session
+ *
+ * @return The last calendar view.
+ */
+function getLastCalendarView() {
+ let deck = getViewDeck();
+ if (deck.selectedIndex > -1) {
+ let viewNode = deck.childNodes[deck.selectedIndex];
+ return viewNode.id.replace(/-view/, "");
+ }
+
+ // No deck item was selected beforehand, default to week view.
+ return "week";
+}
+
+/**
+ * Deletes items currently selected in the view and clears selection.
+ */
+function deleteSelectedEvents() {
+ let selectedItems = currentView().getSelectedItems({});
+ calendarViewController.deleteOccurrences(selectedItems.length,
+ selectedItems,
+ false,
+ false);
+ // clear selection
+ currentView().setSelectedItems(0, [], true);
+}
+
+/**
+ * Edit the items currently selected in the view with the event dialog.
+ */
+function editSelectedEvents() {
+ let selectedItems = currentView().getSelectedItems({});
+ if (selectedItems && selectedItems.length >= 1) {
+ modifyEventWithDialog(selectedItems[0], null, true);
+ }
+}
+
+/**
+ * Select all events from all calendars. Use with care.
+ */
+function selectAllEvents() {
+ let items = [];
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ currentView().setSelectedItems(items.length, items, false);
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ items.push(item);
+ }
+ }
+ };
+
+ let composite = getCompositeCalendar();
+ let filter = composite.ITEM_FILTER_CLASS_OCCURRENCES;
+
+ if (currentView().tasksInView) {
+ filter |= composite.ITEM_FILTER_TYPE_ALL;
+ } else {
+ filter |= composite.ITEM_FILTER_TYPE_EVENT;
+ }
+ if (currentView().showCompleted) {
+ filter |= composite.ITEM_FILTER_COMPLETED_ALL;
+ } else {
+ filter |= composite.ITEM_FILTER_COMPLETED_NO;
+ }
+
+ // Need to move one day out to get all events
+ let end = currentView().endDay.clone();
+ end.day += 1;
+
+ composite.getItems(filter, 0, currentView().startDay, end, listener);
+}
+
+var cal = cal || {};
+cal.navigationBar = {
+ setDateRange: function(aStartDate, aEndDate) {
+ let docTitle = "";
+ if (aStartDate) {
+ let intervalLabel = document.getElementById("intervalDescription");
+ let firstWeekNo = getWeekInfoService().getWeekTitle(aStartDate);
+ let secondWeekNo = firstWeekNo;
+ let weekLabel = document.getElementById("calendarWeek");
+ if (aStartDate.nativeTime == aEndDate.nativeTime) {
+ intervalLabel.value = getDateFormatter().formatDate(aStartDate);
+ } else {
+ intervalLabel.value = currentView().getRangeDescription();
+ secondWeekNo = getWeekInfoService().getWeekTitle(aEndDate);
+ }
+ if (secondWeekNo == firstWeekNo) {
+ weekLabel.value = calGetString("calendar", "singleShortCalendarWeek", [firstWeekNo]);
+ weekLabel.tooltipText = calGetString("calendar", "singleLongCalendarWeek", [firstWeekNo]);
+ } else {
+ weekLabel.value = calGetString("calendar", "severalShortCalendarWeeks", [firstWeekNo, secondWeekNo]);
+ weekLabel.tooltipText = calGetString("calendar", "severalLongCalendarWeeks", [firstWeekNo, secondWeekNo]);
+ }
+ docTitle = intervalLabel.value;
+ }
+ if (document.getElementById("modeBroadcaster").getAttribute("mode") == "calendar") {
+ document.title = (docTitle ? docTitle + " - " : "") +
+ calGetString("brand", "brandFullName", null, "branding");
+ }
+ let viewTabs = document.getElementById("view-tabs");
+ viewTabs.selectedIndex = getViewDeck().selectedIndex;
+ }
+};
+
+/*
+ * Timer for the time indicator in day and week view.
+ */
+var timeIndicator = {
+ timer: null,
+ start: function(aInterval, aThis) {
+ timeIndicator.timer = setInterval(() => aThis.updateTimeIndicatorPosition(false), aInterval * 1000);
+ },
+ cancel: function() {
+ if (timeIndicator.timer) {
+ clearTimeout(timeIndicator.timer);
+ timeIndicator.timer = null;
+ }
+ },
+ lastView: null
+};
diff --git a/calendar/base/content/calendar-views.xml b/calendar/base/content/calendar-views.xml
new file mode 100644
index 000000000..44e756ad9
--- /dev/null
+++ b/calendar/base/content/calendar-views.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="calendar-specific-view-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="calendar-day-view"
+ extends="chrome://calendar/content/calendar-multiday-view.xml#calendar-multiday-view">
+ <implementation implements="calICalendarView">
+ <property name="observerID">
+ <getter><![CDATA[
+ return "day-view-observer";
+ ]]></getter>
+ </property>
+ <property name="supportsWorkdaysOnly"
+ readonly="true"
+ onget="return false;"/>
+
+ <!-- Public methods -->
+ <method name="goToDay">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ if (!aDate) {
+ this.refresh();
+ return;
+ }
+ aDate = aDate.getInTimezone(this.timezone);
+ this.setDateRange(aDate, aDate);
+ this.selectedDay = aDate;
+ ]]></body>
+ </method>
+ <method name="moveView">
+ <parameter name="aNumber"/>
+ <body><![CDATA[
+ if (aNumber) {
+ let currentDay = this.startDay.clone();
+ currentDay.day += aNumber;
+ this.goToDay(currentDay);
+ } else {
+ this.goToDay(now());
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-week-view"
+ extends="chrome://calendar/content/calendar-multiday-view.xml#calendar-multiday-view">
+ <implementation implements="calICalendarView">
+ <constructor><![CDATA[
+ // add a listener for the mode change
+ this.mModeHandler = (event) => {
+ if (event.attrName == "mode") {
+ this.onModeChanged(event);
+ }
+ };
+ document.getElementById("modeBroadcaster").addEventListener("DOMAttrModified", this.mModeHandler, true);
+ ]]></constructor>
+ <destructor><![CDATA[
+ document.getElementById("modeBroadcaster").removeEventListener("DOMAttrModified", this.mModeHandler, true);
+ ]]></destructor>
+
+ <property name="observerID">
+ <getter><![CDATA[
+ return "week-view-observer";
+ ]]></getter>
+ </property>
+
+ <method name="onModeChanged">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ if (currentMode != "calendar") {
+ timeIndicator.cancel();
+ }
+ ]]></body>
+ </method>
+
+ <!--Public methods-->
+ <method name="goToDay">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this.displayDaysOff = !this.mWorkdaysOnly;
+
+ if (!aDate) {
+ this.refresh();
+ return;
+ }
+ aDate = aDate.getInTimezone(this.timezone);
+ let weekStart = cal.getWeekInfoService().getStartOfWeek(aDate);
+ let weekEnd = weekStart.clone();
+ weekEnd.day += 6;
+ this.setDateRange(weekStart, weekEnd);
+ this.selectedDay = aDate;
+ ]]></body>
+ </method>
+ <method name="moveView">
+ <parameter name="aNumber"/>
+ <body><![CDATA[
+ if (aNumber) {
+ let date = this.selectedDay.clone();
+ date.day += 7 * aNumber;
+ this.goToDay(date);
+ } else {
+ this.goToDay(now());
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-multiweek-view" extends="chrome://calendar/content/calendar-month-view.xml#calendar-month-base-view">
+ <implementation implements="calICalendarView">
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+ this.mWeeksInView = Preferences.get("calendar.weeks.inview", 4);
+ ]]></constructor>
+
+ <field name="mWeeksInView">4</field>
+
+ <property name="weeksInView">
+ <getter><![CDATA[
+ return this.mWeeksInView;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mWeeksInView = val;
+ Preferences.set("calendar.weeks.inview", Number(val));
+ this.refreshView();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="supportsZoom" readonly="true"
+ onget="return true;"/>
+
+ <method name="zoomIn">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ let visibleWeeks = Preferences.get("calendar.weeks.inview", 4);
+ visibleWeeks += (aLevel || 1);
+
+ Preferences.set("calendar.weeks.inview", Math.min(visibleWeeks, 6));
+ ]]></body>
+ </method>
+
+ <method name="zoomOut">
+ <parameter name="aLevel"/>
+ <body><![CDATA[
+ let visibleWeeks = Preferences.get("calendar.weeks.inview", 4);
+ visibleWeeks -= aLevel || 1;
+
+ Preferences.set("calendar.weeks.inview", Math.max(visibleWeeks, 2));
+ ]]></body>
+ </method>
+
+ <method name="zoomReset">
+ <body><![CDATA[
+ Preferences.set("calendar.view.visiblehours", 4);
+ ]]></body>
+ </method>
+
+ <property name="observerID">
+ <getter><![CDATA[
+ return "multiweek-view-observer";
+ ]]></getter>
+ </property>
+
+ <!--Public methods-->
+ <method name="goToDay">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this.showFullMonth = false;
+ this.displayDaysOff = !this.mWorkdaysOnly;
+
+ // If aDate is null it means that only a refresh is needed
+ // without changing the start and end of the view.
+ if (aDate) {
+ aDate = aDate.getInTimezone(this.timezone);
+ // Get the first date that should be shown. This is the
+ // start of the week of the day that we're centering around
+ // adjusted for the day the week starts on and the number
+ // of previous weeks we're supposed to display.
+ let dayStart = cal.getWeekInfoService().getStartOfWeek(aDate);
+ dayStart.day -= 7 * Preferences.get("calendar.previousweeks.inview", 0);
+ // The last day we're supposed to show
+ let dayEnd = dayStart.clone();
+ dayEnd.day += (7 * this.mWeeksInView) - 1;
+ this.setDateRange(dayStart, dayEnd);
+ this.selectedDay = aDate;
+ } else {
+ this.refresh();
+ }
+ ]]></body>
+ </method>
+
+ <method name="moveView">
+ <parameter name="aNumber"/>
+ <body><![CDATA[
+ if (aNumber) {
+ let date = this.startDay.clone();
+ let savedSelectedDay = this.selectedDay.clone();
+ // aNumber only corresponds to the number of weeks to move
+ // make sure to compensate for previous weeks in view too
+ date.day += 7 * (aNumber + Preferences.get("calendar.previousweeks.inview", 4));
+ this.goToDay(date);
+ savedSelectedDay.day += 7 * aNumber;
+ this.selectedDay = savedSelectedDay;
+ } else {
+ let date = now();
+ this.goToDay(date);
+ this.selectedDay = date;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-month-view" extends="chrome://calendar/content/calendar-month-view.xml#calendar-month-base-view">
+ <implementation implements="calICalendarView">
+ <property name="observerID">
+ <getter><![CDATA[
+ return "month-view-observer";
+ ]]></getter>
+ </property>
+
+ <!--Public methods-->
+ <method name="goToDay">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this.displayDaysOff = !this.mWorkdaysOnly;
+
+ if (aDate) {
+ aDate = aDate.getInTimezone(this.timezone);
+ }
+ this.showDate(aDate);
+ ]]></body>
+ </method>
+ <method name="getRangeDescription">
+ <body><![CDATA[
+ let monthName = cal.formatMonth(this.rangeStartDate.month + 1,
+ "calendar", "monthInYear");
+ return calGetString("calendar", "monthInYear", [monthName, this.rangeStartDate.year]);
+ ]]></body>
+ </method>
+ <method name="moveView">
+ <parameter name="aNumber"/>
+ <body><![CDATA[
+ let dates = this.getDateList({});
+ this.displayDaysOff = !this.mWorkdaysOnly;
+
+ if (aNumber) {
+ // The first few dates in this list are likely in the month
+ // prior to the one actually being shown (since the month
+ // probably doesn't start on a Sunday). The 7th item must
+ // be in correct month though.
+ let date = dates[6].clone();
+
+ date.month += aNumber;
+ // Need to store this before we move
+ let oldSelectedDay = this.selectedDay;
+
+ this.goToDay(date);
+
+ // Most of the time we want to select the date with the
+ // same day number in the next month
+ let newSelectedDay = oldSelectedDay.clone();
+ newSelectedDay.month += aNumber;
+ // correct for accidental rollover into the next month
+ if ((newSelectedDay.month - aNumber + 12) % 12 != oldSelectedDay.month) {
+ newSelectedDay.month -= 1;
+ newSelectedDay.day = newSelectedDay.endOfMonth.day;
+ }
+
+ this.selectedDay = newSelectedDay;
+ } else {
+ let date = now();
+ this.goToDay(date);
+ this.selectedDay = date;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/calendar-views.xul b/calendar/base/content/calendar-views.xul
new file mode 100644
index 000000000..76eeebff9
--- /dev/null
+++ b/calendar/base/content/calendar-views.xul
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://calendar/skin/calendar-views.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/widgets/calendar-widgets.css" type="text/css"?>
+
+<!DOCTYPE overlay
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://global/locale/global.dtd" > %dtd2;
+]>
+
+
+<overlay id="calendar-views-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="calendar-view-box" context="calendar-view-context-menu">
+ <hbox id="calendar-nav-control">
+ <vbox flex="1">
+ <hbox flex="1" class="navigation-inner-box" align="center">
+ <!-- If you are extending a view, add attributes to these
+ nodes for your view. i.e if your view has the id
+ "foobar-view", then you need to add the attribute
+ tooltiptext-foobar="..." -->
+ <hbox pack="center">
+ <toolbarbutton id="previous-view-button"
+ class="view-navigation-button"
+ type="prev"
+ tooltiptext-day="&calendar.navigation.prevday.tooltip;"
+ tooltiptext-week="&calendar.navigation.prevweek.tooltip;"
+ tooltiptext-multiweek="&calendar.navigation.prevweek.tooltip;"
+ tooltiptext-month="&calendar.navigation.prevmonth.tooltip;"
+ command="calendar_view_prev_command"/>
+ <toolbarbutton id="today-view-button"
+ class="today-navigation-button"
+ label="&calendar.today.button.label;"
+ tooltiptext-all="&calendar.today.button.tooltip;"
+ command="calendar_view_today_command"/>
+ <toolbarbutton id="next-view-button"
+ class="view-navigation-button"
+ type="next"
+ tooltiptext-day="&calendar.navigation.nextday.tooltip;"
+ tooltiptext-week="&calendar.navigation.nextweek.tooltip;"
+ tooltiptext-multiweek="&calendar.navigation.nextweek.tooltip;"
+ tooltiptext-month="&calendar.navigation.nextmonth.tooltip;"
+ command="calendar_view_next_command"/>
+ </hbox>
+ <label id="intervalDescription"
+ class="view-header"
+ crop="end"
+ flex="1"
+ pack="start"/>
+ <spacer flex="1"/>
+ <label id="calendarWeek"
+ class="view-header"
+ type="end"
+ crop="start"/>
+ </hbox>
+ <hbox flex="1" class="navigation-bottombox"/>
+ </vbox>
+ <tabbox id="view-tabbox" pack="end">
+ <tabs id="view-tabs"
+ class="calview-tabs"
+ setfocus="true">
+ <tab id="calendar-day-view-button"
+ label="&calendar.day.button.label;"
+ tooltiptext="&calendar.day.button.tooltip;"
+ calview="day"
+ observes="calendar_day-view_command"/>
+ <tab id="calendar-week-view-button"
+ label="&calendar.week.button.label;"
+ tooltiptext="&calendar.week.button.tooltip;"
+ calview="week"
+ observes="calendar_week-view_command"/>
+ <tab id="calendar-multiweek-view-button"
+ label="&calendar.multiweek.button.label;"
+ tooltiptext="&calendar.multiweek.button.tooltip;"
+ calview="multiweek"
+ observes="calendar_multiweek-view_command"/>
+ <tab id="calendar-month-view-button"
+ label="&calendar.month.button.label;"
+ tooltiptext="&calendar.month.button.tooltip;"
+ calview="month"
+ observes="calendar_month-view_command"/>
+ </tabs>
+ <box class="navigation-bottombox"/>
+ </tabbox>
+ <vbox>
+ <vbox flex="1" class="navigation-spacer-box"/>
+ <hbox class="navigation-bottombox"/>
+ </vbox>
+ </hbox>
+ <deck flex="1"
+ id="view-deck"
+ persist="selectedIndex">
+ <!-- Note: the "id" attributes of the calendar panes **must** follow the
+ notation 'type + "-" + "view"', where "type" should refer to the
+ displayed time period as described in base/public/calICalendarView.idl -->
+ <calendar-day-view id="day-view" flex="1"
+ context="calendar-view-context-menu"
+ item-context="calendar-item-context-menu"/>
+ <calendar-week-view id="week-view" flex="1"
+ context="calendar-view-context-menu"
+ item-context="calendar-item-context-menu"/>
+ <calendar-multiweek-view id="multiweek-view" flex="1"
+ context="calendar-view-context-menu"
+ item-context="calendar-item-context-menu"/>
+ <calendar-month-view id="month-view" flex="1"
+ context="calendar-view-context-menu"
+ item-context="calendar-item-context-menu"/>
+ </deck>
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/dialogs/calendar-alarm-dialog.js b/calendar/base/content/dialogs/calendar-alarm-dialog.js
new file mode 100644
index 000000000..d9911bbea
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-alarm-dialog.js
@@ -0,0 +1,359 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onDismissAllAlarms, setupWindow, finishWindow, addWidgetFor,
+ * removeWidgetFor, onSelectAlarm, ensureCalendarVisible
+ */
+
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/**
+ * Helper function to get the alarm service and cache it.
+ *
+ * @return The alarm service component
+ */
+function getAlarmService() {
+ if (!("mAlarmService" in window)) {
+ window.mAlarmService = Components.classes["@mozilla.org/calendar/alarm-service;1"]
+ .getService(Components.interfaces.calIAlarmService);
+ }
+ return window.mAlarmService;
+}
+
+/**
+ * Event handler for the 'snooze' event. Snoozes the given alarm by the given
+ * number of minutes using the alarm service.
+ *
+ * @param event The snooze event
+ */
+function onSnoozeAlarm(event) {
+ // reschedule alarm:
+ let duration = getDuration(event.detail);
+ if (aboveSnoozeLimit(duration)) {
+ // we prevent snoozing too far if the alarm wouldn't be displayed
+ return;
+ }
+ getAlarmService().snoozeAlarm(event.target.item, event.target.alarm, duration);
+}
+
+/**
+ * Event handler for the 'dismiss' event. Dismisses the given alarm using the
+ * alarm service.
+ *
+ * @param event The snooze event
+ */
+function onDismissAlarm(event) {
+ getAlarmService().dismissAlarm(event.target.item, event.target.alarm);
+}
+
+/**
+ * Called to dismiss all alarms in the alarm window.
+ */
+function onDismissAllAlarms() {
+ // removes widgets on the fly:
+ let alarmRichlist = document.getElementById("alarm-richlist");
+ let parentItems = {};
+
+ // Make a copy of the child nodes as they get modified live
+ for (let node of alarmRichlist.childNodes) {
+ // Check if the node is a valid alarm and is still part of DOM
+ if (node.parentNode && node.item && node.alarm &&
+ !(node.item.parentItem.hashId in parentItems)) {
+ // We only need to acknowledge one occurrence for repeating items
+ parentItems[node.item.parentItem.hashId] = node.item.parentItem;
+ getAlarmService().dismissAlarm(node.item, node.alarm);
+ }
+ }
+}
+
+/**
+ * Event handler fired when the alarm widget's "Details..." label was clicked.
+ * Open the event dialog in the most recent Thunderbird window.
+ *
+ * @param event The itemdetails event.
+ */
+function onItemDetails(event) {
+ // We want this to happen in a calendar window if possible. Otherwise open
+ // it using our window.
+ let calWindow = cal.getCalendarWindow();
+ if (calWindow) {
+ calWindow.modifyEventWithDialog(event.target.item, null, true);
+ } else {
+ modifyEventWithDialog(event.target.item, null, true);
+ }
+}
+
+/**
+ * Sets up the alarm dialog, initializing the default snooze length and setting
+ * up the relative date update timer.
+ */
+var gRelativeDateUpdateTimer;
+function setupWindow() {
+ // We want to update when we are at 0 seconds past the minute. To do so, use
+ // setTimeout to wait until we are there, then setInterval to execute every
+ // minute. Since setInterval is not totally exact, we may run into problems
+ // here. I hope not!
+ let current = new Date();
+
+ let timeout = (60 - current.getSeconds()) * 1000;
+ gRelativeDateUpdateTimer = setTimeout(() => {
+ updateRelativeDates();
+ gRelativeDateUpdateTimer = setInterval(updateRelativeDates, 60 * 1000);
+ }, timeout);
+
+ // Give focus to the alarm richlist after onload completes. See bug 103197
+ setTimeout(onFocusWindow, 0);
+}
+
+/**
+ * Unload function for the alarm dialog. If applicable, snooze the remaining
+ * alarms and clean up the relative date update timer.
+ */
+function finishWindow() {
+ let alarmRichlist = document.getElementById("alarm-richlist");
+
+ if (alarmRichlist.childNodes.length > 0) {
+ // If there are still items, the window wasn't closed using dismiss
+ // all/snooze all. This can happen when the closer is clicked or escape
+ // is pressed. Snooze all remaining items using the default snooze
+ // property.
+ let snoozePref = Preferences.get("calendar.alarms.defaultsnoozelength", 0);
+ if (snoozePref <= 0) {
+ snoozePref = 5;
+ }
+ snoozeAllItems(snoozePref);
+ }
+
+ // Stop updating the relative time
+ clearTimeout(gRelativeDateUpdateTimer);
+}
+
+/**
+ * Set up the focused element. If no element is focused, then switch to the
+ * richlist.
+ */
+function onFocusWindow() {
+ if (!document.commandDispatcher.focusedElement) {
+ document.getElementById("alarm-richlist").focus();
+ }
+}
+
+/**
+ * Timer callback to update all relative date labels
+ */
+function updateRelativeDates() {
+ let alarmRichlist = document.getElementById("alarm-richlist");
+ for (let node of alarmRichlist.childNodes) {
+ if (node.item && node.alarm) {
+ node.updateRelativeDateLabel();
+ }
+ }
+}
+
+/**
+ * Function to snooze all alarms the given number of minutes.
+ *
+ * @param aDurationMinutes The duration in minutes
+ */
+function snoozeAllItems(aDurationMinutes) {
+ let duration = getDuration(aDurationMinutes);
+ if (aboveSnoozeLimit(duration)) {
+ // we prevent snoozing too far if the alarm wouldn't be displayed
+ return;
+ }
+
+ let alarmRichlist = document.getElementById("alarm-richlist");
+ let parentItems = {};
+
+ // Make a copy of the child nodes as they get modified live
+ for (let node of alarmRichlist.childNodes) {
+ // Check if the node is a valid alarm and is still part of DOM
+ if (node.parentNode && node.item && node.alarm &&
+ !(node.item.parentItem.hashId in parentItems)) {
+ // We only need to acknowledge one occurrence for repeating items
+ parentItems[node.item.parentItem.hashId] = node.item.parentItem;
+ getAlarmService().snoozeAlarm(node.item, node.alarm, duration);
+ }
+ }
+}
+
+/**
+ * Receive a calIDuration object for a given number of minutes
+ *
+ * @param {long} aMinutes The number of minutes
+ * @return {calIDuration}
+ */
+function getDuration(aMinutes) {
+ const MINUTESINWEEK = 7 * 24 * 60;
+
+ // converting to weeks if any is required to avoid an integer overflow of duration.minutes as
+ // this is of type short
+ let weeks = Math.floor(aMinutes / MINUTESINWEEK);
+ aMinutes -= weeks * MINUTESINWEEK;
+
+ let duration = cal.createDuration();
+ duration.minutes = aMinutes;
+ duration.weeks = weeks;
+ duration.normalize();
+ return duration;
+}
+
+/**
+ * Check whether the snooze period exceeds the current limitation of the AlarmService and prompt
+ * the user with a message if so
+ * @param {calIDuration} aDuration The duration to snooze
+ * @returns {Boolean}
+ */
+function aboveSnoozeLimit(aDuration) {
+ const LIMIT = Components.interfaces.calIAlarmService.MAX_SNOOZE_MONTHS;
+
+ let currentTime = cal.now().getInTimezone(cal.UTC());
+ let limitTime = currentTime.clone();
+ limitTime.month += LIMIT;
+
+ let durationUntilLimit = limitTime.subtractDate(currentTime);
+ if (aDuration.compare(durationUntilLimit) > 0) {
+ let msg = PluralForm.get(LIMIT, cal.calGetString("calendar", "alarmSnoozeLimitExceeded"));
+ showError(msg.replace("#1", LIMIT));
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Sets up the window title, counting the number of alarms in the window.
+ */
+function setupTitle() {
+ let alarmRichlist = document.getElementById("alarm-richlist");
+ let reminders = alarmRichlist.childNodes.length;
+
+ let title = PluralForm.get(reminders, calGetString("calendar", "alarmWindowTitle.label"));
+ document.title = title.replace("#1", reminders);
+}
+
+/**
+ * Comparison function for the start date of a calendar item and
+ * the start date of a calendar-alarm-widget.
+ *
+ * @param aItem A calendar item for the comparison of the start date property
+ * @param calAlarmWidget The alarm widget item for the start date comparison with the given calendar item
+ * @return 1 - if the calendar item starts before the calendar-alarm-widget
+ * -1 - if the calendar-alarm-widget starts before the calendar item
+ * 0 - otherwise
+ */
+function widgetAlarmComptor(aItem, aWidgetItem) {
+ if (aItem == null || aWidgetItem == null) {
+ return -1;
+ }
+
+ // Get the dates to compare
+ let aDate = aItem[calGetStartDateProp(aItem)];
+ let bDate = aWidgetItem[calGetStartDateProp(aWidgetItem)];
+
+ return aDate.compare(bDate);
+}
+
+/**
+ * Add an alarm widget for the passed alarm and item.
+ *
+ * @param aItem The calendar item to add a widget for.
+ * @param aAlarm The alarm to add a widget for.
+ */
+function addWidgetFor(aItem, aAlarm) {
+ let widget = document.createElement("calendar-alarm-widget");
+ let alarmRichlist = document.getElementById("alarm-richlist");
+
+ // Add widgets sorted by start date ascending
+ cal.binaryInsertNode(alarmRichlist, widget, aItem, widgetAlarmComptor, false);
+
+ widget.item = aItem;
+ widget.alarm = aAlarm;
+ widget.addEventListener("snooze", onSnoozeAlarm, false);
+ widget.addEventListener("dismiss", onDismissAlarm, false);
+ widget.addEventListener("itemdetails", onItemDetails, false);
+
+ setupTitle();
+
+ if (!alarmRichlist.userSelectedWidget) {
+ // Always select first widget of the list.
+ // Since the onselect event causes scrolling,
+ // we don't want to process the event when adding widgets.
+ alarmRichlist.suppressOnSelect = true;
+ alarmRichlist.selectedIndex = 0;
+ alarmRichlist.suppressOnSelect = false;
+ }
+
+ window.focus();
+ window.getAttention();
+}
+
+/**
+ * Remove the alarm widget for the passed alarm and item.
+ *
+ * @param aItem The calendar item to remove the alarm widget for.
+ * @param aAlarm The alarm to remove the widget for.
+ */
+function removeWidgetFor(aItem, aAlarm) {
+ let hashId = aItem.hashId;
+ let alarmRichlist = document.getElementById("alarm-richlist");
+ let nodes = alarmRichlist.childNodes;
+ let notfound = true;
+ for (let i = nodes.length - 1; notfound && i >= 0; --i) {
+ let widget = nodes[i];
+ if (widget.item && widget.item.hashId == hashId &&
+ widget.alarm && widget.alarm.icalString == aAlarm.icalString) {
+ if (widget.selected) {
+ // Advance selection if needed
+ widget.control.selectedItem = widget.previousSibling ||
+ widget.nextSibling;
+ }
+
+ widget.removeEventListener("snooze", onSnoozeAlarm, false);
+ widget.removeEventListener("dismiss", onDismissAlarm, false);
+ widget.removeEventListener("itemdetails", onItemDetails, false);
+
+ widget.remove();
+ closeIfEmpty();
+ notfound = false;
+ }
+ }
+
+ // Update the title
+ setupTitle();
+}
+
+/**
+ * Close the alarm dialog if there are no further alarm widgets
+ */
+function closeIfEmpty() {
+ let alarmRichlist = document.getElementById("alarm-richlist");
+
+ // we don't want to close if the alarm service is still loading, as the
+ // removed alarms may be immediately added again.
+ if (!alarmRichlist.hasChildNodes() && !getAlarmService().isLoading) {
+ window.close();
+ }
+}
+
+/**
+ * Handler function called when an alarm entry in the richlistbox is selected
+ *
+ * @param event The DOM event from the click action
+ */
+function onSelectAlarm(event) {
+ let richList = document.getElementById("alarm-richlist");
+ if (richList == event.target) {
+ richList.ensureElementIsVisible(richList.getSelectedItem(0));
+ richList.userSelectedWidget = true;
+ }
+}
+
+function ensureCalendarVisible(aCalendar) {
+ // This function is called on the alarm dialog from calendar-item-editing.js.
+ // Normally, it makes sure that the calendar being edited is made visible,
+ // but the alarm dialog is too far away from the calendar views that it
+ // makes sense to force visiblity for the calendar. Therefore, do nothing.
+}
diff --git a/calendar/base/content/dialogs/calendar-alarm-dialog.xul b/calendar/base/content/dialogs/calendar-alarm-dialog.xul
new file mode 100644
index 000000000..35aaca268
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-alarm-dialog.xul
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/calendar-alarm-dialog.css" type="text/css"?>
+
+<!-- used for button-text and button-menu-dropmarker classes -->
+<?xml-stylesheet href="chrome://global/skin/button.css" type="text/css"?>
+
+<!-- used for textbox in the menupopup "snooze-menupopup" -->
+<?xml-stylesheet href="chrome://global/skin/spinbuttons.css" type="text/css"?>
+
+<!-- DTD File with all strings specific to the calendar -->
+<!DOCTYPE dialog SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="calendar-alarm-dialog"
+ title="&calendar.alarm.title.label;"
+ windowtype="Calendar:AlarmWindow"
+ persist="screenX screenY width height"
+ onload="setupWindow(); window.arguments[0].wrappedJSObject.window_onLoad();"
+ onunload="finishWindow();"
+ onfocus="onFocusWindow();"
+ onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { window.close(); }"
+ width="600"
+ height="300">
+ <script type="application/javascript" src="chrome://calendar/content/calendar-alarm-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+
+ <richlistbox id="alarm-richlist" flex="1" onselect="onSelectAlarm(event)"/>
+
+ <hbox pack="end" id="alarm-actionbar" align="center">
+ <button id="alarm-snooze-all-button"
+ type="menu"
+ label="&calendar.alarm.snoozeallfor.label;">
+ <menupopup type="snooze-menupopup"
+ ignorekeys="true"
+ onsnooze="snoozeAllItems(event.detail)"/>
+ </button>
+ <button label="&calendar.alarm.dismissall.label;"
+ oncommand="onDismissAllAlarms();"/>
+ </hbox>
+ <hbox pack="end" class="resizer-box">
+ <resizer dir="bottomright"/>
+ </hbox>
+</window>
diff --git a/calendar/base/content/dialogs/calendar-conflicts-dialog.xul b/calendar/base/content/dialogs/calendar-conflicts-dialog.xul
new file mode 100644
index 000000000..4e23a3883
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-conflicts-dialog.xul
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-views.css"?>
+
+<dialog id="calendar-conflicts-dialog"
+ windowtype="Calendar:Conflicts"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://calendar/content/mouseoverPreviews.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript"><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ function onLoad() {
+ let docEl = document.documentElement;
+ let item = window.arguments[0].item;
+ let vbox = getPreviewForEvent(item);
+ let descr = document.getElementById("conflicts-description");
+ descr.parentNode.insertBefore(vbox, descr);
+
+ // TODO These strings should move to DTD files, but we don't want to
+ // disrupt string freeze right now. For that matter, this dialog
+ // should be reworked!
+ docEl.title = cal.calGetString("calendar", "itemModifiedOnServerTitle");
+ descr.textContent = cal.calGetString("calendar", "itemModifiedOnServer");
+
+ if (window.arguments[0].mode == "modify") {
+ descr.textContent += cal.calGetString("calendar", "modifyWillLoseData");
+ docEl.getButton("accept").setAttribute("label", cal.calGetString("calendar", "proceedModify"));
+ } else {
+ descr.textContent += cal.calGetString("calendar", "deleteWillLoseData");
+ docEl.getButton("accept").setAttribute("label", cal.calGetString("calendar", "proceedDelete"));
+ }
+
+ docEl.getButton("cancel").setAttribute("label", cal.calGetString("calendar", "updateFromServer"));
+
+ window.sizeToContent();
+ }
+
+ function onAccept() {
+ window.arguments[0].overwrite = true;
+ }
+
+ function onCancel() {
+ window.arguments[0].overwrite = false;
+ }
+ ]]></script>
+
+ <vbox id="conflicts-vbox">
+ <description id="conflicts-description"
+ style="max-width: 40em; margin-top: 1ex"/>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-creation.js b/calendar/base/content/dialogs/calendar-creation.js
new file mode 100644
index 000000000..a083c81c8
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-creation.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported openLocalCalendar */
+
+/**
+ * Shows the filepicker and creates a new calendar with a local file using the ICS
+ * provider.
+ */
+function openLocalCalendar() {
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+ let picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ picker.init(window, calGetString("calendar", "Open"), nsIFilePicker.modeOpen);
+ let wildmat = "*.ics";
+ let description = calGetString("calendar", "filterIcs", [wildmat]);
+ picker.appendFilter(description, wildmat);
+ picker.appendFilters(nsIFilePicker.filterAll);
+
+ if (picker.show() != nsIFilePicker.returnOK) {
+ return;
+ }
+
+ let calMgr = getCalendarManager();
+ let calendars = calMgr.getCalendars({});
+ if (calendars.some(x => x.uri == picker.fileURL)) {
+ // The calendar already exists, select it and return.
+ document.getElementById("calendar-list-tree-widget")
+ .tree.view.selection.select(index);
+ return;
+ }
+
+ let openCalendar = calMgr.createCalendar("ics", picker.fileURL);
+
+ // Strip ".ics" from filename for use as calendar name, taken from
+ // calendarCreation.js
+ let fullPathRegex = new RegExp("([^/:]+)[.]ics$");
+ let prettyName = picker.fileURL.spec.match(fullPathRegex);
+ let name;
+
+ if (prettyName && prettyName.length >= 1) {
+ name = decodeURIComponent(prettyName[1]);
+ } else {
+ name = calGetString("calendar", "untitledCalendarName");
+ }
+ openCalendar.name = name;
+
+ calMgr.registerCalendar(openCalendar);
+}
diff --git a/calendar/base/content/dialogs/calendar-dialog-utils.js b/calendar/base/content/dialogs/calendar-dialog-utils.js
new file mode 100644
index 000000000..19af89c27
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-dialog-utils.js
@@ -0,0 +1,693 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables,
+ * dispose, setDialogId, loadReminders, saveReminder,
+ * commonUpdateReminder, updateLink, rearrangeAttendees
+ */
+
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/iteratorUtils.jsm");
+
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+
+// Variables related to whether we are in a tab or a window dialog.
+var gInTab = false;
+var gMainWindow = null;
+var gTabmail = null;
+
+/**
+ * Initialize variables for tab vs window.
+ */
+function intializeTabOrWindowVariables() {
+ let args = window.arguments[0];
+ gInTab = args.inTab;
+ if (gInTab) {
+ gTabmail = parent.document.getElementById("tabmail");
+ gMainWindow = parent;
+ } else {
+ gMainWindow = parent.opener;
+ }
+}
+
+/**
+ * Dispose of controlling operations of this event dialog. Uses
+ * window.arguments[0].job.dispose()
+ */
+function dispose() {
+ let args = window.arguments[0];
+ if (args.job && args.job.dispose) {
+ args.job.dispose();
+ }
+ resetDialogId(document.documentElement);
+}
+
+/**
+ * Sets the id of a Dialog to another value to allow different window-icons to be displayed.
+ * The original name is stored as new Attribute of the Dialog to set it back later.
+ *
+ * @param aDialog The Dialog to be changed.
+ * @param aNewId The new ID as String.
+ */
+function setDialogId(aDialog, aNewId) {
+ aDialog.setAttribute("originalId", aDialog.getAttribute("id"));
+ aDialog.setAttribute("id", aNewId);
+ applyPersitedProperties(aDialog);
+}
+
+/**
+ * Sets the Dialog id back to previously stored one,
+ * so that the persisted values are correctly saved.
+ *
+ * @param aDialog The Dialog which is to be restored.
+ */
+function resetDialogId(aDialog) {
+ let id = aDialog.getAttribute("originalId");
+ if (id != "") {
+ aDialog.setAttribute("id", id);
+ }
+ aDialog.removeAttribute("originalId");
+}
+
+/**
+ * Apply the persisted properties from xulstore.json on a dialog based on the current dialog id.
+ * This needs to be invoked after changing a dialog id while loading to apply the values for the
+ * new dialog id.
+ *
+ * @param aDialog The Dialog to apply the property values for
+ */
+function applyPersitedProperties(aDialog) {
+ let xulStore = Components.classes["@mozilla.org/xul/xulstore;1"]
+ .getService(Components.interfaces.nsIXULStore);
+ // first we need to detect which properties are persisted
+ let persistedProps = aDialog.getAttribute("persist") || "";
+ if (persistedProps == "") {
+ return;
+ }
+ let propNames = persistedProps.split(" ");
+ // now let's apply persisted values if applicable
+ for (let propName of propNames) {
+ if (xulStore.hasValue(aDialog.baseURI, aDialog.id, propName)) {
+ aDialog.setAttribute(propName, xulStore.getValue(aDialog.baseURI, aDialog.id, propName));
+ }
+ }
+}
+
+/**
+ * Create a calIAlarm from the given menuitem. The menuitem must have the
+ * following attributes: unit, length, origin, relation.
+ *
+ * @param menuitem The menuitem to create the alarm from.
+ * @return The calIAlarm with information from the menuitem.
+ */
+function createReminderFromMenuitem(aMenuitem) {
+ let reminder = aMenuitem.reminder || cal.createAlarm();
+ // clone immutable reminders if necessary to set default values
+ let isImmutable = !reminder.isMutable;
+ if (isImmutable) {
+ reminder = reminder.clone();
+ }
+ let offset = cal.createDuration();
+ offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length");
+ offset.normalize();
+ offset.isNegative = (aMenuitem.getAttribute("origin") == "before");
+ reminder.related = (aMenuitem.getAttribute("relation") == "START" ?
+ reminder.ALARM_RELATED_START : reminder.ALARM_RELATED_END);
+ reminder.offset = offset;
+ reminder.action = getDefaultAlarmType();
+ // make reminder immutable in case it was before
+ if (isImmutable) {
+ reminder.makeImmutable();
+ }
+ return reminder;
+}
+
+/**
+ * This function opens the needed dialogs to edit the reminder. Note however
+ * that calling this function from an extension is not recommended. To allow an
+ * extension to open the reminder dialog, set the menulist "item-alarm" to the
+ * custom menuitem and call updateReminder().
+ */
+function editReminder() {
+ let customItem = document.getElementById("reminder-custom-menuitem");
+ let args = {};
+ args.reminders = customItem.reminders;
+ args.item = window.calendarItem;
+ args.timezone = window.gStartTimezone ||
+ window.gEndTimezone ||
+ calendarDefaultTimezone();
+
+ args.calendar = getCurrentCalendar();
+
+ // While these are "just" callbacks, the dialog is opened modally, so aside
+ // from whats needed to set up the reminders, nothing else needs to be done.
+ args.onOk = function(reminders) {
+ customItem.reminders = reminders;
+ };
+ args.onCancel = function() {
+ document.getElementById("item-alarm").selectedIndex = gLastAlarmSelection;
+ };
+
+ window.setCursor("wait");
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-reminder.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * Update the reminder details from the selected alarm. This shows a string
+ * describing the reminder set, or nothing in case a preselected reminder was
+ * chosen.
+ */
+function updateReminderDetails() {
+ // find relevant elements in the document
+ let reminderList = document.getElementById("item-alarm");
+ let reminderMultipleLabel = document.getElementById("reminder-multiple-alarms-label");
+ let iconBox = document.getElementById("reminder-icon-box");
+ let reminderSingleLabel = document.getElementById("reminder-single-alarms-label");
+ let reminders = document.getElementById("reminder-custom-menuitem").reminders || [];
+ let calendar = getCurrentCalendar();
+ let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
+ let actionMap = {};
+ for (let action of actionValues) {
+ actionMap[action] = true;
+ }
+
+ // Filter out any unsupported action types.
+ reminders = reminders.filter(x => x.action in actionMap);
+
+ if (reminderList.value == "custom") {
+ // Depending on how many alarms we have, show either the "Multiple Alarms"
+ // label or the single reminder label.
+ setElementValue(reminderMultipleLabel,
+ reminders.length < 2 && "true",
+ "hidden");
+ setElementValue(reminderSingleLabel,
+ reminders.length > 1 && "true",
+ "hidden");
+
+ cal.alarms.addReminderImages(iconBox, reminders);
+
+ // If there is only one reminder, display the reminder string
+ if (reminders.length == 1) {
+ setElementValue(reminderSingleLabel,
+ reminders[0].toString(window.calendarItem));
+ }
+ } else {
+ hideElement(reminderMultipleLabel);
+ hideElement(reminderSingleLabel);
+ if (reminderList.value == "none") {
+ // No reminder selected means show no icons.
+ removeChildren(iconBox);
+ } else {
+ // This is one of the predefined dropdown items. We should show a
+ // single icon in the icons box to tell the user what kind of alarm
+ // this will be.
+ let mockAlarm = cal.createAlarm();
+ mockAlarm.action = getDefaultAlarmType();
+ cal.alarms.addReminderImages(iconBox, [mockAlarm]);
+ }
+ }
+}
+
+var gLastAlarmSelection = 0;
+
+function matchCustomReminderToMenuitem(reminder) {
+ let defaultAlarmType = getDefaultAlarmType();
+ let reminderList = document.getElementById("item-alarm");
+ let reminderPopup = reminderList.firstChild;
+ if (reminder.related != Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE &&
+ reminder.offset &&
+ reminder.action == defaultAlarmType) {
+ // Exactly one reminder thats not absolute, we may be able to match up
+ // popup items.
+ let relation = (reminder.related == reminder.ALARM_RELATED_START ? "START" : "END");
+ let origin;
+
+ // If the time duration for offset is 0, means the reminder is '0 minutes before'
+ if (reminder.offset.inSeconds == 0 || reminder.offset.isNegative) {
+ origin = "before";
+ } else {
+ origin = "after";
+ }
+
+ let unitMap = {
+ days: 86400,
+ hours: 3600,
+ minutes: 60
+ };
+
+ for (let menuitem of reminderPopup.childNodes) {
+ if (menuitem.localName == "menuitem" &&
+ menuitem.hasAttribute("length") &&
+ menuitem.getAttribute("origin") == origin &&
+ menuitem.getAttribute("relation") == relation) {
+ let unitMult = unitMap[menuitem.getAttribute("unit")] || 1;
+ let length = menuitem.getAttribute("length") * unitMult;
+
+ if (Math.abs(reminder.offset.inSeconds) == length) {
+ menuitem.reminder = reminder.clone();
+ reminderList.selectedItem = menuitem;
+ // We've selected an item, so we are done here.
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+/**
+ * Load an item's reminders into the dialog
+ *
+ * @param reminders An array of calIAlarms to load.
+ */
+function loadReminders(reminders) {
+ // select 'no reminder' by default
+ let reminderList = document.getElementById("item-alarm");
+ let customItem = document.getElementById("reminder-custom-menuitem");
+ reminderList.selectedIndex = 0;
+ gLastAlarmSelection = 0;
+
+ if (!reminders || !reminders.length) {
+ // No reminders selected, we are done
+ return;
+ }
+
+ if (reminders.length > 1 ||
+ !matchCustomReminderToMenuitem(reminders[0])) {
+ // If more than one alarm is selected, or we didn't find a matching item
+ // above, then select the "custom" item and attach the item's reminders to
+ // it.
+ reminderList.value = "custom";
+ customItem.reminders = reminders;
+ }
+
+ // remember the selected index
+ gLastAlarmSelection = reminderList.selectedIndex;
+}
+
+/**
+ * Save the selected reminder into the passed item.
+ *
+ * @param item The item save the reminder into.
+ */
+function saveReminder(item) {
+ // We want to compare the old alarms with the new ones. If these are not
+ // the same, then clear the snooze/dismiss times
+ let oldAlarmMap = {};
+ for (let alarm of item.getAlarms({})) {
+ oldAlarmMap[alarm.icalString] = true;
+ }
+
+ // Clear the alarms so we can add our new ones.
+ item.clearAlarms();
+
+ let reminderList = document.getElementById("item-alarm");
+ if (reminderList.value != "none") {
+ let menuitem = reminderList.selectedItem;
+ let reminders;
+
+ if (menuitem.reminders) {
+ // Custom reminder entries carry their own reminder object with
+ // them. Make sure to clone in case these are the original item's
+ // reminders.
+
+ // XXX do we need to clone here?
+ reminders = menuitem.reminders.map(x => x.clone());
+ } else {
+ // Pre-defined entries specify the necessary information
+ // as attributes attached to the menuitem elements.
+ reminders = [createReminderFromMenuitem(menuitem)];
+ }
+
+ let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") ||
+ ["DISPLAY"];
+ let alarmActions = {};
+ for (let action of alarmCaps) {
+ alarmActions[action] = true;
+ }
+
+ // Make sure only alarms are saved that work in the given calendar.
+ reminders.filter(x => x.action in alarmActions)
+ .forEach(item.addAlarm, item);
+ }
+
+ // Compare alarms to see if something changed.
+ for (let alarm of item.getAlarms({})) {
+ let ics = alarm.icalString;
+ if (ics in oldAlarmMap) {
+ // The new alarm is also in the old set, remember this
+ delete oldAlarmMap[ics];
+ } else {
+ // The new alarm is not in the old set, this means the alarms
+ // differ and we can break out.
+ oldAlarmMap[ics] = true;
+ break;
+ }
+ }
+
+ // If the alarms differ, clear the snooze/dismiss properties
+ if (Object.keys(oldAlarmMap).length > 0) {
+ let cmp = "X-MOZ-SNOOZE-TIME";
+
+ // Recurring item alarms potentially have more snooze props, remove them
+ // all.
+ let propIterator = fixIterator(item.propertyEnumerator, Components.interfaces.nsIProperty);
+ let propsToDelete = [];
+ for (let prop in propIterator) {
+ if (prop.name.startsWith(cmp)) {
+ propsToDelete.push(prop.name);
+ }
+ }
+
+ item.alarmLastAck = null;
+ propsToDelete.forEach(item.deleteProperty, item);
+ }
+}
+
+/**
+ * Get the default alarm type for the currently selected calendar. If the
+ * calendar supports DISPLAY alarms, this is the default. Otherwise it is the
+ * first alarm action the calendar supports.
+ *
+ * @return The default alarm type.
+ */
+function getDefaultAlarmType() {
+ let calendar = getCurrentCalendar();
+ let alarmCaps = calendar.getProperty("capabilities.alarms.actionValues") ||
+ ["DISPLAY"];
+ return (alarmCaps.includes("DISPLAY") ? "DISPLAY" : alarmCaps[0]);
+}
+
+/**
+ * Get the currently selected calendar. For dialogs with a menulist of
+ * calendars, this is the currently chosen calendar, otherwise its the fixed
+ * calendar from the window's item.
+ *
+ * @return The currently selected calendar.
+ */
+function getCurrentCalendar() {
+ let calendarNode = document.getElementById("item-calendar");
+ return (calendarNode && calendarNode.selectedItem
+ ? calendarNode.selectedItem.calendar
+ : window.calendarItem.calendar);
+}
+
+/**
+ * Common update functions for both event dialogs. Called when a reminder has
+ * been selected from the menulist.
+ *
+ * @param aSuppressDialogs If true, controls are updated without prompting
+ * for changes with the dialog
+ */
+function commonUpdateReminder(aSuppressDialogs) {
+ // if a custom reminder has been selected, we show the appropriate
+ // dialog in order to allow the user to specify the details.
+ // the result will be placed in the 'reminder-custom-menuitem' tag.
+ let reminderList = document.getElementById("item-alarm");
+ if (reminderList.value == "custom") {
+ // Clear the reminder icons first, this will make sure that while the
+ // dialog is open the default reminder image is not shown which may
+ // confuse users.
+ removeChildren("reminder-icon-box");
+
+ // show the dialog. This call blocks until the dialog is closed. Don't
+ // pop up the dialog if aSuppressDialogs was specified or if this
+ // happens during initialization of the dialog
+ if (!aSuppressDialogs && reminderList.hasAttribute("last-value")) {
+ editReminder();
+ }
+
+ if (reminderList.value == "custom") {
+ // Only do this if the 'custom' item is still selected. If the edit
+ // reminder dialog was canceled then the previously selected
+ // menuitem is selected, which may not be the custom menuitem.
+
+ // If one or no reminders were selected, we have a chance of mapping
+ // them to the existing elements in the dropdown.
+ let customItem = reminderList.selectedItem;
+ if (customItem.reminders.length == 0) {
+ // No reminder was selected
+ reminderList.value = "none";
+ } else if (customItem.reminders.length == 1) {
+ // We might be able to match the custom reminder with one of the
+ // default menu items.
+ matchCustomReminderToMenuitem(customItem.reminders[0]);
+ }
+ }
+ }
+
+ // remember the current reminder drop down selection index.
+ gLastAlarmSelection = reminderList.selectedIndex;
+ reminderList.setAttribute("last-value", reminderList.value);
+
+ // possibly the selected reminder conflicts with the item.
+ // for example an end-relation combined with a task without duedate
+ // is an invalid state we need to take care of. we take the same
+ // approach as with recurring tasks. in case the reminder is related
+ // to the entry date we check the entry date automatically and disable
+ // the checkbox. the same goes for end related reminder and the due date.
+ if (isToDo(window.calendarItem)) {
+ // In general, (re-)enable the due/entry checkboxes. This will be
+ // changed in case the alarms are related to START/END below.
+ enableElementWithLock("todo-has-duedate", "reminder-lock");
+ enableElementWithLock("todo-has-entrydate", "reminder-lock");
+
+ let menuitem = reminderList.selectedItem;
+ if (menuitem.value != "none") {
+ // In case a reminder is selected, retrieve the array of alarms from
+ // it, or create one from the currently selected menuitem.
+ let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem)];
+
+ // If a reminder is related to the entry date...
+ if (reminders.some(x => x.related == x.ALARM_RELATED_START)) {
+ // ...automatically check 'has entrydate'.
+ if (!getElementValue("todo-has-entrydate", "checked")) {
+ setElementValue("todo-has-entrydate", "true", "checked");
+
+ // Make sure gStartTime is properly initialized
+ updateEntryDate();
+ }
+
+ // Disable the checkbox to indicate that we need the entry-date.
+ disableElementWithLock("todo-has-entrydate", "reminder-lock");
+ }
+
+ // If a reminder is related to the due date...
+ if (reminders.some(x => x.related == x.ALARM_RELATED_END)) {
+ // ...automatically check 'has duedate'.
+ if (!getElementValue("todo-has-duedate", "checked")) {
+ setElementValue("todo-has-duedate", "true", "checked");
+
+ // Make sure gStartTime is properly initialized
+ updateDueDate();
+ }
+
+ // Disable the checkbox to indicate that we need the entry-date.
+ disableElementWithLock("todo-has-duedate", "reminder-lock");
+ }
+ }
+ }
+ updateReminderDetails();
+}
+
+/**
+ * Updates the related link on the dialog. Currently only used by the
+ * read-only summary dialog.
+ */
+function updateLink() {
+ function hideOrShow(aBool) {
+ setElementValue("event-grid-link-row", !aBool && "true", "hidden");
+ let separator = document.getElementById("event-grid-link-separator");
+ if (separator) {
+ // The separator is not there in the summary dialog
+ setElementValue("event-grid-link-separator", !aBool && "true", "hidden");
+ }
+ }
+
+ let itemUrlString = window.calendarItem.getProperty("URL") || "";
+ let linkCommand = document.getElementById("cmd_toggle_link");
+
+
+ if (linkCommand) {
+ // Disable if there is no url
+ setElementValue(linkCommand,
+ !itemUrlString.length && "true",
+ "disabled");
+ }
+
+ if ((linkCommand && linkCommand.getAttribute("checked") != "true") ||
+ !itemUrlString.length) {
+ // Hide if there is no url, or the menuitem was chosen so that the url
+ // should be hidden
+ hideOrShow(false);
+ } else {
+ let handler, uri;
+ try {
+ uri = makeURL(itemUrlString);
+ handler = Services.io.getProtocolHandler(uri.scheme);
+ } catch (e) {
+ // No protocol handler for the given protocol, or invalid uri
+ hideOrShow(false);
+ return;
+ }
+
+ // Only show if its either an internal protcol handler, or its external
+ // and there is an external app for the scheme
+ handler = cal.wrapInstance(handler, Components.interfaces.nsIExternalProtocolHandler);
+ hideOrShow(!handler || handler.externalAppExistsForScheme(uri.scheme));
+
+ setTimeout(() => {
+ // HACK the url-link doesn't crop when setting the value in onLoad
+ setElementValue("url-link", itemUrlString);
+ setElementValue("url-link", itemUrlString, "href");
+ }, 0);
+ }
+}
+
+/*
+ * setup attendees in event and summary dialog
+ */
+function setupAttendees() {
+ let attBox = document.getElementById("item-attendees-box");
+ let attBoxRows = attBox.getElementsByClassName("item-attendees-row");
+
+ if (window.attendees && window.attendees.length > 0) {
+ // cloning of the template nodes
+ let selector = "#item-attendees-box-template .item-attendees-row";
+ let clonedRow = document.querySelector(selector).cloneNode(false);
+ selector = "#item-attendees-box-template .item-attendees-row box:nth-of-type(1)";
+ let clonedCell = document.querySelector(selector).cloneNode(true);
+ selector = "#item-attendees-box-template .item-attendees-row box:nth-of-type(2)";
+ let clonedSpacer = document.querySelector(selector).cloneNode(false);
+
+ // determining of attendee box setup
+ let inRow = window.attendeesInRow || -1;
+ if (inRow == -1) {
+ inRow = determineAttendeesInRow();
+ window.attendeesInRow = inRow;
+ } else {
+ while (attBoxRows.length > 0) {
+ attBox.removeChild(attBoxRows[0]);
+ }
+ }
+
+ // set up of the required nodes
+ let maxRows = Math.ceil(window.attendees.length / inRow);
+ let inLastRow = window.attendees.length - ((maxRows - 1) * inRow);
+ let attCount = 0;
+ while (attBox.getElementsByClassName("item-attendees-row").length < maxRows) {
+ let newRow = clonedRow.cloneNode(false);
+ let row = attBox.appendChild(newRow);
+ row.removeAttribute("hidden");
+ let rowCount = attBox.getElementsByClassName("item-attendees-row").length;
+ let reqAtt = rowCount == maxRows ? inLastRow : inRow;
+ // we add as many attendee cells as required
+ while (row.childNodes.length < reqAtt) {
+ let newCell = clonedCell.cloneNode(true);
+ let cell = row.appendChild(newCell);
+ let icon = cell.getElementsByTagName("img")[0];
+ let text = cell.getElementsByTagName("label")[0];
+ let attendee = window.attendees[attCount];
+
+ let label = (attendee.commonName && attendee.commonName.length)
+ ? attendee.commonName : attendee.toString();
+ let userType = attendee.userType || "INDIVIDUAL";
+ let role = attendee.role || "REQ-PARTICIPANT";
+ let partstat = attendee.participationStatus || "NEEDS-ACTION";
+
+ icon.setAttribute("partstat", partstat);
+ icon.setAttribute("usertype", userType);
+ icon.setAttribute("role", role);
+ cell.setAttribute("attendeeid", attendee.id);
+ cell.removeAttribute("hidden");
+
+ let userTypeString = cal.calGetString("calendar", "dialog.tooltip.attendeeUserType2." + userType,
+ [attendee.toString()]);
+ let roleString = cal.calGetString("calendar", "dialog.tooltip.attendeeRole2." + role,
+ [userTypeString]);
+ let partstatString = cal.calGetString("calendar", "dialog.tooltip.attendeePartStat2." + partstat,
+ [label]);
+ let tooltip = cal.calGetString("calendar", "dialog.tooltip.attendee.combined",
+ [roleString, partstatString]);
+
+ let del = cal.resolveDelegation(attendee, window.attendees);
+ if (del.delegators != "") {
+ del.delegators = cal.calGetString("calendar",
+ "dialog.attendee.append.delegatedFrom",
+ [del.delegators]);
+ label += " " + del.delegators;
+ tooltip += " " + del.delegators;
+ }
+ if (del.delegatees != "") {
+ del.delegatees = cal.calGetString("calendar",
+ "dialog.attendee.append.delegatedTo",
+ [del.delegatees]);
+ tooltip += " " + del.delegatees;
+ }
+
+ text.setAttribute("value", label);
+ cell.setAttribute("tooltiptext", tooltip);
+ attCount++;
+ }
+ // we fill the row with placeholders if required
+ if (attBox.getElementsByClassName("item-attendees-row").length > 1 && inRow > 1) {
+ while (row.childNodes.length < inRow) {
+ let newSpacer = clonedSpacer.cloneNode(true);
+ newSpacer.removeAttribute("hidden");
+ row.appendChild(newSpacer);
+ }
+ }
+ }
+
+ // determining of the max width of an attendee label - this needs to
+ // be done only once and is obsolete in case of resizing
+ if (!window.maxLabelWidth) {
+ let maxWidth = 0;
+ for (let cell of attBox.getElementsByClassName("item-attendees-cell")) {
+ cell = cell.cloneNode(true);
+ cell.removeAttribute("flex");
+ cell.getElementsByTagName("label")[0].removeAttribute("flex");
+ maxWidth = cell.clientWidth > maxWidth ? cell.clientWidth : maxWidth;
+ }
+ window.maxLabelWidth = maxWidth;
+ }
+ } else {
+ while (attBoxRows.length > 0) {
+ attBox.removeChild(attBoxRows[0]);
+ }
+ }
+}
+
+/**
+ * Re-arranges the attendees on dialog resizing in event and summary dialog
+ */
+function rearrangeAttendees() {
+ if (window.attendees && window.attendees.length > 0 && window.attendeesInRow) {
+ let inRow = determineAttendeesInRow();
+ if (inRow != window.attendeesInRow) {
+ window.attendeesInRow = inRow;
+ setupAttendees();
+ }
+ }
+}
+
+/**
+ * Calculates the number of columns to distribute attendees for event and summary dialog
+ */
+function determineAttendeesInRow() {
+ // as default value a reasonable high value is appropriate
+ // it will be recalculated anyway.
+ let minWidth = window.maxLabelWidth || 200;
+ let inRow = Math.floor(document.width / minWidth);
+ return inRow > 1 ? inRow : 1;
+}
diff --git a/calendar/base/content/dialogs/calendar-error-prompt.xul b/calendar/base/content/dialogs/calendar-error-prompt.xul
new file mode 100644
index 000000000..5aef370a9
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-error-prompt.xul
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!DOCTYPE dialog
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+]>
+
+
+<dialog id="calendar-error-prompt"
+ title="&calendar.error.title;"
+ windowtype="Calendar:ErrorPrompt"
+ buttons="accept"
+ onload="loadErrorPrompt()"
+ persist="screenX screenY"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="500"
+ xmlns:nc="http://home.netscape.com/NC-rdf#">
+
+ <script type="application/javascript"><![CDATA[
+ function loadErrorPrompt() {
+ var args = window.arguments[0].QueryInterface(Components.interfaces.nsIDialogParamBlock);
+ document.getElementById("general-text").value = args.GetString(0);
+ document.getElementById("error-code").value = args.GetString(1);
+ document.getElementById("error-description").value = args.GetString(2);
+ this.sizeToContent();
+ }
+ function toggleDetails() {
+ var grid = document.getElementById("details-grid");
+ if (grid.collapsed)
+ grid.collapsed = false;
+ else
+ grid.collapsed = true;
+ this.sizeToContent();
+ }
+ ]]></script>
+ <vbox>
+ <textbox id="general-text" class="plain" readonly="true"
+ multiline="true" rows="3"/>
+ <hbox>
+ <button id="details-button" label="&calendar.error.detail;" oncommand="toggleDetails()"/>
+ <spacer flex="1"/>
+ </hbox>
+ <grid id="details-grid" collapsed="true" persist="collapsed">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row>
+ <label value="&calendar.error.code;"/>
+ <label id="error-code" value=""/>
+ </row>
+ <row flex="1">
+ <label value="&calendar.error.description;" control="error-description"/>
+ <textbox id="error-description" class="plain"
+ readonly="true" multiline="true" rows="5"/>
+ </row>
+ </rows>
+ </grid>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-attendees.js b/calendar/base/content/dialogs/calendar-event-dialog-attendees.js
new file mode 100644
index 000000000..f523bf1b1
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.js
@@ -0,0 +1,1004 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAccept, onCancel, zoomWithButtons, updateStartTime,
+ * endWidget, updateEndTime, editStartTimezone, editEndTimezone,
+ * changeAllDay, onNextSlot, onPreviousSlot
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var gStartDate = null;
+var gEndDate = null;
+var gStartTimezone = null;
+var gEndTimezone = null;
+var gDuration = null;
+var gStartHour = 0;
+var gEndHour = 24;
+var gIsReadOnly = false;
+var gIsInvitation = false;
+var gIgnoreUpdate = false;
+var gDisplayTimezone = true;
+var gUndoStack = [];
+var gForce24Hours = false;
+var gZoomFactor = 100;
+
+/**
+ * Sets up the attendee dialog
+ */
+function onLoad() {
+ // first of all, attach all event handlers
+ window.addEventListener("resize", onResize, true);
+ window.addEventListener("modify", onModify, true);
+ window.addEventListener("rowchange", onRowChange, true);
+ window.addEventListener("DOMAttrModified", onAttrModified, true);
+ window.addEventListener("timebar", onTimebar, true);
+ window.addEventListener("timechange", onTimeChange, true);
+
+ // As long as DOMMouseScroll is still implemented, we need to keep it
+ // around to make sure scrolling is blocked.
+ window.addEventListener("wheel", onMouseScroll, true);
+ window.addEventListener("DOMMouseScroll", onMouseScroll, true);
+
+ let args = window.arguments[0];
+ let startTime = args.startTime;
+ let endTime = args.endTime;
+ let calendar = args.calendar;
+
+ gDisplayTimezone = args.displayTimezone;
+
+ onChangeCalendar(calendar);
+
+
+ let zoom = document.getElementById("zoom-menulist");
+ let zoomOut = document.getElementById("zoom-out-button");
+ let zoomIn = document.getElementById("zoom-in-button");
+
+ // Make sure zoom factor is set up correctly (from persisted value)
+ setZoomFactor(zoom.value);
+ if (gZoomFactor == 100) {
+ // if zoom factor was not changed, make sure it is applied at least once
+ applyCurrentZoomFactor();
+ }
+
+ initTimeRange();
+
+ // Check if an all-day event has been passed in (to adapt endDate).
+ if (startTime.isDate) {
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+
+ endTime.day--;
+
+ // for all-day events we expand to 24hrs, set zoom-factor to 25%
+ // and disable the zoom-control.
+ setForce24Hours(true);
+ zoom.value = "400";
+ zoom.setAttribute("disabled", "true");
+ zoomOut.setAttribute("disabled", "true");
+ zoomIn.setAttribute("disabled", "true");
+ setZoomFactor(zoom.value);
+ }
+
+ loadDateTime(startTime, endTime);
+ propagateDateTime();
+ // Set the scroll bar at where the event is
+ scrollToCurrentTime();
+ updateButtons();
+
+ // we need to enforce several layout constraints which can't be modelled
+ // with plain xul and css, at least as far as i know.
+ const kStylesheet = "chrome://calendar/skin/calendar-event-dialog.css";
+ for (let stylesheet of document.styleSheets) {
+ if (stylesheet.href == kStylesheet) {
+ // make the dummy-spacer #1 [top] the same height as the timebar
+ let timebar = document.getElementById("timebar");
+ stylesheet.insertRule(".attendee-spacer-top { height: " +
+ timebar.boxObject.height + "px; }", 0);
+ // make the dummy-spacer #2 [bottom] the same height as the scrollbar
+ let scrollbar = document.getElementById("horizontal-scrollbar");
+ stylesheet.insertRule(".attendee-spacer-bottom { height: " +
+ scrollbar.boxObject.height + "px; }", 0);
+ break;
+ }
+ }
+
+ // attach an observer to get notified of changes
+ // that are relevant to this dialog.
+ let prefObserver = {
+ observe: function(aSubject, aTopic, aPrefName) {
+ switch (aPrefName) {
+ case "calendar.view.daystarthour":
+ case "calendar.view.dayendhour":
+ initTimeRange();
+ propagateDateTime();
+ break;
+ }
+ }
+ };
+ Services.prefs.addObserver("calendar.", prefObserver, false);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("calendar.", prefObserver);
+ }, false);
+
+ opener.setCursor("auto");
+ self.focus();
+}
+
+/**
+ * This function should be called when the accept button was pressed on the
+ * attendee dialog. Calls the accept function specified in the window arguments.
+ *
+ * @return Returns true, if the dialog should be closed.
+ */
+function onAccept() {
+ let attendees = document.getElementById("attendees-list");
+ window.arguments[0].onOk(
+ attendees.attendees,
+ attendees.organizer,
+ gStartDate.getInTimezone(gStartTimezone),
+ gEndDate.getInTimezone(gEndTimezone));
+ return true;
+}
+
+/**
+ * This function should be called when the cancel button was pressed on the
+ * attendee dialog.
+ *
+ * @return Returns true, if the dialog should be closed.
+ */
+function onCancel() {
+ return true;
+}
+
+/**
+ * Function called when zoom buttons (+/-) are clicked.
+ *
+ * @param aZoomOut true -> zoom out; false -> zoom in.
+ */
+function zoomWithButtons(aZoomOut) {
+ let zoom = document.getElementById("zoom-menulist");
+ if (aZoomOut && zoom.selectedIndex < 4) {
+ zoom.selectedIndex++;
+ } else if (!aZoomOut && zoom.selectedIndex > 0) {
+ zoom.selectedIndex--;
+ }
+ setZoomFactor(zoom.value);
+}
+
+/**
+ * Loads the passed start and end dates, fills global variables that give
+ * information about the state of the dialog.
+ *
+ * @param aStartDate The date/time the grid should start at.
+ * @param aEndDate The date/time the grid should end at.
+ */
+function loadDateTime(aStartDate, aEndDate) {
+ gDuration = aEndDate.subtractDate(aStartDate);
+ let kDefaultTimezone = calendarDefaultTimezone();
+ gStartTimezone = aStartDate.timezone;
+ gEndTimezone = aEndDate.timezone;
+ gStartDate = aStartDate.getInTimezone(kDefaultTimezone);
+ gEndDate = aEndDate.getInTimezone(kDefaultTimezone);
+ gStartDate.makeImmutable();
+ gEndDate.makeImmutable();
+}
+
+/**
+ * Sets up the time grid using the global start and end dates.
+ */
+function propagateDateTime() {
+ // Fill the controls
+ updateDateTime();
+
+ // Tell the timebar about the new start/enddate
+ let timebar = document.getElementById("timebar");
+ timebar.startDate = gStartDate;
+ timebar.endDate = gEndDate;
+ timebar.refresh();
+
+ // Tell the selection-bar about the new start/enddate
+ let selectionbar = document.getElementById("selection-bar");
+ selectionbar.startDate = gStartDate;
+ selectionbar.endDate = gEndDate;
+ selectionbar.update();
+
+ // Tell the freebusy grid about the new start/enddate
+ let grid = document.getElementById("freebusy-grid");
+
+ let refresh = (grid.startDate == null) ||
+ (grid.startDate.compare(gStartDate) != 0) ||
+ (grid.endDate == null) ||
+ (grid.endDate.compare(gEndDate) != 0);
+ grid.startDate = gStartDate;
+ grid.endDate = gEndDate;
+ if (refresh) {
+ grid.forceRefresh();
+ }
+
+ // Expand to 24hrs if the new range is outside of the default range.
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let startTime = gStartDate.getInTimezone(kDefaultTimezone);
+ let endTime = gEndDate.getInTimezone(kDefaultTimezone);
+ if ((startTime.hour < gStartHour) ||
+ (startTime.hour >= gEndHour) ||
+ (endTime.hour >= gEndHour) ||
+ (startTime.day != endTime.day) ||
+ (startTime.isDate)) {
+ setForce24Hours(true);
+ }
+}
+
+/**
+ * This function requires gStartDate and gEndDate and the respective timezone
+ * variables to be initialized. It updates the date/time information displayed in
+ * the dialog from the above noted variables.
+ */
+function updateDateTime() {
+ // Convert to default timezone if the timezone option
+ // is *not* checked, otherwise keep the specific timezone
+ // and display the labels in order to modify the timezone.
+ if (gDisplayTimezone) {
+ let startTime = gStartDate.getInTimezone(gStartTimezone);
+ let endTime = gEndDate.getInTimezone(gEndTimezone);
+
+ if (startTime.isDate) {
+ document.getElementById("all-day")
+ .setAttribute("checked", "true");
+ }
+
+ // In the case where the timezones are different but
+ // the timezone of the endtime is "UTC", we convert
+ // the endtime into the timezone of the starttime.
+ if (startTime && endTime) {
+ if (!compareObjects(startTime.timezone, endTime.timezone)) {
+ if (endTime.timezone.isUTC) {
+ endTime = endTime.getInTimezone(startTime.timezone);
+ }
+ }
+ }
+
+ // Before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = floating();
+ endTime.timezone = floating();
+
+ document.getElementById("event-starttime").value = cal.dateTimeToJsDate(startTime);
+ document.getElementById("event-endtime").value = cal.dateTimeToJsDate(endTime);
+ } else {
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ let startTime = gStartDate.getInTimezone(kDefaultTimezone);
+ let endTime = gEndDate.getInTimezone(kDefaultTimezone);
+
+ if (startTime.isDate) {
+ document.getElementById("all-day")
+ .setAttribute("checked", "true");
+ }
+
+ // Before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = floating();
+ endTime.timezone = floating();
+
+ document.getElementById("event-starttime").value = cal.dateTimeToJsDate(startTime);
+ document.getElementById("event-endtime").value = cal.dateTimeToJsDate(endTime);
+ }
+
+ updateTimezone();
+ updateAllDay();
+}
+
+/**
+ * This function requires gStartDate and gEndDate and the respective timezone
+ * variables to be initialized. It updates the timezone information displayed in
+ * the dialog from the above noted variables.
+ */
+function updateTimezone() {
+ gIgnoreUpdate = true;
+
+ if (gDisplayTimezone) {
+ let startTimezone = gStartTimezone;
+ let endTimezone = gEndTimezone;
+ let equalTimezones = false;
+ if (startTimezone && endTimezone &&
+ (compareObjects(startTimezone, endTimezone) || endTimezone.isUTC)) {
+ equalTimezones = true;
+ }
+
+ let tzStart = document.getElementById("timezone-starttime");
+ let tzEnd = document.getElementById("timezone-endtime");
+ if (startTimezone) {
+ tzStart.removeAttribute("collapsed");
+ tzStart.value = startTimezone.displayName || startTimezone.tzid;
+ } else {
+ tzStart.setAttribute("collapsed", "true");
+ }
+
+ // we never display the second timezone if both are equal
+ if (endTimezone != null && !equalTimezones) {
+ tzEnd.removeAttribute("collapsed");
+ tzEnd.value = endTimezone.displayName || endTimezone.tzid;
+ } else {
+ tzEnd.setAttribute("collapsed", "true");
+ }
+ } else {
+ document.getElementById("timezone-starttime")
+ .setAttribute("collapsed", "true");
+ document.getElementById("timezone-endtime")
+ .setAttribute("collapsed", "true");
+ }
+
+ gIgnoreUpdate = false;
+}
+
+/**
+ * Updates gStartDate from the start time picker "event-starttime"
+ */
+function updateStartTime() {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ let startWidgetId = "event-starttime";
+
+ let startWidget = document.getElementById(startWidgetId);
+
+ // jsDate is always in OS timezone, thus we create a calIDateTime
+ // object from the jsDate representation and simply set the new
+ // timezone instead of converting.
+ let timezone = gDisplayTimezone ? gStartTimezone : calendarDefaultTimezone();
+ let start = cal.jsDateToDateTime(startWidget.value, timezone);
+
+ gStartDate = start.clone();
+ start.addDuration(gDuration);
+ gEndDate = start.getInTimezone(gEndTimezone);
+
+ let allDayElement = document.getElementById("all-day");
+ let allDay = allDayElement.getAttribute("checked") == "true";
+ if (allDay) {
+ gStartDate.isDate = true;
+ gEndDate.isDate = true;
+ }
+
+ propagateDateTime();
+}
+
+/**
+ * Updates gEndDate from the end time picker "event-endtime"
+ */
+function updateEndTime() {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ let startWidgetId = "event-starttime";
+ let endWidgetId = "event-endtime";
+
+ let startWidget = document.getElementById(startWidgetId);
+ let endWidget = document.getElementById(endWidgetId);
+
+ let saveStartTime = gStartDate;
+ let saveEndTime = gEndDate;
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ gStartDate = cal.jsDateToDateTime(startWidget.value,
+ gDisplayTimezone ? gStartTimezone : calendarDefaultTimezone());
+
+ let timezone = gEndTimezone;
+ if (timezone.isUTC &&
+ gStartDate &&
+ !compareObjects(gStartTimezone, gEndTimezone)) {
+ timezone = gStartTimezone;
+ }
+ gEndDate = cal.jsDateToDateTime(endWidget.value,
+ gDisplayTimezone ? timezone : kDefaultTimezone);
+
+ let allDayElement = document.getElementById("all-day");
+ let allDay = allDayElement.getAttribute("checked") == "true";
+ if (allDay) {
+ gStartDate.isDate = true;
+ gEndDate.isDate = true;
+ }
+
+ // Calculate the new duration of start/end-time.
+ // don't allow for negative durations.
+ let warning = false;
+ if (gEndDate.compare(gStartDate) >= 0) {
+ gDuration = gEndDate.subtractDate(gStartDate);
+ } else {
+ gStartDate = saveStartTime;
+ gEndDate = saveEndTime;
+ warning = true;
+ }
+
+ propagateDateTime();
+
+ if (warning) {
+ let callback = function() {
+ Services.prompt.alert(
+ null,
+ document.title,
+ calGetString("calendar", "warningEndBeforeStart"));
+ };
+ setTimeout(callback, 1);
+ }
+}
+
+/**
+ * Prompts the user to pick a new timezone for the starttime. The dialog is
+ * opened modally.
+ */
+function editStartTimezone() {
+ let tzStart = document.getElementById("timezone-starttime");
+ if (tzStart.hasAttribute("disabled")) {
+ return;
+ }
+
+ let self = this;
+ let args = {};
+ args.calendar = window.arguments[0].calendar;
+ args.time = gStartDate.getInTimezone(gStartTimezone);
+ args.onOk = function(datetime) {
+ let equalTimezones = false;
+ if (gStartTimezone && gEndTimezone &&
+ compareObjects(gStartTimezone, gEndTimezone)) {
+ equalTimezones = true;
+ }
+ gStartTimezone = datetime.timezone;
+ if (equalTimezones) {
+ gEndTimezone = datetime.timezone;
+ }
+ self.propagateDateTime();
+ };
+
+ // Open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-timezone.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * Prompts the user to pick a new timezone for the endtime. The dialog is
+ * opened modally.
+ */
+function editEndTimezone() {
+ let tzStart = document.getElementById("timezone-endtime");
+ if (tzStart.hasAttribute("disabled")) {
+ return;
+ }
+
+ let self = this;
+ let args = {};
+ args.calendar = window.arguments[0].calendar;
+ args.time = gEndTime.getInTimezone(gEndTimezone);
+ args.onOk = function(datetime) {
+ if (gStartTimezone && gEndTimezone &&
+ compareObjects(gStartTimezone, gEndTimezone)) {
+ gStartTimezone = datetime.timezone;
+ }
+ gEndTimezone = datetime.timezone;
+ self.propagateDateTime();
+ };
+
+ // Open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-timezone.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * Updates the dialog controls in case the window's event is an allday event, or
+ * was set to one in the attendee dialog.
+ *
+ * This for example disables the timepicker since its not needed.
+ */
+function updateAllDay() {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ let allDayElement = document.getElementById("all-day");
+ let allDay = (allDayElement.getAttribute("checked") == "true");
+ let startpicker = document.getElementById("event-starttime");
+ let endpicker = document.getElementById("event-endtime");
+
+ let tzStart = document.getElementById("timezone-starttime");
+ let tzEnd = document.getElementById("timezone-endtime");
+
+ // Disable the timezone links if 'allday' is checked OR the
+ // calendar of this item is read-only. In any other case we
+ // enable the links.
+ if (allDay) {
+ startpicker.setAttribute("timepickerdisabled", "true");
+ endpicker.setAttribute("timepickerdisabled", "true");
+
+ tzStart.setAttribute("disabled", "true");
+ tzEnd.setAttribute("disabled", "true");
+ tzStart.removeAttribute("class");
+ tzEnd.removeAttribute("class");
+ } else {
+ startpicker.removeAttribute("timepickerdisabled");
+ endpicker.removeAttribute("timepickerdisabled");
+
+ tzStart.removeAttribute("disabled");
+ tzEnd.removeAttribute("disabled");
+ tzStart.setAttribute("class", "text-link");
+ tzEnd.setAttribute("class", "text-link");
+ }
+}
+
+/**
+ * Changes the global variables to adapt for the change of the allday checkbox.
+ *
+ * XXX Function names are all very similar here. This needs some consistency!
+ */
+function changeAllDay() {
+ let allDayElement = document.getElementById("all-day");
+ let allDay = (allDayElement.getAttribute("checked") == "true");
+
+ gStartDate = gStartDate.clone();
+ gEndDate = gEndDate.clone();
+
+ gStartDate.isDate = allDay;
+ gEndDate.isDate = allDay;
+
+ propagateDateTime();
+
+ // After propagating the modified times we enforce some constraints
+ // on the zoom-factor. In case this events is now said to be all-day,
+ // we automatically enforce a 25% zoom-factor and disable the control.
+ let zoom = document.getElementById("zoom-menulist");
+ let zoomOut = document.getElementById("zoom-out-button");
+ let zoomIn = document.getElementById("zoom-in-button");
+ if (allDay) {
+ zoom.value = "400";
+ zoom.setAttribute("disabled", "true");
+ zoomOut.setAttribute("disabled", "true");
+ zoomIn.setAttribute("disabled", "true");
+ setZoomFactor(zoom.value);
+ setForce24Hours(true);
+ } else {
+ zoom.removeAttribute("disabled");
+ zoomOut.removeAttribute("disabled");
+ zoomIn.removeAttribute("disabled");
+ }
+}
+
+/**
+ * Handler function used when the window is resized.
+ */
+function onResize() {
+ // Don't do anything if we haven't been initialized.
+ if (!gStartDate || !gEndDate) {
+ return;
+ }
+
+ let grid = document.getElementById("freebusy-grid");
+ let gridScrollbar = document.getElementById("horizontal-scrollbar");
+ grid.fitDummyRows();
+ let gridRatio = grid.boxObject.width / grid.documentSize;
+ let gridMaxpos = gridScrollbar.getAttribute("maxpos");
+ let gridInc = gridMaxpos * gridRatio / (1 - gridRatio);
+ gridScrollbar.setAttribute("pageincrement", gridInc);
+
+ let attendees = document.getElementById("attendees-list");
+ let attendeesScrollbar = document.getElementById("vertical-scrollbar");
+ let box = document.getElementById("vertical-scrollbar-box");
+ attendees.fitDummyRows();
+ let attRatio = attendees.boxObject.height / attendees.documentSize;
+ let attMaxpos = attendeesScrollbar.getAttribute("maxpos");
+ if (attRatio < 1) {
+ box.removeAttribute("collapsed");
+ let attInc = attMaxpos * attRatio / (1 - attRatio);
+ attendeesScrollbar.setAttribute("pageincrement", attInc);
+ } else {
+ box.setAttribute("collapsed", "true");
+ }
+}
+
+/**
+ * Handler function to call when changing the calendar used in this dialog.
+ *
+ * @param calendar The calendar to change to.
+ */
+function onChangeCalendar(calendar) {
+ let args = window.arguments[0];
+
+ // set 'mIsReadOnly' if the calendar is read-only
+ if (calendar && calendar.readOnly) {
+ gIsReadOnly = true;
+ }
+
+ // assume we're the organizer [in case that the calendar
+ // does not support the concept of identities].
+ gIsInvitation = false;
+ calendar = cal.wrapInstance(args.item.calendar, Components.interfaces.calISchedulingSupport);
+ if (calendar) {
+ gIsInvitation = calendar.isInvitation(args.item);
+ }
+
+ if (gIsReadOnly || gIsInvitation) {
+ document.getElementById("next-slot")
+ .setAttribute("disabled", "true");
+ document.getElementById("previous-slot")
+ .setAttribute("disabled", "true");
+ }
+
+ let freebusy = document.getElementById("freebusy-grid");
+ freebusy.onChangeCalendar(calendar);
+}
+
+/**
+ * Updates the slot buttons.
+ */
+function updateButtons() {
+ let previousButton = document.getElementById("previous-slot");
+ if (gUndoStack.length > 0) {
+ previousButton.removeAttribute("disabled");
+ } else {
+ previousButton.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Handler function called to advance to the next slot.
+ */
+function onNextSlot() {
+ // Store the current setting in the undo-stack.
+ let currentSlot = {};
+ currentSlot.startTime = gStartDate;
+ currentSlot.endTime = gEndDate;
+ gUndoStack.push(currentSlot);
+
+ // Ask the grid for the next possible timeslot.
+ let grid = document.getElementById("freebusy-grid");
+ let duration = gEndDate.subtractDate(gStartDate);
+ let start = grid.nextSlot();
+ let end = start.clone();
+ end.addDuration(duration);
+ if (start.isDate) {
+ end.day++;
+ }
+ gStartDate = start.clone();
+ gEndDate = end.clone();
+ let endDate = gEndDate.clone();
+
+ // Check if an all-day event has been passed in (to adapt endDate).
+ if (gStartDate.isDate) {
+ gEndDate.day--;
+ }
+ gStartDate.makeImmutable();
+ gEndDate.makeImmutable();
+ endDate.makeImmutable();
+
+ propagateDateTime();
+
+ // Scroll the grid/timebar such that the current time is visible
+ scrollToCurrentTime();
+
+ updateButtons();
+}
+
+/**
+ * Handler function called to advance to the previous slot.
+ */
+function onPreviousSlot() {
+ let previousSlot = gUndoStack.pop();
+ if (!previousSlot) {
+ return;
+ }
+
+ // In case the new starttime happens to be scheduled
+ // on a different day, we also need to update the
+ // complete freebusy informations and appropriate
+ // underlying arrays holding the information.
+ let refresh = previousSlot.startTime.day != gStartDate.day;
+
+ gStartDate = previousSlot.startTime.clone();
+ gEndDate = previousSlot.endTime.clone();
+
+ propagateDateTime();
+
+ // scroll the grid/timebar such that the current time is visible
+ scrollToCurrentTime();
+
+ updateButtons();
+
+ if (refresh) {
+ let grid = document.getElementById("freebusy-grid");
+ grid.forceRefresh();
+ }
+}
+
+/**
+ * Scrolls the time grid to a position where the time of the item in question is
+ * visible.
+ */
+function scrollToCurrentTime() {
+ let timebar = document.getElementById("timebar");
+ let ratio = (gStartDate.hour - gStartHour - 1) * timebar.step;
+ if (ratio <= 0.0) {
+ ratio = 0.0;
+ }
+ if (ratio >= 1.0) {
+ ratio = 1.0;
+ }
+ let scrollbar = document.getElementById("horizontal-scrollbar");
+ let maxpos = scrollbar.getAttribute("maxpos");
+ scrollbar.setAttribute("curpos", ratio * maxpos);
+}
+
+
+/**
+ * Sets the zoom factor for the time grid
+ *
+ * @param aValue The zoom factor to set.
+ * @return aValue (for chaining)
+ */
+function setZoomFactor(aValue) {
+ // Correct zoom factor, if needed
+ aValue = parseInt(aValue, 10) || 100;
+
+ if (gZoomFactor == aValue) {
+ return aValue;
+ }
+
+ gZoomFactor = aValue;
+ applyCurrentZoomFactor();
+ return aValue;
+}
+
+/**
+ * applies the current zoom factor for the time grid
+ */
+function applyCurrentZoomFactor() {
+ let timebar = document.getElementById("timebar");
+ timebar.zoomFactor = gZoomFactor;
+ let selectionbar = document.getElementById("selection-bar");
+ selectionbar.zoomFactor = gZoomFactor;
+ let grid = document.getElementById("freebusy-grid");
+ grid.zoomFactor = gZoomFactor;
+
+ // Calling onResize() will update the scrollbars and everything else
+ // that needs to adopt the previously made changes. We need to call
+ // this after the changes have actually been made...
+ onResize();
+
+ let scrollbar = document.getElementById("horizontal-scrollbar");
+ if (scrollbar.hasAttribute("maxpos")) {
+ let curpos = scrollbar.getAttribute("curpos");
+ let maxpos = scrollbar.getAttribute("maxpos");
+ let ratio = curpos / maxpos;
+ timebar.scroll = ratio;
+ grid.scroll = ratio;
+ selectionbar.ratio = ratio;
+ }
+}
+
+/**
+ * Force the time grid to show 24 hours.
+ *
+ * @param aValue If true, the view will be forced to 24 hours.
+ * @return aValue (for chaining)
+ */
+function setForce24Hours(aValue) {
+ if (gForce24Hours == aValue) {
+ return aValue;
+ }
+
+ gForce24Hours = aValue;
+ initTimeRange();
+ let timebar = document.getElementById("timebar");
+ timebar.force24Hours = gForce24Hours;
+ let selectionbar = document.getElementById("selection-bar");
+ selectionbar.force24Hours = gForce24Hours;
+ let grid = document.getElementById("freebusy-grid");
+ grid.force24Hours = gForce24Hours;
+
+ // Calling onResize() will update the scrollbars and everything else
+ // that needs to adopt the previously made changes. We need to call
+ // this after the changes have actually been made...
+ onResize();
+
+ let scrollbar = document.getElementById("horizontal-scrollbar");
+ if (!scrollbar.hasAttribute("maxpos")) {
+ return aValue;
+ }
+ let curpos = scrollbar.getAttribute("curpos");
+ let maxpos = scrollbar.getAttribute("maxpos");
+ let ratio = curpos / maxpos;
+ timebar.scroll = ratio;
+ grid.scroll = ratio;
+ selectionbar.ratio = ratio;
+
+ return aValue;
+}
+
+/**
+ * Initialize the time range, setting the start and end hours from the prefs, or
+ * to 24 hrs if gForce24Hours is set.
+ */
+function initTimeRange() {
+ if (gForce24Hours) {
+ gStartHour = 0;
+ gEndHour = 24;
+ } else {
+ gStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ gEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+}
+
+/**
+ * Handler function for the "modify" event, emitted from the attendees-list
+ * binding. event.details is an array of objects containing the user's email
+ * (calid) and a flag that tells if the user has entered text before the last
+ * onModify was called (dirty).
+ *
+ * @param event The DOM event that caused the modification.
+ */
+function onModify(event) {
+ onResize();
+ document.getElementById("freebusy-grid").onModify(event);
+}
+
+/**
+ * Handler function for the "rowchange" event, emitted from the attendees-list
+ * binding. event.details is the row that was changed to.
+ *
+ * @param event The DOM event caused by the row change.
+ */
+function onRowChange(event) {
+ let scrollbar = document.getElementById("vertical-scrollbar");
+ let attendees = document.getElementById("attendees-list");
+ let maxpos = scrollbar.getAttribute("maxpos");
+ scrollbar.setAttribute(
+ "curpos",
+ event.details / attendees.mMaxAttendees * maxpos);
+}
+
+/**
+ * Handler function to take care of mouse scrolling on the window
+ *
+ * @param event The wheel event caused by scrolling.
+ */
+function onMouseScroll(event) {
+ // ignore mouse scrolling for now...
+ event.stopPropagation();
+}
+
+/**
+ * Hanlder function to take care of attribute changes on the window
+ *
+ * @param event The DOMAttrModified event caused by this change.
+ */
+function onAttrModified(event) {
+ if (event.attrName == "width") {
+ let selectionbar = document.getElementById("selection-bar");
+ selectionbar.setWidth(selectionbar.boxObject.width);
+ return;
+ }
+
+ // Synchronize grid and attendee list
+ let target = event.originalTarget;
+ if (target.hasAttribute("anonid") &&
+ target.getAttribute("anonid") == "input" &&
+ event.attrName == "focused") {
+ let attendees = document.getElementById("attendees-list");
+ if (event.newValue == "true") {
+ let grid = document.getElementById("freebusy-grid");
+ if (grid.firstVisibleRow != attendees.firstVisibleRow) {
+ grid.firstVisibleRow = attendees.firstVisibleRow;
+ }
+ }
+ if (!target.lastListCheckedValue ||
+ target.lastListCheckedValue != target.value) {
+ attendees.resolvePotentialList(target);
+ target.lastListCheckedValue = target.value;
+ }
+ }
+
+ if (event.originalTarget.localName == "scrollbar") {
+ let scrollbar = event.originalTarget;
+ if (scrollbar.hasAttribute("maxpos")) {
+ if (scrollbar.getAttribute("id") == "vertical-scrollbar") {
+ let attendees = document.getElementById("attendees-list");
+ let grid = document.getElementById("freebusy-grid");
+ if (event.attrName == "curpos") {
+ let maxpos = scrollbar.getAttribute("maxpos");
+ attendees.ratio = event.newValue / maxpos;
+ }
+ grid.firstVisibleRow = attendees.firstVisibleRow;
+ } else if (scrollbar.getAttribute("id") == "horizontal-scrollbar") {
+ if (event.attrName == "curpos") {
+ let maxpos = scrollbar.getAttribute("maxpos");
+ let ratio = event.newValue / maxpos;
+ let timebar = document.getElementById("timebar");
+ let grid = document.getElementById("freebusy-grid");
+ let selectionbar = document.getElementById("selection-bar");
+ timebar.scroll = ratio;
+ grid.scroll = ratio;
+ selectionbar.ratio = ratio;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Handler function for initializing the selection bar, event usually emitted
+ * from the freebusy-timebar binding.
+ *
+ * @param event The "timebar" event with details and height property.
+ */
+function onTimebar(event) {
+ document.getElementById(
+ "selection-bar")
+ .init(event.details, event.height);
+}
+
+/**
+ * Handler function to update controls when the time has changed on the
+ * selection bar.
+ *
+ * @param event The "timechange" event with startDate and endDate
+ * properties.
+ */
+function onTimeChange(event) {
+ let start = event.startDate.getInTimezone(gStartTimezone);
+ let end = event.endDate.getInTimezone(gEndTimezone);
+
+ loadDateTime(start, end);
+
+ // fill the controls
+ updateDateTime();
+
+ // tell the timebar about the new start/enddate
+ let timebar = document.getElementById("timebar");
+ timebar.startDate = gStartDate;
+ timebar.endDate = gEndDate;
+ timebar.refresh();
+
+ // tell the freebusy grid about the new start/enddate
+ let grid = document.getElementById("freebusy-grid");
+
+ let refresh = (grid.startDate == null) ||
+ (grid.startDate.compare(gStartDate) != 0) ||
+ (grid.endDate == null) ||
+ (grid.endDate.compare(gEndDate) != 0);
+ grid.startDate = gStartDate;
+ grid.endDate = gEndDate;
+ if (refresh) {
+ grid.forceRefresh();
+ }
+}
+
+/**
+ * This listener is used in calendar-event-dialog-freebusy.xml inside the
+ * binding. It has been taken out of the binding to prevent leaks.
+ */
+function calFreeBusyListener(aFbElement, aBinding) {
+ this.mFbElement = aFbElement;
+ this.mBinding = aBinding;
+}
+
+calFreeBusyListener.prototype = {
+ onResult: function(aRequest, aEntries) {
+ if (aRequest && !aRequest.isPending) {
+ // Find request in list of pending requests and remove from queue:
+ this.mBinding.mPendingRequests = this.mBinding.mPendingRequests.filter(aOp => aRequest.id != aOp.id);
+ }
+ if (aEntries) {
+ this.mFbElement.onFreeBusy(aEntries);
+ }
+ }
+};
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
new file mode 100644
index 000000000..5a8513a1a
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
@@ -0,0 +1,1604 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd3;
+]>
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <binding id="attendees-list">
+ <content>
+ <xul:listbox anonid="listbox"
+ seltype="multiple"
+ class="listbox-noborder"
+ rows="-1"
+ flex="1">
+ <xul:listcols>
+ <xul:listcol/>
+ <xul:listcol/>
+ <xul:listcol flex="1"/>
+ </xul:listcols>
+ <xul:listitem anonid="item" class="addressingWidgetItem" allowevents="true">
+ <xul:listcell class="addressingWidgetCell" align="center" pack="center">
+ <xul:image id="attendeeCol1#1" anonid="rolestatus-icon"/>
+ </xul:listcell>
+ <xul:listcell class="addressingWidgetCell">
+ <xul:image id="attendeeCol2#1" anonid="usertype-icon" class="usertype-icon" onclick="this.parentNode.select();"/>
+ </xul:listcell>
+ <xul:listcell class="addressingWidgetCell">
+ <xul:textbox id="attendeeCol3#1"
+ anonid="input"
+ class="plain textbox-addressingWidget uri-element"
+ type="autocomplete"
+ flex="1"
+ autocompletesearch="addrbook ldap"
+ autocompletesearchparam="{}"
+ timeout="300"
+ maxrows="4"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="1"
+ onblur="if (this.localName == 'textbox') document.getBindingParent(this).returnHit(this, true)"
+ ignoreblurwhilesearching="true"
+ oninput="this.setAttribute('dirty', 'true');"
+ ontextentered="document.getBindingParent(this).returnHit(this);">
+ </xul:textbox>
+ </xul:listcell>
+ </xul:listitem>
+ </xul:listbox>
+ </content>
+
+ <implementation>
+ <field name="mMaxAttendees">0</field>
+ <field name="mContentHeight">0</field>
+ <field name="mRowHeight">0</field>
+ <field name="mNumColumns">0</field>
+ <field name="mIsOffline">0</field>
+ <field name="mIsReadOnly">false</field>
+ <field name="mIsInvitation">false</field>
+ <field name="mPopupOpen">false</field>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource:///modules/mailServices.js");
+
+ this.mMaxAttendees = 0;
+
+ window.addEventListener("load", this.onLoad.bind(this), true);
+ ]]></constructor>
+
+ <method name="onLoad">
+ <body><![CDATA[
+ this.onInitialize();
+
+ // this trigger the continous update chain, which
+ // effectively calls this.onModify() on predefined
+ // time intervals [each second].
+ let self = this;
+ let callback = function() {
+ setTimeout(callback, 1000);
+ self.onModify();
+ };
+ callback();
+ ]]></body>
+ </method>
+
+ <method name="onInitialize">
+ <body><![CDATA[
+ let args = window.arguments[0];
+ let organizer = args.organizer;
+ let attendees = args.attendees;
+ let calendar = args.calendar;
+
+ this.mIsReadOnly = calendar.readOnly;
+
+ // assume we're the organizer [in case that the calendar
+ // does not support the concept of identities].
+ let organizerID = ((organizer && organizer.id)
+ ? organizer.id
+ : calendar.getProperty("organizerId"));
+
+ calendar = cal.wrapInstance(calendar, Components.interfaces.calISchedulingSupport);
+ this.mIsInvitation = (calendar && calendar.isInvitation(args.item));
+
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "item");
+ template.focus();
+
+ if (this.mIsReadOnly || this.mIsInvitation) {
+ listbox.setAttribute("disabled", "true");
+ }
+
+ // TODO: the organizer should show up in the attendee list, but this information
+ // should be based on the organizer contained in the appropriate field of calIItemBase.
+ // This is currently not supported, since we're still missing calendar identities.
+ if (organizerID && organizerID != "") {
+ if (organizer) {
+ if (!organizer.id) {
+ organizer.id = organizerID;
+ }
+ if (!organizer.role) {
+ organizer.role = "CHAIR";
+ }
+ if (!organizer.participationStatus) {
+ organizer.participationStatus = "ACCEPTED";
+ }
+ } else {
+ organizer = this.createAttendee();
+ organizer.id = organizerID;
+ organizer.role = "CHAIR";
+ organizer.participationStatus = "ACCEPTED";
+ }
+ if (!organizer.commonName || !organizer.commonName.length) {
+ organizer.commonName = calendar.getProperty("organizerCN");
+ }
+ organizer.isOrganizer = true;
+ this.appendAttendee(organizer, listbox, template, true);
+ }
+
+ let numRowsAdded = 0;
+ if (attendees.length > 0) {
+ for (let attendee of attendees) {
+ this.appendAttendee(attendee, listbox, template, false);
+ numRowsAdded++;
+ }
+ }
+ if (numRowsAdded == 0) {
+ this.appendAttendee(null, listbox, template, false);
+ }
+
+ // detach the template item from the listbox, but hold the reference.
+ // until this function returns we add at least a single copy of this template back again.
+ template.remove();
+
+ this.setFocus(this.mMaxAttendees);
+ ]]></body>
+ </method>
+
+ <!-- appends a new row using an existing attendee structure -->
+ <method name="appendAttendee">
+ <parameter name="aAttendee"/>
+ <parameter name="aParentNode"/>
+ <parameter name="aTemplateNode"/>
+ <parameter name="aDisableIfOrganizer"/>
+ <body><![CDATA[
+ // create a new listbox item and append it to our parent control.
+ let newNode = aTemplateNode.cloneNode(true);
+
+ let input =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "input");
+ let roleStatusIcon =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "rolestatus-icon");
+ let userTypeIcon =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "usertype-icon");
+
+ // We always clone the first row. The problem is that the first row
+ // could be focused. When we clone that row, we end up with a cloned
+ // XUL textbox that has a focused attribute set. Therefore we think
+ // we're focused and don't properly refocus. The best solution to this
+ // would be to clone a template row that didn't really have any presentation,
+ // rather than using the real visible first row of the listbox.
+ // For now we'll just put in a hack that ensures the focused attribute
+ // is never copied when the node is cloned.
+ if (input.getAttribute("focused") != "") {
+ input.removeAttribute("focused");
+ }
+
+ aParentNode.appendChild(newNode);
+
+ // the template could have its fields disabled,
+ // that's why we need to reset their status.
+ input.removeAttribute("disabled");
+ userTypeIcon.removeAttribute("disabled");
+ roleStatusIcon.removeAttribute("disabled");
+
+ if (this.mIsReadOnly || this.mIsInvitation) {
+ input.setAttribute("disabled", "true");
+ userTypeIcon.setAttribute("disabled", "true");
+ roleStatusIcon.setAttribute("disabled", "true");
+ }
+
+ // disable the input-field [name <email>] if this attendee
+ // appears to be the organizer.
+ if (aDisableIfOrganizer && aAttendee && aAttendee.isOrganizer) {
+ input.setAttribute("disabled", "true");
+ }
+
+ this.mMaxAttendees++;
+ let rowNumber = this.mMaxAttendees;
+ if (rowNumber >= 0) {
+ roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
+ userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
+ input.setAttribute("id", "attendeeCol3#" + rowNumber);
+ }
+
+ if (!aAttendee) {
+ aAttendee = this.createAttendee();
+ }
+
+ // construct the display string from common name and/or email address.
+ let commonName = aAttendee.commonName || "";
+ let inputValue = cal.removeMailTo(aAttendee.id || "");
+ if (commonName.length) {
+ // Make the commonName appear in quotes if it contains a
+ // character that could confuse the header parser
+ if (commonName.search(/[,;<>@]/) != -1) {
+ commonName = '"' + commonName + '"';
+ }
+ inputValue = inputValue.length ? commonName + " <" + inputValue + ">" : commonName;
+ }
+
+ // trim spaces if any
+ inputValue = inputValue.trim();
+
+ // don't set value with null, otherwise autocomplete stops working,
+ // but make sure attendee and dirty are set
+ if (inputValue.length) {
+ input.setAttribute("value", inputValue);
+ input.value = inputValue;
+ }
+ input.attendee = aAttendee;
+ input.setAttribute("dirty", "true");
+
+ if (aAttendee) {
+ // Set up userType
+ setElementValue(userTypeIcon, aAttendee.userType || false, "cutype");
+ this.updateTooltip(userTypeIcon);
+
+ // Set up role/status icon
+ if (aAttendee.isOrganizer) {
+ roleStatusIcon.setAttribute("class", "status-icon");
+ setElementValue(roleStatusIcon, aAttendee.participationStatus || false, "status");
+ } else {
+ roleStatusIcon.setAttribute("class", "role-icon");
+ setElementValue(roleStatusIcon, aAttendee.role || false, "role");
+ }
+ this.updateTooltip(roleStatusIcon);
+ }
+
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="appendNewRow">
+ <parameter name="aSetFocus"/>
+ <parameter name="aInsertAfter"/>
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let listitem1 = this.getListItem(1);
+ let newNode = null;
+
+ if (listbox && listitem1) {
+ let newAttendee = this.createAttendee();
+ let nextDummy = this.getNextDummyRow();
+ newNode = listitem1.cloneNode(true);
+
+ if (aInsertAfter) {
+ listbox.insertBefore(newNode, aInsertAfter.nextSibling);
+ } else if (nextDummy) {
+ listbox.replaceChild(newNode, nextDummy);
+ } else {
+ listbox.appendChild(newNode);
+ }
+
+ let input =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "input");
+ let roleStatusIcon =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "rolestatus-icon");
+ let userTypeIcon =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "usertype-icon");
+
+ // the template could have its fields disabled,
+ // that's why we need to reset their status.
+ input.removeAttribute("disabled");
+ roleStatusIcon.removeAttribute("disabled");
+ userTypeIcon.removeAttribute("disabled");
+
+ if (this.mIsReadOnly || this.mIsInvitation) {
+ input.setAttribute("disabled", "true");
+ roleStatusIcon.setAttribute("disabled", "true");
+ userTypeIcon.setAttribute("disabled", "true");
+ }
+
+ this.mMaxAttendees++;
+ let rowNumber = this.mMaxAttendees;
+ if (rowNumber >= 0) {
+ roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
+ userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
+ input.setAttribute("id", "attendeeCol3#" + rowNumber);
+ }
+
+ input.value = null;
+ input.removeAttribute("value");
+ input.attendee = newAttendee;
+
+ // set role and participation status
+ roleStatusIcon.setAttribute("class", "role-icon");
+ roleStatusIcon.setAttribute("role", "REQ-PARTICIPANT");
+ userTypeIcon.setAttribute("cutype", "INDIVIDUAL");
+
+ // Set tooltip for rolenames and usertype icon
+ this.updateTooltip(roleStatusIcon);
+ this.updateTooltip(userTypeIcon);
+
+ // We always clone the first row. The problem is that the first row
+ // could be focused. When we clone that row, we end up with a cloned
+ // XUL textbox that has a focused attribute set. Therefore we think
+ // we're focused and don't properly refocus. The best solution to this
+ // would be to clone a template row that didn't really have any presentation,
+ // rather than using the real visible first row of the listbox.
+ // For now we'll just put in a hack that ensures the focused attribute
+ // is never copied when the node is cloned.
+ if (input.getAttribute("focused") != "") {
+ input.removeAttribute("focused");
+ }
+
+ // focus on new input widget
+ if (aSetFocus) {
+ this.setFocus(newNode);
+ }
+ }
+ return newNode;
+ ]]></body>
+ </method>
+
+ <property name="attendees">
+ <getter><![CDATA[
+ let attendees = [];
+
+ for (let i = 1; true; i++) {
+ let inputField = this.getInputElement(i);
+ if (!inputField) {
+ break;
+ } else if (inputField.value == "") {
+ continue;
+ }
+
+ // the inputfield already has a reference to the attendee
+ // object, we just need to fill in the name.
+ let attendee = inputField.attendee.clone();
+ if (attendee.isOrganizer) {
+ continue;
+ }
+
+ attendee.role = this.getRoleElement(i).getAttribute("role");
+ // attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
+ let userType = this.getUserTypeElement(i).getAttribute("cutype");
+ attendee.userType = (userType == "INDIVIDUAL" ? null : userType); // INDIVIDUAL is the default
+
+ // break the list of potentially many attendees back into individual names. This
+ // is required in case the user entered comma-separated attendees in one field and
+ // then clicked OK without switching to the next line.
+ let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
+ let j = 0;
+ let addAttendee = function(aAddress) {
+ if (j > 0) {
+ attendee = attendee.clone();
+ }
+ attendee.id = cal.prependMailTo(aAddress.email);
+ if (aAddress.name.length > 0) {
+ // we remove any double quotes within CN due to bug 1209399
+ attendee.commonName = aAddress.name.replace(/(?:[\\]"|")/, "");
+ }
+ attendees.push(attendee);
+ j++;
+ };
+ parsedInput.forEach(addAttendee);
+ }
+
+ return attendees;
+ ]]></getter>
+ </property>
+
+ <property name="organizer">
+ <getter><![CDATA[
+ for (let i = 1; true; i++) {
+ let inputField = this.getInputElement(i);
+ if (!inputField) {
+ break;
+ } else if (inputField.value == "") {
+ continue;
+ }
+
+ // The inputfield already has a reference to the attendee
+ // object, we just need to fill in the name.
+ let attendee = inputField.attendee.clone();
+
+ // attendee.role = this.getRoleElement(i).getAttribute("role");
+ attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
+ // Organizers do not have a CUTYPE
+ attendee.userType = null;
+
+ // break the list of potentially many attendees back into individual names
+ let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
+ if (parsedInput[0].email > 0) {
+ attendee.id = cal.prependMailTo(parsedInput[0].email);
+ }
+ if (parsedInput[0].name.length > 0) {
+ attendee.commonName = parsedInput[0].name;
+ }
+
+ if (attendee.isOrganizer) {
+ return attendee;
+ }
+ }
+
+ return null;
+ ]]></getter>
+ </property>
+
+ <method name="_resolveListByName">
+ <parameter name="value"/>
+ <body><![CDATA[
+ let entries = MailServices.headerParser.makeFromDisplayAddress(value);
+ return entries.length ? this._findListInAddrBooks(entries[0].name) : null;
+ ]]></body>
+ </method>
+
+ <method name="_findListInAddrBooks">
+ <parameter name="entryname"/>
+ <body><![CDATA[
+ let allAddressBooks = MailServices.ab.directories;
+
+ while (allAddressBooks.hasMoreElements()) {
+ let abDir = null;
+ try {
+ abDir = allAddressBooks.getNext()
+ .QueryInterface(Components.interfaces.nsIAbDirectory);
+ } catch (ex) {
+ cal.WARN("[eventDialog] Error Encountered" + ex);
+ }
+
+ if (abDir != null && abDir.supportsMailingLists) {
+ let childNodes = abDir.childNodes;
+ while (childNodes.hasMoreElements()) {
+ let dir = null;
+ try {
+ dir = childNodes.getNext().QueryInterface(Components.interfaces.nsIAbDirectory);
+ } catch (ex) {
+ cal.WARN("[eventDialog] Error Encountered" + ex);
+ }
+
+ if (dir && dir.isMailList && (dir.dirName == entryname)) {
+ return dir;
+ }
+ }
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="_getListEntriesInt">
+ <parameter name="mailingList"/>
+ <parameter name="attendees"/>
+ <parameter name="allListsUri"/>
+ <body><![CDATA[
+ let addressLists = mailingList.addressLists;
+ for (let i = 0; i < addressLists.length; i++) {
+ let abCard = addressLists.queryElementAt(i, Components.interfaces.nsIAbCard);
+ let thisId = abCard.primaryEmail;
+ if (abCard.displayName.length > 0) {
+ let rCn = abCard.displayName;
+ if (rCn.includes(",")) {
+ rCn = '"' + rCn + '"';
+ }
+ thisId = rCn + " <" + thisId + ">";
+ }
+ if (attendees.some(att => att == thisId)) {
+ continue;
+ }
+
+ if (abCard.displayName.length > 0) {
+ let list = this._findListInAddrBooks(abCard.displayName);
+ if (list) {
+ if (allListsUri.some(uri => uri == list.URI)) {
+ continue;
+ }
+ allListsUri.push(list.URI);
+
+ this._getListEntriesInt(list, attendees, allListsUri);
+
+ continue;
+ }
+ }
+
+ attendees.push(thisId);
+ }
+
+ return attendees;
+ ]]></body>
+ </method>
+
+ <method name="_getListEntries">
+ <parameter name="mailingList"/>
+ <body><![CDATA[
+
+ let attendees = [];
+ let allListsUri = [];
+
+ allListsUri.push(mailingList.URI);
+
+ this._getListEntriesInt(mailingList, attendees, allListsUri);
+
+ return attendees;
+
+ ]]></body>
+ </method>
+
+ <method name="_fillListItemWithEntry">
+ <parameter name="listitem"/>
+ <parameter name="entry"/>
+ <parameter name="rowNumber"/>
+ <body><![CDATA[
+ let newAttendee = this.createAttendee(entry);
+ let input = document.getAnonymousElementByAttribute(listitem, "anonid", "input");
+ input.removeAttribute("disabled");
+ input.setAttribute("id", "attendeeCol3#" + rowNumber);
+
+ input.attendee = newAttendee;
+ input.value = entry;
+ input.setAttribute("value", entry);
+ input.setAttribute("dirty", "true");
+ if (input.getAttribute("focused") != "") {
+ input.removeAttribute("focused");
+ }
+
+ let roleStatusIcon = document.getAnonymousElementByAttribute(listitem, "anonid", "rolestatus-icon");
+ roleStatusIcon.removeAttribute("disabled");
+ roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
+ roleStatusIcon.setAttribute("class", "role-icon");
+ roleStatusIcon.setAttribute("role", newAttendee.role);
+
+ let userTypeIcon = document.getAnonymousElementByAttribute(listitem, "anonid", "usertype-icon");
+ userTypeIcon.removeAttribute("disabled");
+ userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
+ userTypeIcon.setAttribute("cutype", newAttendee.userType);
+ ]]></body>
+ </method>
+
+ <method name="resolvePotentialList">
+ <parameter name="aInput"/>
+ <body><![CDATA[
+ let fieldValue = aInput.value;
+ if (aInput.id.length > 0 && fieldValue.length > 0) {
+ let mailingList = this._resolveListByName(fieldValue);
+ if (mailingList) {
+ let entries = this._getListEntries(mailingList);
+ if (entries.length > 0) {
+ let currentIndex = parseInt(aInput.id.substr(13), 10);
+ let template = document.getAnonymousElementByAttribute(this, "anonid", "item");
+ let currentNode = template.parentNode.childNodes[currentIndex];
+ this._fillListItemWithEntry(currentNode, entries[0], currentIndex);
+ entries.shift();
+ let nextNode = template.parentNode.childNodes[currentIndex + 1];
+ currentIndex++;
+ for (let entry of entries) {
+ currentNode = template.cloneNode(true);
+ template.parentNode.insertBefore(currentNode, nextNode);
+ this._fillListItemWithEntry(currentNode, entry, currentIndex);
+ currentIndex++;
+ }
+ this.mMaxAttendees += entries.length;
+ for (let i = currentIndex; i <= this.mMaxAttendees; i++) {
+ let row = template.parentNode.childNodes[i];
+ let roleStatusIcon = document.getAnonymousElementByAttribute(row, "anonid", "rolestatus-icon");
+ roleStatusIcon.setAttribute("id", "attendeeCol1#" + i);
+
+ let userTypeIcon = document.getAnonymousElementByAttribute(row, "anonid", "usertype-icon");
+ userTypeIcon.setAttribute("id", "attendeeCol2#" + i);
+
+ let input = document.getAnonymousElementByAttribute(row, "anonid", "input");
+ input.setAttribute("id", "attendeeCol3#" + i);
+ input.setAttribute("dirty", "true");
+ }
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="onModify">
+ <body><![CDATA[
+ let list = [];
+ for (let i = 1; i <= this.mMaxAttendees; i++) {
+ // retrieve the string from the appropriate row
+ let input = this.getInputElement(i);
+ if (input && input.value) {
+ // parse the string to break this down to individual names and addresses
+ let parsedInput = MailServices.headerParser.makeFromDisplayAddress(input.value);
+ let email = cal.prependMailTo(parsedInput[0].email);
+
+ let isdirty = false;
+ if (input.hasAttribute("dirty")) {
+ isdirty = input.getAttribute("dirty");
+ }
+ input.removeAttribute("dirty");
+ let entry = {
+ dirty: isdirty,
+ calid: email
+ };
+ list.push(entry);
+ }
+ }
+
+ let event = document.createEvent("Events");
+ event.initEvent("modify", true, false);
+ event.details = list;
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="updateTooltip">
+ <parameter name="targetIcon"/>
+ <body><![CDATA[
+ // Function setting the tooltip of attendeeicons based on their role
+ if (targetIcon.className == "role-icon") {
+ let role = targetIcon.getAttribute("role");
+ // Set tooltip for rolenames
+
+ const roleMap = {
+ "REQ-PARTICIPANT": "required",
+ "OPT-PARTICIPANT": "optional",
+ "NON-PARTICIPANT": "nonparticipant",
+ "CHAIR": "chair"
+ };
+
+ let roleNameString = "event.attendee.role." + (role in roleMap ? roleMap[role] : "unknown");
+ let tooltip = cal.calGetString("calendar-event-dialog-attendees",
+ roleNameString,
+ role in roleMap ? null : [role]);
+ targetIcon.setAttribute("tooltiptext", tooltip);
+ } else if (targetIcon.className == "usertype-icon") {
+ let cutype = targetIcon.getAttribute("cutype");
+ const cutypeMap = {
+ INDIVIDUAL: "individual",
+ GROUP: "group",
+ RESOURCE: "resource",
+ ROOM: "room",
+ // I've decided UNKNOWN will not be handled.
+ };
+
+ let cutypeString = "event.attendee.usertype." + (cutype in cutypeMap ? cutypeMap[cutype] : "unknown");
+ let tooltip = cal.calGetString("calendar-event-dialog-attendees",
+ cutypeString,
+ cutype in cutypeMap ? null : [cutype]);
+ targetIcon.setAttribute("tooltiptext", tooltip);
+ }
+ ]]></body>
+ </method>
+
+ <property name="documentSize">
+ <getter><![CDATA[
+ return this.mRowHeight * this.mMaxAttendees;
+ ]]></getter>
+ </property>
+
+ <method name="fitDummyRows">
+ <body><![CDATA[
+ setTimeout(() => {
+ this.calcContentHeight();
+ this.createOrRemoveDummyRows();
+ }, 0);
+ ]]></body>
+ </method>
+
+ <method name="calcContentHeight">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let items = listbox.getElementsByTagNameNS("*", "listitem");
+ this.mContentHeight = 0;
+ if (items.length > 0) {
+ let i = 0;
+ do {
+ this.mRowHeight = items[i].boxObject.height;
+ ++i;
+ } while (i < items.length && !this.mRowHeight);
+ this.mContentHeight = this.mRowHeight * items.length;
+ }
+ ]]></body>
+ </method>
+
+ <method name="createOrRemoveDummyRows">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let listboxHeight = listbox.boxObject.height;
+
+ // remove rows to remove scrollbar
+ let kids = listbox.childNodes;
+ for (let i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
+ if (kids[i].hasAttribute("_isDummyRow")) {
+ this.mContentHeight -= this.mRowHeight;
+ kids[i].remove();
+ }
+ }
+
+ // add rows to fill space
+ if (this.mRowHeight) {
+ while (this.mContentHeight + this.mRowHeight < listboxHeight) {
+ this.createDummyItem(listbox);
+ this.mContentHeight += this.mRowHeight;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="createDummyCell">
+ <parameter name="aParent"/>
+ <body><![CDATA[
+ let cell = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listcell");
+ cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+ if (aParent) {
+ aParent.appendChild(cell);
+ }
+ return cell;
+ ]]></body>
+ </method>
+
+ <method name="createDummyItem">
+ <parameter name="aParent"/>
+ <body><![CDATA[
+ let titem = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+ for (let i = this.numColumns; i > 0; i--) {
+ this.createDummyCell(titem);
+ }
+ if (aParent) {
+ aParent.appendChild(titem);
+ }
+ return titem;
+ ]]></body>
+ </method>
+
+ <!-- gets the next row from the top down -->
+ <method name="getNextDummyRow">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let kids = listbox.childNodes;
+ for (let i = 0; i < kids.length; ++i) {
+ if (kids[i].hasAttribute("_isDummyRow")) {
+ return kids[i];
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- This method returns the <xul:listitem> at row numer 'aRow' -->
+ <method name="getListItem">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ if (listbox && aRow > 0) {
+ let listitems = listbox.getElementsByTagNameNS("*", "listitem");
+ if (listitems && listitems.length >= aRow) {
+ return listitems[aRow - 1];
+ }
+ }
+ return 0;
+ ]]></body>
+ </method>
+
+ <method name="getInputFromListitem">
+ <parameter name="aListItem"/>
+ <body><![CDATA[
+ return aListItem.getElementsByTagNameNS("*", "textbox")[0];
+ ]]></body>
+ </method>
+
+ <method name="getRowByInputElement">
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ let row = 0;
+ while (aElement && aElement.localName != "listitem") {
+ aElement = aElement.parentNode;
+ }
+ if (aElement) {
+ while (aElement) {
+ if (aElement.localName == "listitem") {
+ ++row;
+ }
+ aElement = aElement.previousSibling;
+ }
+ }
+ return row;
+ ]]></body>
+ </method>
+
+ <!-- This method returns the <xul:textbox> that contains
+ the name of the attendee at row number 'aRow' -->
+ <method name="getInputElement">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "id", "attendeeCol3#" + aRow);
+ ]]></body>
+ </method>
+
+ <method name="getRoleElement">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "id", "attendeeCol1#" + aRow);
+ ]]></body>
+ </method>
+
+ <method name="getStatusElement">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "id", "attendeeCol1#" + aRow);
+ ]]></body>
+ </method>
+
+ <method name="getUserTypeElement">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "id", "attendeeCol2#" + aRow);
+ ]]></body>
+ </method>
+
+ <method name="setFocus">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ let self = this;
+ let set_focus = function() {
+ let node;
+ if (typeof aRow == "number") {
+ node = self.getListItem(aRow);
+ } else {
+ node = aRow;
+ }
+
+ // do we need to scroll in order to see the selected row?
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ self, "anonid", "listbox");
+ let firstVisibleRow = listbox.getIndexOfFirstVisibleRow();
+ let numOfVisibleRows = listbox.getNumberOfVisibleRows();
+ if (aRow <= firstVisibleRow) {
+ listbox.scrollToIndex(aRow - 1);
+ } else if (aRow - 1 >= (firstVisibleRow + numOfVisibleRows)) {
+ listbox.scrollToIndex(aRow - numOfVisibleRows);
+ }
+ let input =
+ document.getAnonymousElementByAttribute(
+ node, "anonid", "input");
+ input.focus();
+ };
+ setTimeout(set_focus, 0);
+ ]]></body>
+ </method>
+
+ <property name="firstVisibleRow">
+ <getter><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ return listbox.getIndexOfFirstVisibleRow();
+ ]]></getter>
+ </property>
+
+ <method name="createAttendee">
+ <body><![CDATA[
+ let attendee = createAttendee();
+ attendee.id = "";
+ attendee.rsvp = "TRUE";
+ attendee.role = "REQ-PARTICIPANT";
+ attendee.participationStatus = "NEEDS-ACTION";
+ return attendee;
+ ]]></body>
+ </method>
+
+ <property name="numColumns">
+ <getter><![CDATA[
+ if (!this.mNumColumns) {
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let listCols = listbox.getElementsByTagNameNS("*", "listcol");
+ this.mNumColumns = listCols.length;
+ if (!this.mNumColumns) {
+ this.mNumColumns = 1;
+ }
+ }
+ return this.mNumColumns;
+ ]]></getter>
+ </property>
+
+ <property name="ratio">
+ <setter><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let rowcount = listbox.getRowCount();
+ listbox.scrollToIndex(Math.floor(rowcount * val));
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="returnHit">
+ <parameter name="element"/>
+ <parameter name="noAdvance"/>
+ <body><![CDATA[
+ function parseHeaderValue(aMsgIAddressObject) {
+ if (aMsgIAddressObject.name.match(/[<>@,]/)) {
+ // special handling only needed for a name with a comma which are not already quoted
+ return (aMsgIAddressObject.name.match(/^".*"$/)
+ ? aMsgIAddressObject.name
+ : '"' + aMsgIAddressObject.name + '"'
+ ) + " <" + aMsgIAddressObject.email + ">";
+ } else {
+ return aMsgIAddressObject.toString();
+ }
+ }
+
+ let arrowLength = 1;
+ if (element.value.includes(",") || element.value.match(/^[^"].*[<>@,].*[^"] <.+@.+>$/)) {
+ let strippedAddresses = element.value.replace(/.* >> /, "");
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(strippedAddresses);
+ element.value = parseHeaderValue(addresses[0]);
+
+ // the following code is needed to split attendees, if the user enters a comma
+ // separated list of attendees without using autocomplete functionality
+ let insertAfterItem = this.getListItem(this.getRowByInputElement(element));
+ for (let key in addresses) {
+ if (key > 0) {
+ insertAfterItem = this.appendNewRow(false, insertAfterItem);
+ let textinput = this.getInputFromListitem(insertAfterItem);
+ textinput.value = parseHeaderValue(addresses[key]);
+ }
+ }
+ arrowLength = addresses.length;
+ }
+
+ if (!noAdvance) {
+ this.arrowHit(element, arrowLength);
+ }
+ ]]></body>
+ </method>
+
+ <method name="arrowHit">
+ <parameter name="aElement"/>
+ <parameter name="aDirection"/>
+ <body><![CDATA[
+ let row = this.getRowByInputElement(aElement) + aDirection;
+ if (row) {
+ if (row > this.mMaxAttendees) {
+ this.appendNewRow(true);
+ } else {
+ let input = this.getInputElement(row);
+ if (input.hasAttribute("disabled")) {
+ return;
+ }
+ this.setFocus(row);
+ }
+ let event = document.createEvent("Events");
+ event.initEvent("rowchange", true, false);
+ event.details = row;
+ this.dispatchEvent(event);
+ }
+ ]]></body>
+ </method>
+
+ <method name="deleteHit">
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ // don't delete the row if only the organizer is remaining
+ if (this.mMaxAttendees <= 1) {
+ return;
+ }
+
+ let row = this.getRowByInputElement(aElement);
+ this.deleteRow(row);
+ if (row > 0) {
+ row = row - 1;
+ }
+ this.setFocus(row);
+ this.onModify();
+
+ let event = document.createEvent("Events");
+ event.initEvent("rowchange", true, false);
+ event.details = row;
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="deleteRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ // reset id's in order to not break the sequence
+ let maxAttendees = this.mMaxAttendees;
+ this.removeRow(aRow);
+ let numberOfCols = this.numColumns;
+ for (let row = aRow + 1; row <= maxAttendees; row++) {
+ for (let col = 1; col <= numberOfCols; col++) {
+ let colID = "attendeeCol" + col + "#" + row;
+ let elem = document.getAnonymousElementByAttribute(this, "id", colID);
+ if (elem) {
+ elem.setAttribute("id", "attendeeCol" + col + "#" + (row - 1));
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="removeRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ this.getListItem(aRow).remove();
+ this.fitDummyRows();
+ this.mMaxAttendees--;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ function cycle(values, current) {
+ let nextIndex = (values.indexOf(current) + 1) % values.length;
+ return values[nextIndex];
+ }
+
+ let target = event.originalTarget;
+ if (target.className == "role-icon") {
+ if (target.getAttribute("disabled") != "true") {
+ const roleCycle = ["REQ-PARTICIPANT", "OPT-PARTICIPANT",
+ "NON-PARTICIPANT", "CHAIR"];
+
+ let nextValue = cycle(roleCycle, target.getAttribute("role"));
+ target.setAttribute("role", nextValue);
+ this.updateTooltip(target);
+ }
+ } else if (target.className == "status-icon") {
+ if (target.getAttribute("disabled") != "true") {
+ const statusCycle = ["ACCEPTED", "DECLINED", "TENTATIVE"];
+
+ let nextValue = cycle(statusCycle, target.getAttribute("status"));
+ target.setAttribute("status", nextValue);
+ this.updateTooltip(target);
+ }
+ } else if (target.className == "usertype-icon") {
+ let fieldNum = target.getAttribute("id").split("#")[1];
+ let inputField = this.getInputElement(fieldNum);
+ if (target.getAttribute("disabled") != "true" &&
+ !inputField.attendee.isOrganizer) {
+ const cutypeCycle = ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM"];
+
+ let nextValue = cycle(cutypeCycle, target.getAttribute("cutype"));
+ target.setAttribute("cutype", nextValue);
+ this.updateTooltip(target);
+ }
+ } else if (this.mIsReadOnly || this.mIsInvitation || target == null ||
+ (target.localName != "listboxbody" &&
+ target.localName != "listcell" &&
+ target.localName != "listitem")) {
+ // These are cases where we don't want to append a new row, keep
+ // them here so we can put the rest in the else case.
+ } else {
+ let lastInput = this.getInputElement(this.mMaxAttendees);
+ if (lastInput && lastInput.value) {
+ this.appendNewRow(true);
+ }
+ }
+ ]]></handler>
+
+ <handler event="popupshown"><![CDATA[
+ this.mPopupOpen = true;
+ ]]></handler>
+
+ <handler event="popuphidden"><![CDATA[
+ this.mPopupOpen = false;
+ ]]></handler>
+
+ <handler event="keydown"><![CDATA[
+ if (this.mIsReadOnly || this.mIsInvitation) {
+ return;
+ }
+ if (event.originalTarget.localName == "input") {
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_DELETE:
+ case KeyEvent.DOM_VK_BACK_SPACE: {
+ let curRowId = this.getRowByInputElement(event.originalTarget);
+ let allSelected = (event.originalTarget.textLength ==
+ event.originalTarget.selectionEnd -
+ event.originalTarget.selectionStart);
+
+ if (!event.originalTarget.value ||
+ event.originalTarget.textLength < 2 ||
+ allSelected) {
+ // if the user selected the entire attendee string, only one character was
+ // left or the row was already empty before hitting the key, we remove the
+ // entire row to assure the attendee is deleted
+ this.deleteHit(event.originalTarget);
+
+ // if the last row was removed, we append an empty one which has the focus
+ // to enable adding a new attendee directly with freebusy information cleared
+ let targetRowId = (event.keyCode == KeyEvent.DOM_VK_BACK_SPACE && curRowId > 2)
+ ? curRowId - 1 : curRowId;
+ if (this.mMaxAttendees == 1) {
+ this.appendNewRow(true);
+ } else {
+ this.setFocus(targetRowId);
+ }
+
+ // set cursor to begin or end of focused input box based on deletion direction
+ let cPos = 0;
+ let input = document.getAnonymousElementByAttribute(this.getListItem(targetRowId),
+ "anonid", "input");
+ if (targetRowId != curRowId) {
+ cPos = input.textLength;
+ }
+ input.setSelectionRange(cPos, cPos);
+ }
+
+ event.stopPropagation();
+ break;
+ }
+ }
+ }
+ ]]></handler>
+
+ <handler event="keypress" phase="capturing"><![CDATA[
+ // In case we're currently showing the autocompletion popup
+ // don't care about keypress-events and let them go. Otherwise
+ // this event indicates the user wants to travel between
+ // the different attendees. In this case we set the focus
+ // appropriately and stop the event propagation.
+ if (this.mPopupOpen || this.mIsReadOnly || this.mIsInvitation) {
+ return;
+ }
+ if (event.originalTarget.localName == "input") {
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ this.arrowHit(event.originalTarget, -1);
+ event.stopPropagation();
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ this.arrowHit(event.originalTarget, 1);
+ event.stopPropagation();
+ break;
+ case KeyEvent.DOM_VK_TAB:
+ this.arrowHit(event.originalTarget, event.shiftKey ? -1 : +1);
+ break;
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <!-- the 'selection-bar' binding implements the vertical bar that provides
+ a visual indication for the time range the event is configured for. -->
+ <binding id="selection-bar">
+ <content>
+ <xul:scrollbox anonid="scrollbox" width="0" orient="horizontal" flex="1">
+ <xul:box class="selection-bar" anonid="selection-bar">
+ <xul:box class="selection-bar-left" anonid="leftbox"/>
+ <xul:spacer class="selection-bar-spacer" flex="1"/>
+ <xul:box class="selection-bar-right" anonid="rightbox"/>
+ </xul:box>
+ </xul:scrollbox>
+ </content>
+
+ <implementation>
+ <field name="mRange">0</field>
+ <field name="mStartHour">0</field>
+ <field name="mEndHour">24</field>
+ <field name="mContentWidth">0</field>
+ <field name="mHeaderHeight">0</field>
+ <field name="mRatio">0</field>
+ <field name="mBaseDate">null</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mMouseX">0</field>
+ <field name="mMouseY">0</field>
+ <field name="mDragState">0</field>
+ <field name="mMargin">0</field>
+ <field name="mWidth">0</field>
+ <field name="mForce24Hours">false</field>
+ <field name="mZoomFactor">100</field>
+ <!-- constant that defines at which ratio an event is clipped, when moved or resized -->
+ <field name="mfClipRatio">0.7</field>
+ <field name="mLeftBox"/>
+ <field name="mRightBox"/>
+ <field name="mSelectionbar"/>
+
+ <property name="zoomFactor">
+ <getter><![CDATA[
+ return this.mZoomFactor;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mZoomFactor = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="force24Hours">
+ <getter><![CDATA[
+ return this.mForce24Hours;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mForce24Hours = val;
+ this.initTimeRange();
+ this.update();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="ratio">
+ <setter><![CDATA[
+ this.mRatio = val;
+ this.update();
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.initTimeRange();
+
+ // The basedate is the date/time from which the display
+ // of the timebar starts. The range is the number of days
+ // we should be able to show. the start- and enddate
+ // is the time the event is scheduled for.
+ this.mRange = Number(this.getAttribute("range"));
+ this.mSelectionbar =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "selection-bar");
+ ]]></constructor>
+
+ <property name="baseDate">
+ <setter><![CDATA[
+ // we need to convert the date/time in question in
+ // order to calculate with hours that are aligned
+ // with our timebar display.
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.mBaseDate = val.getInTimezone(kDefaultTimezone);
+ this.mBaseDate.isDate = true;
+ this.mBaseDate.makeImmutable();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="startDate">
+ <setter><![CDATA[
+ // currently we *always* set the basedate to be
+ // equal to the startdate. we'll most probably
+ // want to change this later.
+ this.baseDate = val;
+ // we need to convert the date/time in question in
+ // order to calculate with hours that are aligned
+ // with our timebar display.
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.mStartDate = val.getInTimezone(kDefaultTimezone);
+ this.mStartDate.makeImmutable();
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ return this.mStartDate;
+ ]]></getter>
+ </property>
+
+ <property name="endDate">
+ <setter><![CDATA[
+ // we need to convert the date/time in question in
+ // order to calculate with hours that are aligned
+ // with our timebar display.
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.mEndDate = val.getInTimezone(kDefaultTimezone);
+ if (this.mEndDate.isDate) {
+ this.mEndDate.day += 1;
+ }
+ this.mEndDate.makeImmutable();
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ return this.mEndDate;
+ ]]></getter>
+ </property>
+
+ <property name="leftdragWidth">
+ <getter><![CDATA[
+ if (!this.mLeftBox) {
+ this.mLeftBox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "leftbox");
+ }
+ return this.mLeftBox.boxObject.width;
+ ]]></getter>
+ </property>
+ <property name="rightdragWidth">
+ <getter><![CDATA[
+ if (!this.mRightBox) {
+ this.mRightBox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "rightbox");
+ }
+ return this.mRightBox.boxObject.width;
+ ]]></getter>
+ </property>
+
+ <method name="init">
+ <parameter name="width"/>
+ <parameter name="height"/>
+ <body><![CDATA[
+ this.mContentWidth = width;
+ this.mHeaderHeight = height + 2;
+ this.mMargin = 0;
+ this.update();
+ ]]></body>
+ </method>
+
+ <!-- given some specific date this method calculates the
+ corrposonding offset in fractional hours -->
+ <method name="date2offset">
+ <parameter name="date"/>
+ <body><![CDATA[
+ let num_hours = this.mEndHour - this.mStartHour;
+ let diff = date.subtractDate(this.mBaseDate);
+ let offset = diff.days * num_hours;
+ let hours = (diff.hours - this.mStartHour) + (diff.minutes / 60.0);
+ if (hours < 0) {
+ hours = 0;
+ }
+ if (hours > num_hours) {
+ hours = num_hours;
+ }
+ offset += hours;
+ return offset;
+ ]]></body>
+ </method>
+
+ <method name="update">
+ <body><![CDATA[
+ if (!this.mStartDate || !this.mEndDate) {
+ return;
+ }
+
+ // Calculate the relation of startdate/basedate and enddate/startdate.
+ let offset = this.mStartDate.subtractDate(this.mBaseDate);
+
+ // Calculate how much pixels a single hour and a single day take up.
+ let num_hours = this.mEndHour - this.mStartHour;
+ let hour_width = this.mContentWidth / num_hours;
+
+ // Calculate the offset in fractional hours that corrospond
+ // to our start- and end-time.
+ let start_offset_in_hours = this.date2offset(this.mStartDate);
+ let end_offset_in_hours = this.date2offset(this.mEndDate);
+ let duration_in_hours = end_offset_in_hours - start_offset_in_hours;
+
+ // Calculate width & margin for the selection bar based on the
+ // relation of startdate/basedate and enddate/startdate.
+ // This is a simple conversion from hours to pixels.
+ this.mWidth = duration_in_hours * hour_width;
+ let totaldragwidths = this.leftdragWidth + this.rightdragWidth;
+ if (this.mWidth < totaldragwidths) {
+ this.mWidth = totaldragwidths;
+ }
+ this.mMargin = start_offset_in_hours * hour_width;
+
+ // Calculate the difference between content and container in pixels.
+ // The container is the window showing this control, the content is the
+ // total number of pixels the selection bar can theoretically take up.
+ let total_width = this.mContentWidth * this.mRange - this.parentNode.boxObject.width;
+
+ // Calculate the current scroll offset.
+ offset = Math.floor(total_width * this.mRatio);
+
+ // The final margin is the difference between the date-based margin
+ // and the scroll-based margin.
+ this.mMargin -= offset;
+
+ // Set the styles based on the calculations above for the 'selection-bar'.
+ let style = "width: " + this.mWidth +
+ "px; margin-inline-start: " + this.mMargin +
+ "px; margin-top: " + this.mHeaderHeight + "px;";
+ this.mSelectionbar.setAttribute("style", style);
+
+ let event = document.createEvent("Events");
+ event.initEvent("timechange", true, false);
+ event.startDate = this.mStartDate;
+ event.endDate = this.mEndDate.clone();
+ if (event.endDate.isDate) {
+ event.endDate.day--;
+ }
+ event.endDate.makeImmutable();
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="setWidth">
+ <parameter name="width"/>
+ <body><![CDATA[
+ let scrollbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "scrollbox");
+ scrollbox.setAttribute("width", width);
+ ]]></body>
+ </method>
+
+ <method name="initTimeRange">
+ <body><![CDATA[
+ if (this.force24Hours) {
+ this.mStartHour = 0;
+ this.mEndHour = 24;
+ } else {
+ this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+ ]]></body>
+ </method>
+
+ <method name="moveTime">
+ <parameter name="time"/>
+ <parameter name="delta"/>
+ <parameter name="doclip"/>
+ <body><![CDATA[
+ let newTime = time.clone();
+ let clip_minutes = 60 * this.zoomFactor / 100;
+ if (newTime.isDate) {
+ clip_minutes = 60 * 24;
+ }
+ let num_hours = this.mEndHour - this.mStartHour;
+ let hour_width = this.mContentWidth / num_hours;
+ let minutes_per_pixel = 60 / hour_width;
+ let minute_shift = minutes_per_pixel * delta;
+ let isClipped = Math.abs(minute_shift) >= (this.mfClipRatio * clip_minutes);
+ if (isClipped) {
+ if (delta > 0) {
+ if (time.isDate) {
+ newTime.day++;
+ } else {
+ if (doclip) {
+ newTime.minute -= newTime.minute % clip_minutes;
+ }
+ newTime.minute += clip_minutes;
+ }
+ } else if (delta < 0) {
+ if (time.isDate) {
+ newTime.day--;
+ } else {
+ if (doclip) {
+ newTime.minute -= newTime.minute % clip_minutes;
+ }
+ newTime.minute -= clip_minutes;
+ }
+ }
+ }
+
+ if (!newTime.isDate) {
+ if (newTime.hour < this.mStartHour) {
+ newTime.hour = this.mEndHour - 1;
+ newTime.day--;
+ }
+ if (newTime.hour >= this.mEndHour) {
+ newTime.hour = this.mStartHour;
+ newTime.day++;
+ }
+ }
+
+ return newTime;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown"><![CDATA[
+ let element = event.target;
+ this.mMouseX = event.screenX;
+ let mouseX = event.clientX - element.boxObject.x;
+
+ if (mouseX >= this.mMargin) {
+ if (mouseX <= (this.mMargin + this.mWidth)) {
+ if (mouseX <= (this.mMargin + this.leftdragWidth)) {
+ // Move the startdate only...
+ window.setCursor("w-resize");
+ this.mDragState = 2;
+ } else if (mouseX >= (this.mMargin + this.mWidth - (this.rightdragWidth))) {
+ // Move the enddate only..
+ window.setCursor("e-resize");
+ this.mDragState = 3;
+ } else {
+ // Move the startdate and the enddate
+ this.mDragState = 1;
+ window.setCursor("grab");
+ }
+ }
+ }
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ let mouseX = event.screenX;
+ if (this.mDragState == 1) {
+ // Move the startdate and the enddate
+ let delta = mouseX - this.mMouseX;
+ let newStart = this.moveTime(this.mStartDate, delta, false);
+ if (newStart.compare(this.mStartDate) != 0) {
+ newEnd = this.moveTime(this.mEndDate, delta, false);
+
+ // We need to adapt this date in case we're dealing with
+ // an all-day event. This is because setting 'endDate' will
+ // automatically add one day extra for all-day events.
+ if (newEnd.isDate) {
+ newEnd.day--;
+ }
+
+ this.startDate = newStart;
+ this.endDate = newEnd;
+ this.mMouseX = mouseX;
+ this.update();
+ }
+ } else if (this.mDragState == 2) {
+ // Move the startdate only...
+ let delta = event.screenX - this.mSelectionbar.boxObject.screenX;
+ let newStart = this.moveTime(this.mStartDate, delta, true);
+ if (newStart.compare(this.mEndDate) >= 0) {
+ if (this.mStartDate.isDate) {
+ return;
+ }
+ newStart = this.mEndDate;
+ }
+ if (newStart.compare(this.mStartDate) != 0) {
+ this.startDate = newStart;
+ this.update();
+ }
+ } else if (this.mDragState == 3) {
+ // Move the enddate only..
+ let delta = mouseX - (this.mSelectionbar.boxObject.screenX +
+ this.mSelectionbar.boxObject.width);
+ let newEnd = this.moveTime(this.mEndDate, delta, true);
+ if (newEnd.compare(this.mStartDate) < 0) {
+ newEnd = this.mStartDate;
+ }
+ if (newEnd.compare(this.mEndDate) != 0) {
+ // We need to adapt this date in case we're dealing with
+ // an all-day event. This is because setting 'endDate' will
+ // automatically add one day extra for all-day events.
+ if (newEnd.isDate) {
+ newEnd.day--;
+ }
+
+ // Don't allow all-day events to be shorter than a single day.
+ if (!newEnd.isDate || (newEnd.compare(this.startDate) >= 0)) {
+ this.endDate = newEnd;
+ this.update();
+ }
+ }
+ }
+ ]]></handler>
+
+ <handler event="mouseup"><![CDATA[
+ this.mDragState = 0;
+ window.setCursor("auto");
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-attendees.xul b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xul
new file mode 100644
index 000000000..de09754a6
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xul
@@ -0,0 +1,198 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %dtd2;
+]>
+
+<dialog id="calendar-event-dialog-attendees-v2"
+ title="&invite.title.label;"
+ windowtype="Calendar:EventDialog:Attendees"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ defaultButton="none"
+ persist="screenX screenY height width"
+ orient="vertical"
+ style="padding-top: 8px; padding-bottom: 10px; padding-inline-start: 8px; padding-inline-end: 10px;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-event-dialog-attendees.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <hbox align="center" pack="end">
+ <spacer flex="1"/>
+ <label value="&event.freebusy.suggest.slot;"/>
+ <button label="&event.freebusy.button.previous.slot;"
+ dir="normal"
+ class="left-icon"
+ id="previous-slot"
+ oncommand="onPreviousSlot();"/>
+ <button label="&event.freebusy.button.next.slot;"
+ dir="reverse"
+ class="right-icon"
+ id="next-slot"
+ oncommand="onNextSlot();"/>
+ <spacer style="width: 10em"/>
+ <label value="&event.freebusy.zoom;" control="zoom-menulist"/>
+ <toolbarbutton id="zoom-out-button"
+ class="zoom-out-icon"
+ oncommand="zoomWithButtons(true);"/>
+ <menulist id="zoom-menulist"
+ oncommand="setZoomFactor(this.value);"
+ persist="value">
+ <menupopup>
+ <menuitem label="400%" value="25"/>
+ <menuitem label="200%" value="50"/>
+ <menuitem label="100%" value="100"/>
+ <menuitem label="50%" value="200"/>
+ <menuitem label="25%" value="400"/>
+ </menupopup>
+ </menulist>
+ <toolbarbutton id="zoom-in-button"
+ class="zoom-in-icon"
+ oncommand="zoomWithButtons(false);"/>
+ </hbox>
+ <hbox flex="1">
+ <vbox id="attendees-container" flex="1" persist="width">
+ <box class="attendee-spacer-top"/>
+ <attendees-list flex="1" id="attendees-list"/>
+ <box class="attendee-spacer-bottom"/>
+ </vbox>
+ <splitter id="splitter"/>
+ <vbox id="freebusy-container" persist="width">
+ <stack flex="1">
+ <vbox flex="1">
+ <freebusy-timebar id="timebar"
+ range="16"/>
+ <freebusy-grid flex="1"
+ id="freebusy-grid"
+ range="16"/>
+ </vbox>
+ <selection-bar id="selection-bar"
+ range="16"/>
+ </stack>
+ <scrollbar orient="horizontal"
+ id="horizontal-scrollbar"
+ maxpos="100"/>
+ </vbox>
+ <vbox
+ id="vertical-scrollbar-box"
+ collapsed="true">
+ <box class="attendee-spacer-top"/>
+ <scrollbar orient="vertical"
+ flex="1"
+ id="vertical-scrollbar"
+ maxpos="100"/>
+ <box class="attendee-spacer-bottom"/>
+ </vbox>
+ </hbox>
+ <hbox>
+ <grid flex="1">
+ <columns>
+ <column/> <!-- role icon -->
+ <column flex="1"/><!-- role description -->
+ <column/> <!-- status color -->
+ <column flex="1"/><!-- status description -->
+ <column/> <!-- status color -->
+ <column flex="1"/><!-- status description -->
+ </columns>
+ <rows>
+ <row align="center">
+ <image class="role-icon" role="REQ-PARTICIPANT"/>
+ <label value="&event.attendee.role.required;"/>
+ <image class="usertype-icon" cutype="INDIVIDUAL"/>
+ <label value="&event.attendee.usertype.individual;"/>
+ <box class="legend" status="FREE"/>
+ <label value="&event.freebusy.legend.free;"/>
+ </row>
+ <row align="center">
+ <image class="role-icon" role="OPT-PARTICIPANT"/>
+ <label value="&event.attendee.role.optional;"/>
+ <image class="usertype-icon" cutype="GROUP"/>
+ <label value="&event.attendee.usertype.group;"/>
+ <box class="legend" status="BUSY_TENTATIVE"/>
+ <label value="&event.freebusy.legend.busy_tentative;"/>
+ </row>
+ <row align="center">
+ <image class="role-icon" role="CHAIR"/>
+ <label value="&event.attendee.role.chair;"/>
+ <image class="usertype-icon" cutype="RESOURCE"/>
+ <label value="&event.attendee.usertype.resource;"/>
+ <box class="legend" status="BUSY"/>
+ <label value="&event.freebusy.legend.busy;"/>
+ </row>
+ <row align="center">
+ <image class="role-icon" role="NON-PARTICIPANT"/>
+ <label value="&event.attendee.role.nonparticipant;"/>
+ <image class="usertype-icon" cutype="ROOM"/>
+ <label value="&event.attendee.usertype.room;"/>
+ <box class="legend" status="BUSY_UNAVAILABLE"/>
+ <label value="&event.freebusy.legend.busy_unavailable;"/>
+ </row>
+ <row align="center">
+ <spacer/>
+ <spacer/>
+ <spacer/>
+ <spacer/>
+ <box class="legend" status="UNKNOWN"/>
+ <label value="&event.freebusy.legend.unknown;"/>
+ </row>
+ </rows>
+ </grid>
+ <vbox>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <spacer/>
+ <checkbox id="all-day"
+ oncommand="changeAllDay();"
+ label="&event.alldayevent.label;"/>
+ </row>
+ <row align="center">
+ <label value="&newevent.from.label;" control="event-starttime"/>
+ <datetimepicker id="event-starttime"
+ onchange="updateStartTime();"/>
+ <label id="timezone-starttime"
+ crop="right"
+ class="text-link"
+ flex="1"
+ collapsed="true"
+ hyperlink="true"
+ onclick="editStartTimezone()"/>
+ </row>
+ <row align="center">
+ <label value="&newevent.to.label;" control="event-endtime"/>
+ <datetimepicker id="event-endtime"
+ onchange="updateEndTime();"/>
+ <label id="timezone-endtime"
+ crop="right"
+ class="text-link"
+ flex="1"
+ collapsed="true"
+ hyperlink="true"
+ onclick="editEndTimezone()"/>
+ </row>
+ </rows>
+ </grid>
+ </vbox>
+ </hbox>
+ <separator class="groove"/>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml b/calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml
new file mode 100644
index 000000000..94066fbdb
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml
@@ -0,0 +1,1599 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %dtd3;
+]>
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!--
+ ########################################################################
+ ## scroll-container
+ ########################################################################
+ -->
+ <binding id="scroll-container" extends="xul:box">
+ <content>
+ <xul:box class="container"
+ xbl:inherits="flex"
+ anonid="container"
+ style="overflow: hidden; clip: rect(0px 0px 0px 0px);">
+ <xul:box class="content"
+ xbl:inherits="flex,orient"
+ anonid="content">
+ <children/>
+ </xul:box>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <property name="x">
+ <getter><![CDATA[
+ let content =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "content");
+ let margin = getComputedStyle(content, null).marginInlineStart;
+ return -parseInt(margin.replace(/px/, ""), 10);
+ ]]></getter>
+ <setter><![CDATA[
+ let content =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "content");
+ content.setAttribute("style",
+ "margin-inline-start: " + (-val) + "px;");
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="y">
+ <getter><![CDATA[
+ let content =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "content");
+ let margin = getComputedStyle(content, null).marginTop;
+ return -parseInt(margin.replace(/px/, ""), 10);
+ ]]></getter>
+ <setter><![CDATA[
+ let content =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "content");
+ content.setAttribute("style",
+ "margin-top: " + (-val) + "px;");
+ return val;
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <!--
+ ########################################################################
+ ## freebusy-day
+ ########################################################################
+ -->
+ <binding id="freebusy-day" extends="xul:box">
+ <content>
+ <xul:box orient="vertical">
+ <xul:text class="freebusy-timebar-title"
+ style="font-weight:bold;"
+ anonid="day"/>
+ <xul:box equalsize="always" anonid="hours"/>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <field name="mDateFormatter">null</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mStartHour">0</field>
+ <field name="mEndHour">24</field>
+ <field name="mForce24Hours">false</field>
+ <field name="mZoomFactor">100</field>
+
+ <property name="zoomFactor">
+ <getter><![CDATA[
+ return this.mZoomFactor;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mZoomFactor = val;
+ let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
+ removeChildren(hours);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="force24Hours">
+ <getter><![CDATA[
+ return this.mForce24Hours;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mForce24Hours = val;
+ this.initTimeRange();
+
+ let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
+ removeChildren(hours);
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.initTimeRange();
+ ]]></constructor>
+
+ <method name="initTimeRange">
+ <body><![CDATA[
+ if (this.force24Hours) {
+ this.mStartHour = 0;
+ this.mEndHour = 24;
+ } else {
+ this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+ ]]></body>
+ </method>
+
+ <property name="startDate">
+ <setter><![CDATA[
+ this.mStartDate = val.clone();
+ this.mStartDate.minute = 0;
+ this.mStartDate.second = 0;
+ this.mStartDate.makeImmutable();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="endDate">
+ <setter><![CDATA[
+ this.mEndDate = val.clone();
+ this.mEndDate.makeImmutable();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="dayHeight">
+ <getter><![CDATA[
+ let day =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "day");
+ return day.boxObject.height;
+ ]]></getter>
+ </property>
+
+ <property name="date">
+ <setter><![CDATA[
+ let date = val.clone();
+ date.hour = 0;
+ date.minute = 0;
+ date.isDate = false;
+
+ if (!this.mDateFormatter) {
+ this.mDateFormatter =
+ Components.classes[
+ "@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(
+ Components.interfaces.calIDateTimeFormatter);
+ }
+
+ // First set the formatted date string as title
+ let day =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "day");
+ let dateValue = this.mZoomFactor > 100 ? this.mDateFormatter.formatDateShort(date)
+ : this.mDateFormatter.formatDateLong(date);
+ day.setAttribute("value", dateValue);
+
+ // Now create as many 'hour' elements as needed
+ let step_in_minutes = Math.floor(60 * this.mZoomFactor / 100);
+ let hours =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "hours");
+ date.hour = this.mStartHour;
+ if (hours.childNodes.length <= 0) {
+ let template = createXULElement("text");
+ template.className = "freebusy-timebar-hour";
+ let count = Math.ceil(
+ (this.mEndHour - this.mStartHour) * 60 / step_in_minutes);
+ let remain = count;
+ let first = true;
+ while (remain--) {
+ let newNode = template.cloneNode(false);
+ let value = this.mDateFormatter.formatTime(date);
+ if (first) {
+ newNode.className += " first-in-day";
+ first = false;
+ }
+ newNode.setAttribute("value", value);
+ hours.appendChild(newNode);
+ date.minute += step_in_minutes;
+
+ if (remain == 0) {
+ newNode.className += " last-in-day";
+ }
+ }
+ }
+
+ return val;
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <!--
+ ########################################################################
+ ## freebusy-timebar
+ ########################################################################
+ -->
+ <binding id="freebusy-timebar" extends="xul:box">
+ <content>
+ <xul:listbox anonid="listbox"
+ class="listbox-noborder"
+ seltype="multiple"
+ rows="1"
+ flex="1"
+ disabled="true">
+ <xul:listcols>
+ <xul:listcol anonid="day-column" flex="1"/>
+ </xul:listcols>
+ <xul:listitem anonid="item"
+ class="freebusy-listitem"
+ allowevents="true">
+ <xul:listcell >
+ <xul:scroll-container anonid="container">
+ <xul:freebusy-day anonid="template"/>
+ </xul:scroll-container>
+ </xul:listcell>
+ </xul:listitem>
+ </xul:listbox>
+ </content>
+
+ <implementation>
+ <field name="mNumDays">0</field>
+ <field name="mRange">0</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mDayOffset">0</field>
+ <field name="mScrollOffset">0</field>
+ <field name="mStartHour">0</field>
+ <field name="mEndHour">24</field>
+ <field name="mForce24Hours">false</field>
+ <field name="mZoomFactor">100</field>
+
+ <property name="zoomFactor">
+ <getter><![CDATA[
+ return this.mZoomFactor;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mZoomFactor = val;
+
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+ let parent = template.parentNode;
+ while (parent.childNodes.length > 1) {
+ parent.lastChild.remove();
+ }
+
+ template.force24Hours = this.mForce24Hours;
+ template.zoomFactor = this.mZoomFactor;
+
+ this.onLoad();
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="force24Hours">
+ <getter><![CDATA[
+ return this.mForce24Hours;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mForce24Hours = val;
+ this.initTimeRange();
+
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+
+ let parent = template.parentNode;
+ while (parent.childNodes.length > 1) {
+ parent.lastChild.remove();
+ }
+
+ template.force24Hours = this.mForce24Hours;
+ template.zoomFactor = this.mZoomFactor;
+
+ this.onLoad();
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="contentWidth">
+ <getter><![CDATA[
+ // Calculate the difference between the first to day-elements, since
+ // the width of the head element does not specify the width we need
+ // due to an arbitrary margin value.
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+ return template.nextSibling.boxObject.x - template.boxObject.x;
+ ]]></getter>
+ </property>
+
+ <property name="containerWidth">
+ <getter><![CDATA[
+ return this.parentNode.boxObject.width;
+ ]]></getter>
+ </property>
+
+ <property name="startDate">
+ <setter><![CDATA[
+ this.mStartDate = val.clone();
+ this.mStartDate.makeImmutable();
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ return this.mStartDate;
+ ]]></getter>
+ </property>
+
+ <property name="endDate">
+ <setter><![CDATA[
+ this.mEndDate = val.clone();
+ this.mEndDate.makeImmutable();
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ return this.mEndDate;
+ ]]></getter>
+ </property>
+
+ <property name="dayOffset">
+ <setter><![CDATA[
+ this.mDayOffset = val;
+ let container =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "container");
+ let date = this.mStartDate.clone();
+ date.day += val;
+ let numChilds = container.childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let child = container.childNodes[i];
+ child.date = date;
+ date.day++;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="step">
+ <getter><![CDATA[
+ // How much pixels spans a single day
+ let oneday = this.contentWidth;
+
+ // The difference in pixels between the content and the container.
+ let shift = (oneday * this.mRange) - (this.containerWidth);
+
+ // What we want to know is the scale of the total shift
+ // needed to step one block further. Since the content
+ // is divided into 'numHours' equal parts, we can simply state:
+ let numHours = this.mEndHour - this.mStartHour;
+ return (this.contentWidth) / (numHours * shift);
+ ]]></getter>
+ </property>
+
+ <property name="scroll">
+ <setter><![CDATA[
+ this.mScrollOffset = val;
+
+ // How much pixels spans a single day
+ let oneday = this.contentWidth;
+
+ // The difference in pixels between the content and the container.
+ let shift = (oneday * this.mRange) - (this.containerWidth);
+
+ // Now calculate the (positive) offset in pixels which the content
+ // needs to be shifted. This is a simple scaling in one dimension.
+ let offset = Math.floor(val * shift);
+
+ // Now find out how much days this offset effectively skips.
+ // this is a simple division which always yields a positive integer value.
+ this.dayOffset = (offset - (offset % oneday)) / oneday;
+
+ // Set the pixel offset for the content which will always need
+ // to be in the range [0 <= offset <= oneday].
+ offset %= oneday;
+
+ // Set the offset at the content node.
+ let container =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "container");
+ container.x = offset;
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ return this.mScrollOffset;
+ ]]></getter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ let args = window.arguments[0];
+ let startTime = args.startTime;
+ let endTime = args.endTime;
+
+ this.initTimeRange();
+
+ // The basedate is the date/time from which the display
+ // of the timebar starts. The range is the number of days
+ // we should be able to show. The start- and enddate
+ // is the time the event is scheduled for.
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.startDate = startTime.getInTimezone(kDefaultTimezone);
+ this.endDate = endTime.getInTimezone(kDefaultTimezone);
+ this.mRange = Number(this.getAttribute("range"));
+
+ window.addEventListener("load", this.onLoad.bind(this), true);
+ ]]></constructor>
+
+ <method name="refresh">
+ <body><![CDATA[
+ let date = this.mStartDate.clone();
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+ let parent = template.parentNode;
+ let numChilds = parent.childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let child = parent.childNodes[i];
+ child.startDate = this.mStartDate;
+ child.endDate = this.mEndDate;
+ child.date = date;
+ date.day++;
+ }
+ let offset = this.mDayOffset;
+ this.dayOffset = offset;
+ ]]></body>
+ </method>
+
+ <method name="onLoad">
+ <body><![CDATA[
+ this.initialize();
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+ let event = document.createEvent("Events");
+ event.initEvent("timebar", true, false);
+ event.details = this.contentWidth;
+ event.height = template.dayHeight;
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="initialize">
+ <body><![CDATA[
+ let args = window.arguments[0];
+ let startTime = args.startTime;
+ let endTime = args.endTime;
+
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.startDate = startTime.getInTimezone(kDefaultTimezone);
+ this.endDate = endTime.getInTimezone(kDefaultTimezone);
+
+ // Set the number of 'freebusy-day'-elements
+ // we need to fill up the content box.
+ // TODO: hardcoded value
+ this.mNumDays = 4 * this.mZoomFactor / 100;
+ if (this.mNumDays < 2) {
+ this.mNumDays = 2;
+ }
+
+ // Now create those elements and set their date property.
+ let date = this.mStartDate.clone();
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "template");
+ template.force24Hours = this.mForce24Hours;
+ template.zoomFactor = this.mZoomFactor;
+ template.startDate = this.mStartDate;
+ template.endDate = this.mEndDate;
+ template.date = date;
+ let parent = template.parentNode;
+ if (parent.childNodes.length <= 1) {
+ let count = this.mNumDays - 1;
+ if (count > 0) {
+ for (let i = 0; i < count; i++) {
+ date.day++;
+ let newNode = template.cloneNode(false);
+ newNode.force24Hours = this.mForce24Hours;
+ newNode.zoomFactor = this.mZoomFactor;
+ newNode.startDate = this.mStartDate;
+ newNode.endDate = this.mEndDate;
+ newNode.date = date;
+ parent.appendChild(newNode);
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="initTimeRange">
+ <body><![CDATA[
+ if (this.force24Hours) {
+ this.mStartHour = 0;
+ this.mEndHour = 24;
+ } else {
+ this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!--
+ ########################################################################
+ ## freebusy-row
+ ########################################################################
+ -->
+ <binding id="freebusy-row" extends="xul:box">
+ <content>
+ <xul:scroll-container flex="1" anonid="container">
+ <xul:box equalsize="always" anonid="hours"/>
+ </xul:scroll-container>
+ </content>
+
+ <implementation>
+ <field name="mState">null</field>
+ <field name="mEntries">null</field>
+ <field name="mOffset">0</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mRange">0</field>
+ <field name="mStartHour">0</field>
+ <field name="mEndHour">24</field>
+ <field name="mForce24Hours">false</field>
+ <field name="mZoomFactor">100</field>
+
+ <property name="zoomFactor">
+ <getter><![CDATA[
+ return this.mZoomFactor;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mZoomFactor = val;
+
+ let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
+ removeChildren(hours);
+ this.onLoad();
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="force24Hours">
+ <getter><![CDATA[
+ return this.mForce24Hours;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mForce24Hours = val;
+ this.initTimeRange();
+
+ let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
+ removeChildren(hours);
+ this.onLoad();
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="startDate">
+ <setter><![CDATA[
+ this.mStartDate = val.clone();
+ this.mStartDate.isDate = false;
+ this.mStartDate.makeImmutable();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="endDate">
+ <setter><![CDATA[
+ this.mEndDate = val.clone();
+ this.mEndDate.isDate = false;
+ this.mEndDate.makeImmutable();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="numHours">
+ <getter><![CDATA[
+ let numHours = this.mEndHour - this.mStartHour;
+ return Math.ceil(numHours * 100 / this.mZoomFactor);
+ ]]></getter>
+ </property>
+
+ <property name="contentWidth">
+ <getter><![CDATA[
+ let hours =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "hours");
+ return (hours.childNodes[1].boxObject.x -
+ hours.childNodes[0].boxObject.x) *
+ this.numHours;
+ ]]></getter>
+ </property>
+
+ <property name="containerWidth">
+ <getter><![CDATA[
+ // Step up the hierarchy until we reach the listbox
+ return this.parentNode
+ .parentNode
+ .parentNode
+ .parentNode.boxObject.width;
+ ]]></getter>
+ </property>
+
+ <property name="dayOffset">
+ <setter><![CDATA[
+ this.mOffset = val * this.numHours;
+ this.showState();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="documentSize">
+ <getter><![CDATA[
+ return this.contentWidth * this.mRange;
+ ]]></getter>
+ </property>
+
+ <property name="scroll">
+ <setter><![CDATA[
+ // How much pixels spans a single day
+ let oneday = this.contentWidth;
+ if (oneday <= 0) {
+ return val;
+ }
+
+ // The difference in pixels between the content and the container.
+ let shift = (oneday * this.mRange) - (this.containerWidth);
+
+ // Now calculate the (positive) offset in pixels which the content
+ // needs to be shifted. This is a simple scaling in one dimension.
+ let offset = Math.floor(val * shift);
+
+ // Now find out how much days this offset effectively skips.
+ // this is a simple division which always yields a positive integer value.
+ this.dayOffset = (offset - (offset % oneday)) / oneday;
+
+ // Set the pixel offset for the content which will always need
+ // to be in the range [0 <= offset <= oneday].
+ offset %= oneday;
+
+ // Set the offset at the content node.
+ let container =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "container");
+ container.x = offset;
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.initTimeRange();
+ this.mRange = Number(this.getAttribute("range"));
+ this.onLoad();
+ ]]></constructor>
+
+ <method name="onLoad">
+ <body><![CDATA[
+ let numHours = this.mEndHour - this.mStartHour;
+ this.mState = new Array(this.mRange * numHours);
+ for (let i = 0; i < this.mState.length; i++) {
+ this.mState[i] = Components.interfaces.calIFreeBusyInterval.UNKNOWN;
+ }
+
+ let step_in_minutes = Math.floor(60 * this.mZoomFactor / 100);
+ let formatter = Components.classes[
+ "@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(
+ Components.interfaces.calIDateTimeFormatter);
+ let date = cal.jsDateToDateTime(new Date());
+ date.hour = this.mStartHour;
+ date.minute = 0;
+ let hours =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "hours");
+ if (hours.childNodes.length <= 0) {
+ let template = createXULElement("text");
+ template.className = "freebusy-grid";
+ // TODO: hardcoded value
+ let num_days = Math.max(2, 4 * this.mZoomFactor / 100);
+ let count = Math.ceil(
+ (this.mEndHour - this.mStartHour) * 60 / step_in_minutes);
+ let remain = count;
+ for (let day = 1; day <= num_days; day++) {
+ let first = true;
+ while (remain--) {
+ let newNode = template.cloneNode(false);
+ let value = formatter.formatTime(date);
+ if (first) {
+ newNode.className += " first-in-day";
+ first = false;
+ }
+
+ newNode.setAttribute("value", value);
+ hours.appendChild(newNode);
+ date.minute += step_in_minutes;
+
+ if (remain == 0) {
+ newNode.className += " last-in-day";
+ }
+ }
+ date.hour = this.mStartHour;
+ date.day++;
+ remain = count;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="onFreeBusy">
+ <parameter name="aEntries"/>
+ <body><![CDATA[
+ // The argument denotes the requested freebusy intervals.
+ // We need to set our state array according to this
+ // result. After the state has been updated we call showState()
+ // which will map the entries to attributes on the xul elements.
+ if (aEntries) {
+ // Remember the free/busy array which is used to find a
+ // new time for an event. We store this array only if
+ // the provider returned a valid array. In any other case
+ // (temporarily clean the display) we keep the last know result.
+ this.mEntries = aEntries;
+
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ let start = this.mStartDate.clone();
+ start.hour = 0;
+ start.minute = 0;
+ start.second = 0;
+ start.timezone = kDefaultTimezone;
+ let end = start.clone();
+ end.day += this.mRange;
+ end.timezone = kDefaultTimezone;
+
+ // First of all set all state slots to 'free'
+ for (let i = 0; i < this.mState.length; i++) {
+ this.mState[i] = Components.interfaces.calIFreeBusyInterval.FREE;
+ }
+
+ // Iterate all incoming freebusy entries
+ for (let entry of aEntries) {
+ let rangeStart = entry.interval.start.getInTimezone(kDefaultTimezone);
+ let rangeEnd = entry.interval.end.getInTimezone(kDefaultTimezone);
+
+ if (rangeStart.compare(start) < 0) {
+ rangeStart = start.clone();
+ }
+ if (rangeEnd.compare(end) > 0) {
+ rangeEnd = end.clone();
+ }
+
+ let rangeDuration = rangeEnd.subtractDate(rangeStart);
+ let rangeStartHour = rangeStart.hour;
+ let rangeEndHour = rangeStartHour + (rangeDuration.inSeconds / 3600);
+
+ if ((rangeStartHour < this.mEndHour) &&
+ (rangeEndHour >= this.mStartHour)) {
+ let dayingrid = start.clone();
+ dayingrid.year = rangeStart.year;
+ dayingrid.month = rangeStart.month;
+ dayingrid.day = rangeStart.day;
+ dayingrid.getInTimezone(kDefaultTimezone);
+
+ // Ok, this is an entry we're interested in. Find out
+ // which hours are actually occupied.
+ let offset = rangeStart.subtractDate(dayingrid);
+
+ // Calculate how many days we're offset from the
+ // start of the grid. Eliminate hours in case
+ // we encounter the daylight-saving hop.
+ let dayoffset = dayingrid.subtractDate(start);
+ dayoffset.hours = 0;
+
+ // Add both offsets to find the total offset.
+ // dayoffset -> offset in days from start of grid
+ // offset -> offset in hours from start of current day
+ offset.addDuration(dayoffset);
+
+ let duration = rangeEnd.subtractDate(rangeStart);
+ let start_in_minutes = Math.floor(offset.inSeconds / 60);
+ let end_in_minutes = Math.ceil((duration.inSeconds / 60) +
+ (offset.inSeconds / 60));
+
+ let minute2offset = function(value, fNumHours, numHours, start_hour, zoomfactor) {
+ // 'value' is some integer in the interval [0, range * 24 * 60].
+ // we need to map this offset into our array which
+ // holds elements for 'range' days with [start, end] hours each.
+ let minutes_per_day = 24 * 60;
+ let day = (value - (value % minutes_per_day)) / minutes_per_day;
+ let minute = Math.floor(value % minutes_per_day) - (start_hour * 60);
+
+ minute = Math.max(0, minute);
+
+ if (minute >= (numHours * 60)) {
+ minute = (numHours * 60) - 1;
+ }
+ // How to get from minutes to offset?
+ // 60 = 100%, 30 = 50%, 15 = 25%, etc.
+ let minutes_per_block = 60 * zoomfactor / 100;
+
+ let block = Math.floor(minute / minutes_per_block);
+
+ return Math.ceil(fNumHours) * day + block;
+ };
+
+ // Number of hours (fractional representation)
+ let calcNumHours = this.mEndHour - this.mStartHour;
+ let fNumHours = calcNumHours * 100 / this.mZoomFactor;
+
+ let start_offset =
+ minute2offset(start_in_minutes,
+ fNumHours,
+ calcNumHours,
+ this.mStartHour,
+ this.mZoomFactor);
+ let end_offset =
+ minute2offset(end_in_minutes - 1,
+ fNumHours,
+ calcNumHours,
+ this.mStartHour,
+ this.mZoomFactor);
+
+ // Set all affected state slots
+ for (let i = start_offset; i <= end_offset; i++) {
+ this.mState[i] = entry.freeBusyType;
+ }
+ }
+ }
+ } else {
+ // First of all set all state slots to 'unknown'
+ for (let i = 0; i < this.mState.length; i++) {
+ this.mState[i] = Components.interfaces.calIFreeBusyInterval.UNKNOWN;
+ }
+ }
+
+ this.showState();
+ ]]></body>
+ </method>
+
+ <method name="showState">
+ <body><![CDATA[
+ let hours =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "hours");
+ for (let i = 0; i < hours.childNodes.length; i++) {
+ let hour = hours.childNodes[i];
+ switch (this.mState[i + this.mOffset]) {
+ case Components.interfaces.calIFreeBusyInterval.FREE:
+ hour.setAttribute("state", "free");
+ break;
+ case Components.interfaces.calIFreeBusyInterval.BUSY:
+ hour.setAttribute("state", "busy");
+ break;
+ case Components.interfaces.calIFreeBusyInterval.BUSY_TENTATIVE:
+ hour.setAttribute("state", "busy_tentative");
+ break;
+ case Components.interfaces.calIFreeBusyInterval.BUSY_UNAVAILABLE:
+ hour.setAttribute("state", "busy_unavailable");
+ break;
+ default:
+ hour.removeAttribute("state");
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="nextSlot">
+ <parameter name="aStartTime"/>
+ <parameter name="aEndTime"/>
+ <parameter name="allDay"/>
+ <body><![CDATA[
+ let newTime = aStartTime.clone();
+ let duration = aEndTime.subtractDate(aStartTime);
+ let newEndTime = newTime.clone();
+ newEndTime.addDuration(duration);
+
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ if (this.mEntries) {
+ for (let entry of this.mEntries) {
+ let rangeStart =
+ entry.interval.start.getInTimezone(kDefaultTimezone);
+ let rangeEnd =
+ entry.interval.end.getInTimezone(kDefaultTimezone);
+
+ let isZeroLength = !newTime.compare(newEndTime);
+ if ((isZeroLength &&
+ newTime.compare(rangeStart) >= 0 &&
+ newTime.compare(rangeEnd) < 0) ||
+ (!isZeroLength &&
+ newTime.compare(rangeEnd) < 0 &&
+ newEndTime.compare(rangeStart) > 0)) {
+ // Current range of event conflicts with another event.
+ // we need to find a new time for this event. A trivial approach
+ // is to set the new start-time of the event equal to the end-time
+ // of the conflict-range. All-day events need to be considered
+ // separately, in which case we skip to the next day.
+ newTime = rangeEnd.clone();
+ if (allDay) {
+ if (!((newTime.hour == 0) &&
+ (newTime.minute == 0) &&
+ (newTime.second == 0))) {
+ newTime.day++;
+ newTime.hour = 0;
+ newTime.minute = 0;
+ newTime.second = 0;
+ }
+ }
+ newEndTime = newTime.clone();
+ newEndTime.addDuration(duration);
+ }
+ }
+ }
+
+ return newTime;
+ ]]></body>
+ </method>
+
+ <method name="initTimeRange">
+ <body><![CDATA[
+ if (this.force24Hours) {
+ this.mStartHour = 0;
+ this.mEndHour = 24;
+ } else {
+ this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- ############################################################################# -->
+ <!-- 'freebusy-grid'-binding -->
+ <!-- ############################################################################# -->
+
+ <!-- id's are evil, use anonid -->
+ <binding id="freebusy-grid">
+ <content>
+ <xul:listbox anonid="listbox"
+ class="listbox-noborder"
+ seltype="multiple"
+ rows="-1"
+ disabled="true"
+ flex="1"
+ style="min-width: 50em; min-height: 30em">
+ <xul:listcols>
+ <xul:listcol anonid="grid-column" flex="1"/>
+ </xul:listcols>
+ <xul:listitem anonid="item"
+ class="addressingWidgetItem"
+ allowevents="true">
+ <xul:listcell class="addressingWidgetCell">
+ <xul:freebusy-row id="attendeeCol4#1"
+ anonid="grid"
+ dirty="true"
+ xbl:inherits="range"/>
+ </xul:listcell>
+ </xul:listitem>
+ </xul:listbox>
+ </content>
+
+ <implementation>
+ <field name="mContentHeight">0</field>
+ <field name="mRowHeight">0</field>
+ <field name="mNumColumns">0</field>
+ <field name="mMaxFreeBusy">0</field>
+ <field name="mPendingRequests">null</field>
+ <field name="mStartDate">null</field>
+ <field name="mEndDate">null</field>
+ <field name="mScrollOffset">0</field>
+ <field name="mRange">0</field>
+ <field name="mStartHour">0</field>
+ <field name="mEndHour">24</field>
+ <field name="mForce24Hours">false</field>
+ <field name="mZoomFactor">100</field>
+
+ <property name="zoomFactor">
+ <getter><![CDATA[
+ return this.mZoomFactor;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mZoomFactor = val;
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let freebusy = this.getFreeBusyElement(i);
+ freebusy.zoomFactor = this.mZoomFactor;
+ }
+ this.forceRefresh();
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="force24Hours">
+ <getter><![CDATA[
+ return this.mForce24Hours;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mForce24Hours = val;
+ this.initTimeRange();
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let freebusy = this.getFreeBusyElement(i);
+ freebusy.force24Hours = this.mForce24Hours;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="firstVisibleRow">
+ <getter><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ return listbox.getIndexOfFirstVisibleRow();
+ ]]></getter>
+ <setter><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ listbox.scrollToIndex(val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="ratio">
+ <setter><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let rowcount = listbox.getRowCount();
+ listbox.scrollToIndex(Math.floor(rowcount * val));
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.initTimeRange();
+
+ this.mRange = Number(this.getAttribute("range"));
+
+ this.mMaxFreeBusy = 0;
+ this.mPendingRequests = [];
+
+ window.addEventListener("load", this.onLoad.bind(this), true);
+ window.addEventListener("unload", this.onUnload.bind(this), true);
+ ]]></constructor>
+
+ <property name="startDate">
+ <getter><![CDATA[
+ return this.mStartDate;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mStartDate = val.clone();
+ this.mStartDate.makeImmutable();
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ this.getFreeBusyElement(i).startDate = val;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="endDate">
+ <getter><![CDATA[
+ return this.mEndDate;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mEndDate = val.clone();
+ this.mEndDate.makeImmutable();
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ this.getFreeBusyElement(i).endDate = val;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="documentSize">
+ <getter><![CDATA[
+ return this.getFreeBusyElement(1).documentSize;
+ ]]></getter>
+ </property>
+
+ <method name="onLoad">
+ <body><![CDATA[
+ this.onInitialize();
+ ]]></body>
+ </method>
+
+ <method name="onUnload">
+ <body><![CDATA[
+ // Cancel pending free/busy requests
+ for (let request of this.mPendingRequests) {
+ request.cancel(null);
+ }
+
+ this.mPendingRequests = [];
+ ]]></body>
+ </method>
+
+ <method name="onInitialize">
+ <body><![CDATA[
+ let args = window.arguments[0];
+ let startTime = args.startTime;
+ let endTime = args.endTime;
+
+ let kDefaultTimezone = calendarDefaultTimezone();
+ this.startDate = startTime.getInTimezone(kDefaultTimezone);
+ this.endDate = endTime.getInTimezone(kDefaultTimezone);
+
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "item");
+ this.appendNewRow(listbox, template, null);
+ template.remove();
+
+ this.updateFreeBusy();
+ ]]></body>
+ </method>
+
+ <method name="onChangeCalendar">
+ <parameter name="calendar"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <!-- appends a new empty row -->
+ <method name="appendNewRow">
+ <parameter name="aParentNode"/>
+ <parameter name="aTemplateNode"/>
+ <parameter name="aReplaceNode"/>
+ <body><![CDATA[
+ this.mMaxFreeBusy++;
+ let newNode = aTemplateNode.cloneNode(true);
+ if (aReplaceNode) {
+ aParentNode.replaceChild(newNode, aReplaceNode);
+ } else {
+ aParentNode.appendChild(newNode);
+ }
+
+ let grid =
+ document.getAnonymousElementByAttribute(
+ newNode, "anonid", "grid");
+ let rowNumber = this.mMaxFreeBusy;
+ if (rowNumber >= 0) {
+ grid.setAttribute("id", "attendeeCol4#" + rowNumber);
+ }
+
+ // Propagate start/enddate to the new row.
+ grid.startDate = this.mStartDate;
+ grid.endDate = this.mEndDate;
+
+ grid.force24Hours = this.mForce24Hours;
+ grid.zoomFactor = this.mZoomFactor;
+
+ // We always clone the first row. The problem is that the first row
+ // could be focused. When we clone that row, we end up with a cloned
+ // XUL textbox that has a focused attribute set. Therefore we think
+ // we're focused and don't properly refocus. The best solution to this
+ // would be to clone a template row that didn't really have any presentation,
+ // rather than using the real visible first row of the listbox.
+ // For now we'll just put in a hack that ensures the focused attribute
+ // is never copied when the node is cloned.
+ if (grid.getAttribute("focused") != "") {
+ grid.removeAttribute("focused");
+ }
+ ]]></body>
+ </method>
+
+ <property name="scroll">
+ <setter><![CDATA[
+ this.mScrollOffset = val;
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ this.getFreeBusyElement(i).scroll = val;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="onModify">
+ <parameter name="event"/>
+ <body><![CDATA[
+ // Add or remove rows depending on the number of items
+ // contained in the list passed as argument.
+ let list = event.details;
+ if (this.mMaxFreeBusy != list.length) {
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "item");
+ while (this.mMaxFreeBusy < list.length) {
+ let nextDummy = this.getNextDummyRow();
+ this.appendNewRow(listbox, template, nextDummy);
+ template =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "item");
+ }
+ while (this.mMaxFreeBusy > list.length) {
+ this.deleteRow(this.mMaxFreeBusy);
+ }
+ }
+
+ // Store the attributes in our grid rows.
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let freebusy = this.getFreeBusyElement(i);
+ freebusy.setAttribute("calid", list[i - 1].calid);
+ freebusy.removeAttribute("dirty");
+ if (list[i - 1].dirty) {
+ freebusy.setAttribute("dirty", "true");
+ }
+ }
+
+ // Align all rows
+ this.scroll = this.mScrollOffset;
+
+ this.updateFreeBusy();
+ ]]></body>
+ </method>
+
+ <!-- updateFreeBusy(), implementation of the core functionality of this binding -->
+ <method name="updateFreeBusy">
+ <body><![CDATA[
+ let fbService = getFreeBusyService();
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ // Retrieve the string from the appropriate row
+ let freebusy = this.getFreeBusyElement(i);
+ if (freebusy.hasAttribute("dirty")) {
+ freebusy.removeAttribute("dirty");
+ let calid = freebusy.getAttribute("calid");
+ if (calid && calid.length > 0) {
+ // Define the datetime range we would like to ask for.
+ let start = this.mStartDate.clone();
+ start.hour = 0;
+ start.minute = 0;
+ start.second = 0;
+ let end = start.clone();
+ end.day += this.mRange;
+ // Update with 'no data available' until response will be received
+ freebusy.onFreeBusy(null);
+ try {
+ let listener = new calFreeBusyListener(freebusy, this);
+ let request = fbService.getFreeBusyIntervals(calid,
+ start,
+ end,
+ Components.interfaces.calIFreeBusyInterval.BUSY_ALL,
+ listener);
+ if (request && request.isPending) {
+ this.mPendingRequests.push(request);
+ }
+ } catch (ex) {
+ Components.utils.reportError(ex);
+ }
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="nextSlot">
+ <body><![CDATA[
+ let startTime = this.mStartDate.clone();
+ let endTime = this.mEndDate.clone();
+
+ startTime.isDate = false;
+ endTime.isDate = false;
+
+ let allDay = this.mStartDate.isDate;
+ let step_in_minutes = Math.floor(60 * this.zoomFactor / 100);
+ if (allDay) {
+ step_in_minutes = 60 * 24;
+ endTime.day++;
+ }
+
+ let duration = endTime.subtractDate(startTime);
+
+ startTime.minute += step_in_minutes;
+
+ if (startTime.hour < this.mStartHour) {
+ startTime.hour = this.mStartHour;
+ startTime.minute = 0;
+ }
+
+ endTime = startTime.clone();
+ endTime.addDuration(duration);
+ if (endTime.hour > this.mEndHour) {
+ startTime.day++;
+ startTime.hour = this.mStartHour;
+ startTime.minute = 0;
+ endTime = startTime.clone();
+ endTime.addDuration(duration);
+ if (endTime.hour > this.mEndHour) {
+ return this.mStartDate.clone();
+ }
+ }
+
+ // Now iterate all freebusy-rows and ask each one
+ // if it wants to modify the suggested time slot.
+ // we keep iterating the rows until all of them
+ // are happy with it.
+ let recheck;
+ do {
+ recheck = false;
+
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let row = this.getFreeBusyElement(i);
+ let newTime = row.nextSlot(startTime, endTime, allDay);
+ if (newTime) {
+ if (newTime.compare(startTime) != 0) {
+ startTime = newTime;
+
+ if (startTime.hour < this.mStartHour) {
+ startTime.hour = this.mStartHour;
+ startTime.minute = 0;
+ }
+
+ endTime = startTime.clone();
+ endTime.addDuration(duration);
+
+ if (endTime.hour > this.mEndHour) {
+ startTime.day++;
+ startTime.hour = this.mStartHour;
+ startTime.minute = 0;
+ endTime = startTime.clone();
+ endTime.addDuration(duration);
+ }
+
+ recheck = true;
+ }
+ } else {
+ // A new slot could not be found
+ // and the given time was also invalid.
+ return this.mStartDate.clone();
+ }
+ }
+ } while (recheck);
+
+ // Return the unmodifed startdate of the item
+ // in case no possible match was found.
+ if (startTime.compare(this.mStartDate) == 0) {
+ return this.mStartDate.clone();
+ }
+
+ // Special case for allday events - if the original
+ // datetime was indeed a date we need to carry this
+ // state over to the calculated datetime.
+ if (this.mStartDate.isDate) {
+ startTime.isDate = true;
+ }
+
+ // In case the new starttime happens to be scheduled
+ // on a different day, we also need to update the
+ // complete freebusy informations and appropriate
+ // underlying arrays holding the informaion.
+ if (this.mStartDate.day != startTime.day) {
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let fbelem = this.getFreeBusyElement(i);
+ fbelem.setAttribute("dirty", "true");
+ }
+ this.updateFreeBusy();
+ }
+
+ // Return the new starttime of the item.
+ return startTime;
+ ]]></body>
+ </method>
+
+ <method name="forceRefresh">
+ <body><![CDATA[
+ for (let i = 1; i <= this.mMaxFreeBusy; i++) {
+ let row = this.getFreeBusyElement(i);
+ row.setAttribute("dirty", "true");
+ }
+ this.updateFreeBusy();
+ ]]></body>
+ </method>
+
+ <!-- This method returns the <xul:listitem> at row numer 'aRow' -->
+ <method name="getListItem">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ if (listbox && aRow > 0) {
+ let listitems = listbox.getElementsByTagNameNS("*", "listitem");
+ if (listitems && listitems.length >= aRow) {
+ return listitems[aRow - 1];
+ }
+ }
+ return 0;
+ ]]></body>
+ </method>
+
+ <method name="getFreeBusyElement">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "id",
+ "attendeeCol4#" + aRow);
+ ]]></body>
+ </method>
+
+ <method name="deleteRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ // Reset id's in order to not break the sequence
+ let max = this.mMaxFreeBusy;
+ this.removeRow(aRow);
+ let numberOfCols = this.numColumns;
+ for (let row = aRow + 1; row <= max; row++) {
+ for (let col = 1; col <= numberOfCols; col++) {
+ let colID = "attendeeCol" + col + "#" + row;
+ let elem = document.getAnonymousElementByAttribute(this, "id", colID);
+ if (elem) {
+ elem.setAttribute(
+ "id",
+ "attendeeCol" + (col) + "#" + (row - 1));
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="removeRow">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ this.getListItem(aRow).remove();
+ this.fitDummyRows();
+ this.mMaxFreeBusy--;
+ ]]></body>
+ </method>
+
+ <!-- gets the next row from the top down -->
+ <method name="getNextDummyRow">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let kids = listbox.childNodes;
+ for (let i = 0; i < kids.length; ++i) {
+ if (kids[i].hasAttribute("_isDummyRow")) {
+ return kids[i];
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="fitDummyRows">
+ <body><![CDATA[
+ setTimeout(() => {
+ this.calcContentHeight();
+ this.createOrRemoveDummyRows();
+ }, 0);
+ ]]></body>
+ </method>
+
+ <method name="calcContentHeight">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let items = listbox.getElementsByTagNameNS("*", "listitem");
+ this.mContentHeight = 0;
+ if (items.length > 0) {
+ let i = 0;
+ do {
+ this.mRowHeight = items[i].boxObject.height;
+ ++i;
+ } while (i < items.length && !this.mRowHeight);
+ this.mContentHeight = this.mRowHeight * items.length;
+ }
+ ]]></body>
+ </method>
+
+ <method name="createOrRemoveDummyRows">
+ <body><![CDATA[
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let listboxHeight = listbox.boxObject.height;
+
+ // Remove rows to remove scrollbar
+ let kids = listbox.childNodes;
+ for (let i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
+ if (kids[i].hasAttribute("_isDummyRow")) {
+ this.mContentHeight -= this.mRowHeight;
+ kids[i].remove();
+ }
+ }
+
+ // Add rows to fill space
+ if (this.mRowHeight) {
+ while (this.mContentHeight + this.mRowHeight < listboxHeight) {
+ this.createDummyItem(listbox);
+ this.mContentHeight += this.mRowHeight;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="createDummyCell">
+ <parameter name="aParent"/>
+ <body><![CDATA[
+ let cell = document.createElement("listcell");
+ cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+ if (aParent) {
+ aParent.appendChild(cell);
+ }
+ return cell;
+ ]]></body>
+ </method>
+
+ <method name="createDummyItem">
+ <parameter name="aParent"/>
+ <body><![CDATA[
+ let titem = document.createElement("listitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+ for (let i = this.numColumns; i > 0; i--) {
+ this.createDummyCell(titem);
+ }
+ if (aParent) {
+ aParent.appendChild(titem);
+ }
+ return titem;
+ ]]></body>
+ </method>
+
+ <property name="numColumns">
+ <getter><![CDATA[
+ if (!this.mNumColumns) {
+ let listbox =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "listbox");
+ let listCols = listbox.getElementsByTagNameNS("*", "listcol");
+ this.mNumColumns = listCols.length || 1;
+ }
+ return this.mNumColumns;
+ ]]></getter>
+ </property>
+
+ <method name="initTimeRange">
+ <body><![CDATA[
+ if (this.force24Hours) {
+ this.mStartHour = 0;
+ this.mEndHour = 24;
+ } else {
+ this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
+ this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-recurrence-preview.xml b/calendar/base/content/dialogs/calendar-event-dialog-recurrence-preview.xml
new file mode 100644
index 000000000..e5138544b
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-recurrence-preview.xml
@@ -0,0 +1,245 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="recurrence-preview" extends="xul:box">
+ <resources>
+ <stylesheet src="chrome://calendar/content/widgets/calendar-widget-bindings.css"/>
+ </resources>
+ <content>
+ <xul:box flex="1" style="overflow: hidden;">
+ <xul:grid flex="1" anonid="mainbox">
+ <xul:columns>
+ <xul:column anonid="column"/>
+ <xul:column flex="1"/>
+ </xul:columns>
+ <xul:rows>
+ <xul:row anonid="row">
+ <xul:minimonth anonid="minimonth" readonly="true"/>
+ <xul:minimonth anonid="minimonth" readonly="true"/>
+ <xul:minimonth anonid="minimonth" readonly="true"/>
+ <xul:spacer/>
+ </xul:row>
+ </xul:rows>
+ </xul:grid>
+ </xul:box>
+ </content>
+
+ <implementation>
+ <field name="mRecurrenceInfo">null</field>
+ <field name="mResizeHandler">null</field>
+ <field name="mDateTime">null</field>
+
+ <constructor><![CDATA[
+ this.mResizeHandler = this.onResize.bind(this);
+ window.addEventListener("resize", this.mResizeHandler, true);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ window.removeEventListener("resize", this.mResizeHandler, true);
+ ]]></destructor>
+
+ <property name="dateTime">
+ <setter><![CDATA[
+ this.mDateTime = val.clone();
+ return this.mDateTime;
+ ]]></setter>
+ <getter><![CDATA[
+ if (this.mDateTime == null) {
+ this.mDateTime = now();
+ }
+ return this.mDateTime;
+ ]]></getter>
+ </property>
+ <method name="onResize">
+ <body><![CDATA[
+ let minimonth =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "minimonth");
+
+ let row =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "row");
+ let rows = row.parentNode;
+
+ let contentWidth = minimonth.boxObject.width;
+ let containerWidth =
+ document.getAnonymousNodes(this)[0]
+ .boxObject.width;
+
+ // Now find out how much elements can be displayed.
+ // this is a simple division which always yields a positive integer value.
+ let numHorizontal =
+ (containerWidth -
+ (containerWidth % contentWidth)) /
+ contentWidth;
+
+ let contentHeight = minimonth.boxObject.height;
+ let containerHeight =
+ document.getAnonymousNodes(this)[0]
+ .boxObject.height;
+
+ // Now find out how much elements can be displayed.
+ // this is a simple division which always yields a positive integer value.
+ let numVertical =
+ (containerHeight -
+ (containerHeight % contentHeight)) /
+ contentHeight;
+ numVertical = Math.max(1, numVertical);
+
+ // Count the number of existing rows
+ let numRows = 0;
+ let rowIterator = row;
+ while (rowIterator) {
+ numRows++;
+ rowIterator = rowIterator.nextSibling;
+ }
+
+ // Adjust rows
+ while (numRows < numVertical) {
+ let newNode = row.cloneNode(true);
+ rows.appendChild(newNode);
+ numRows++;
+ }
+ while (numRows > numVertical) {
+ rows.firstChild.remove();
+ numRows--;
+ }
+
+ // Adjust columns in the grid
+ let column =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "column");
+ let columns = column.parentNode;
+ while ((columns.childNodes.length - 1) < numHorizontal) {
+ let newColumn = column.cloneNode(false);
+ columns.insertBefore(newColumn, column.nextSibling);
+ }
+ while ((columns.childNodes.length - 1) > numHorizontal) {
+ columns.firstChild.remove();
+ }
+
+ // Walk all rows and adjust column elements
+ row = document.getAnonymousElementByAttribute(
+ this, "anonid", "row");
+ while (row) {
+ let firstChild = row.firstChild;
+ while ((row.childNodes.length - 1) < numHorizontal) {
+ let newNode = firstChild.cloneNode(true);
+ firstChild.parentNode.insertBefore(newNode, firstChild);
+ }
+ while ((row.childNodes.length - 1) > numHorizontal) {
+ row.firstChild.remove();
+ }
+ row = row.nextSibling;
+ }
+
+ this.updateContent();
+ this.updatePreview(this.mRecurrenceInfo);
+ ]]></body>
+ </method>
+
+ <method name="updateContent">
+ <body><![CDATA[
+ let date = cal.dateTimeToJsDate(this.dateTime);
+ let row = document.getAnonymousElementByAttribute(
+ this, "anonid", "row");
+ while (row) {
+ let numChilds = row.childNodes.length - 1;
+ for (let i = 0; i < numChilds; i++) {
+ let minimonth = row.childNodes[i];
+ minimonth.showMonth(date);
+ date.setMonth(date.getMonth() + 1);
+ }
+ row = row.nextSibling;
+ }
+ ]]></body>
+ </method>
+
+ <method name="updatePreview">
+ <parameter name="aRecurrenceInfo"/>
+ <body><![CDATA[
+ this.mRecurrenceInfo = aRecurrenceInfo;
+ let start = this.dateTime.clone();
+ start.day = 1;
+ start.hour = 0;
+ start.minute = 0;
+ start.second = 0;
+ let end = start.clone();
+ end.month++;
+
+ // the 'minimonth' controls are arranged in a
+ // grid, sorted by rows first -> iterate the rows that may exist.
+ let row = document.getAnonymousElementByAttribute(this, "anonid", "row");
+ while (row) {
+ // now iterater all the child nodes of this row
+ // in order to visit each minimonth in turn.
+ let numChilds = row.childNodes.length - 1;
+ for (let i = 0; i < numChilds; i++) {
+ // we now have one of the minimonth controls while 'start'
+ // and 'end' are set to the interval this minimonth shows.
+ let minimonth = row.childNodes[i];
+ minimonth.showMonth(cal.dateTimeToJsDate(start));
+ if (aRecurrenceInfo) {
+ // retrieve an array of dates that represents all occurrences
+ // that fall into this time interval [start,end[.
+ // note: the following loop assumes that this array conains
+ // dates that are strictly monotonically increasing.
+ // should getOccurrenceDates() not enforce this assumption we
+ // need to fall back to some different algorithm.
+ let dates = aRecurrenceInfo.getOccurrenceDates(start, end, 0, {});
+
+ // now run throgh all days of this month and set the
+ // 'busy' attribute with respect to the occurrence array.
+ let index = 0;
+ let occurrence = null;
+ if (index < dates.length) {
+ occurrence =
+ dates[index++]
+ .getInTimezone(start.timezone);
+ }
+ let current = start.clone();
+ while (current.compare(end) < 0) {
+ let box = minimonth.getBoxForDate(current);
+ if (box) {
+ if (occurrence &&
+ occurrence.day == current.day &&
+ occurrence.month == current.month &&
+ occurrence.year == current.year) {
+ box.setAttribute("busy", 1);
+ if (index < dates.length) {
+ occurrence =
+ dates[index++]
+ .getInTimezone(start.timezone);
+ // take into account that the very next occurrence
+ // can happen at the same day as the previous one.
+ if (occurrence.day == current.day &&
+ occurrence.month == current.month &&
+ occurrence.year == current.year) {
+ continue;
+ }
+ } else {
+ occurrence = null;
+ }
+ } else {
+ box.removeAttribute("busy");
+ }
+ }
+ current.day++;
+ }
+ }
+ start.month++;
+ end.month++;
+ }
+ row = row.nextSibling;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js b/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
new file mode 100644
index 000000000..6be40c577
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
@@ -0,0 +1,804 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAccept, onCancel */
+
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gIsReadOnly = false;
+var gStartTime = null;
+var gEndTime = null;
+var gUntilDate = null;
+
+/**
+ * Sets up the recurrence dialog from the window arguments. Takes care of filling
+ * the dialog controls with the recurrence information for this window.
+ */
+function onLoad() {
+ changeWidgetsOrder();
+
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ let calendar = item.calendar;
+ let recinfo = args.recurrenceInfo;
+
+ gStartTime = args.startTime;
+ gEndTime = args.endTime;
+ let preview = document.getElementById("recurrence-preview");
+ preview.dateTime = gStartTime.getInTimezone(calendarDefaultTimezone());
+
+ onChangeCalendar(calendar);
+
+ // Set starting value for 'repeat until' rule and highlight the start date.
+ let repeatDate = cal.dateTimeToJsDate(gStartTime.getInTimezone(cal.floating()));
+ setElementValue("repeat-until-date", repeatDate);
+ document.getElementById("repeat-until-date").extraDate = repeatDate;
+
+ if (item.parentItem != item) {
+ item = item.parentItem;
+ }
+ let rule = null;
+ if (recinfo) {
+ // Split out rules and exceptions
+ try {
+ let rrules = splitRecurrenceRules(recinfo);
+ let rules = rrules[0];
+ // Deal with the rules
+ if (rules.length > 0) {
+ // We only handle 1 rule currently
+ rule = cal.wrapInstance(rules[0], Components.interfaces.calIRecurrenceRule);
+ }
+ } catch (ex) {
+ Components.utils.reportError(ex);
+ }
+ }
+ if (!rule) {
+ rule = createRecurrenceRule();
+ rule.type = "DAILY";
+ rule.interval = 1;
+ rule.count = -1;
+ }
+ initializeControls(rule);
+
+ // Update controls
+ updateRecurrenceDeck();
+
+ opener.setCursor("auto");
+ self.focus();
+}
+
+/**
+ * Initialize the dialog controls according to the passed rule
+ *
+ * @param rule The recurrence rule to parse.
+ */
+function initializeControls(rule) {
+ function getOrdinalAndWeekdayOfRule(aByDayRuleComponent) {
+ return {
+ ordinal: (aByDayRuleComponent - (aByDayRuleComponent % 8)) / 8,
+ weekday: Math.abs(aByDayRuleComponent % 8)
+ };
+ }
+
+ function setControlsForByMonthDay_YearlyRule(aDate, aByMonthDay) {
+ if (aByMonthDay == -1) {
+ // The last day of the month.
+ document.getElementById("yearly-group").selectedIndex = 1;
+ setElementValue("yearly-ordinal", -1);
+ setElementValue("yearly-weekday", -1);
+ } else {
+ if (aByMonthDay < -1) {
+ // The UI doesn't manage negative days apart from -1 but we can
+ // display in the controls the day from the start of the month.
+ aByMonthDay += aDate.endOfMonth.day + 1;
+ }
+ document.getElementById("yearly-group").selectedIndex = 0;
+ setElementValue("yearly-days", aByMonthDay);
+ }
+ }
+
+ function everyWeekDay(aByDay) {
+ // Checks if aByDay contains only values from 1 to 7 with any order.
+ let mask = aByDay.reduce((value, item) => value | (1 << item), 1);
+ return aByDay.length == 7 && mask == Math.pow(2, 8) - 1;
+ }
+
+ switch (rule.type) {
+ case "DAILY":
+ document.getElementById("period-list").selectedIndex = 0;
+ setElementValue("daily-days", rule.interval);
+ break;
+ case "WEEKLY":
+ setElementValue("weekly-weeks", rule.interval);
+ document.getElementById("period-list").selectedIndex = 1;
+ break;
+ case "MONTHLY":
+ setElementValue("monthly-interval", rule.interval);
+ document.getElementById("period-list").selectedIndex = 2;
+ break;
+ case "YEARLY":
+ setElementValue("yearly-interval", rule.interval);
+ document.getElementById("period-list").selectedIndex = 3;
+ break;
+ default:
+ document.getElementById("period-list").selectedIndex = 0;
+ dump("unable to handle your rule type!\n");
+ break;
+ }
+
+ let byDayRuleComponent = rule.getComponent("BYDAY", {});
+ let byMonthDayRuleComponent = rule.getComponent("BYMONTHDAY", {});
+ let byMonthRuleComponent = rule.getComponent("BYMONTH", {});
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let startDate = gStartTime.getInTimezone(kDefaultTimezone);
+
+ // "DAILY" ruletype
+ // byDayRuleComponents may have been set priorily by "MONTHLY"- ruletypes
+ // where they have a different context-
+ // that's why we also query the current rule-type
+ if (byDayRuleComponent.length == 0 || rule.type != "DAILY") {
+ document.getElementById("daily-group").selectedIndex = 0;
+ } else {
+ document.getElementById("daily-group").selectedIndex = 1;
+ }
+
+ // "WEEKLY" ruletype
+ if (byDayRuleComponent.length == 0 || rule.type != "WEEKLY") {
+ document.getElementById("daypicker-weekday").days = [startDate.weekday + 1];
+ } else {
+ document.getElementById("daypicker-weekday").days = byDayRuleComponent;
+ }
+
+ // "MONTHLY" ruletype
+ let ruleComponentsEmpty = (byDayRuleComponent.length == 0 &&
+ byMonthDayRuleComponent.length == 0);
+ if (ruleComponentsEmpty || rule.type != "MONTHLY") {
+ document.getElementById("monthly-group").selectedIndex = 1;
+ document.getElementById("monthly-days").days = [startDate.day];
+ let day = Math.floor((startDate.day - 1) / 7) + 1;
+ setElementValue("monthly-ordinal", day);
+ setElementValue("monthly-weekday", startDate.weekday + 1);
+ } else if (everyWeekDay(byDayRuleComponent)) {
+ // Every day of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ setElementValue("monthly-ordinal", 0);
+ setElementValue("monthly-weekday", -1);
+ } else if (byDayRuleComponent.length > 0) {
+ // One of the first five days or weekdays of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ let ruleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
+ setElementValue("monthly-ordinal", ruleInfo.ordinal);
+ setElementValue("monthly-weekday", ruleInfo.weekday);
+ } else if (byMonthDayRuleComponent.length == 1 && byMonthDayRuleComponent[0] == -1) {
+ // The last day of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ setElementValue("monthly-ordinal", byMonthDayRuleComponent[0]);
+ setElementValue("monthly-weekday", byMonthDayRuleComponent[0]);
+ } else if (byMonthDayRuleComponent.length > 0) {
+ document.getElementById("monthly-group").selectedIndex = 1;
+ document.getElementById("monthly-days").days = byMonthDayRuleComponent;
+ }
+
+ // "YEARLY" ruletype
+ if (byMonthRuleComponent.length == 0 || rule.type != "YEARLY") {
+ setElementValue("yearly-month-rule", startDate.month + 1);
+ setElementValue("yearly-month-ordinal", startDate.month + 1);
+ if (byMonthDayRuleComponent.length > 0) {
+ setControlsForByMonthDay_YearlyRule(startDate, byMonthDayRuleComponent[0]);
+ } else {
+ setElementValue("yearly-days", startDate.day);
+ let ordinalDay = Math.floor((startDate.day - 1) / 7) + 1;
+ setElementValue("yearly-ordinal", ordinalDay);
+ setElementValue("yearly-weekday", startDate.weekday + 1);
+ }
+ } else {
+ setElementValue("yearly-month-rule", byMonthRuleComponent[0]);
+ setElementValue("yearly-month-ordinal", byMonthRuleComponent[0]);
+ if (byMonthDayRuleComponent.length > 0) {
+ let date = startDate.clone();
+ date.month = byMonthRuleComponent[0] - 1;
+ setControlsForByMonthDay_YearlyRule(date, byMonthDayRuleComponent[0]);
+ } else if (byDayRuleComponent.length > 0) {
+ document.getElementById("yearly-group").selectedIndex = 1;
+ if (everyWeekDay(byDayRuleComponent)) {
+ // Every day of the month.
+ setElementValue("yearly-ordinal", 0);
+ setElementValue("yearly-weekday", -1);
+ } else {
+ let yearlyRuleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
+ setElementValue("yearly-ordinal", yearlyRuleInfo.ordinal);
+ setElementValue("yearly-weekday", yearlyRuleInfo.weekday);
+ }
+ } else if (byMonthRuleComponent.length > 0) {
+ document.getElementById("yearly-group").selectedIndex = 0;
+ setElementValue("yearly-days", startDate.day);
+ }
+ }
+
+ /* load up the duration of the event radiogroup */
+ if (rule.isByCount) {
+ if (rule.count == -1) {
+ setElementValue("recurrence-duration", "forever");
+ } else {
+ setElementValue("recurrence-duration", "ntimes");
+ setElementValue("repeat-ntimes-count", rule.count);
+ }
+ } else {
+ let untilDate = rule.untilDate;
+ if (untilDate) {
+ gUntilDate = untilDate.getInTimezone(gStartTime.timezone); // calIRecurrenceRule::untilDate is always UTC or floating
+ // Change the until date to start date if the rule has a forbidden
+ // value (earlier than the start date).
+ if (gUntilDate.compare(gStartTime) < 0) {
+ gUntilDate = gStartTime.clone();
+ }
+ let repeatDate = cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating()));
+ setElementValue("recurrence-duration", "until");
+ setElementValue("repeat-until-date", repeatDate);
+ } else {
+ setElementValue("recurrence-duration", "forever");
+ }
+ }
+}
+
+/**
+ * Save the recurrence information selected in the dialog back to the given
+ * item.
+ *
+ * @param item The item to save back to.
+ * @return The saved recurrence info.
+ */
+function onSave(item) {
+ // Always return 'null' if this item is an occurrence.
+ if (!item || item.parentItem != item) {
+ return null;
+ }
+
+ // This works, but if we ever support more complex recurrence,
+ // e.g. recurrence for Martians, then we're going to want to
+ // not clone and just recreate the recurrenceInfo each time.
+ // The reason is that the order of items (rules/dates/datesets)
+ // matters, so we can't always just append at the end. This
+ // code here always inserts a rule first, because all our
+ // exceptions should come afterward.
+ let deckNumber = Number(getElementValue("period-list"));
+
+ let args = window.arguments[0];
+ let recurrenceInfo = args.recurrenceInfo;
+ if (recurrenceInfo) {
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ if (rrules[0].length > 0) {
+ recurrenceInfo.deleteRecurrenceItem(rrules[0][0]);
+ }
+ recurrenceInfo.item = item;
+ } else {
+ recurrenceInfo = createRecurrenceInfo(item);
+ }
+
+ let recRule = createRecurrenceRule();
+ const ALL_WEEKDAYS = [2, 3, 4, 5, 6, 7, 1]; // The sequence MO,TU,WE,TH,FR,SA,SU.
+ switch (deckNumber) {
+ case 0: {
+ recRule.type = "DAILY";
+ let dailyGroup = document.getElementById("daily-group");
+ if (dailyGroup.selectedIndex == 0) {
+ let ndays = Math.max(1, Number(getElementValue("daily-days")));
+ recRule.interval = ndays;
+ } else {
+ recRule.interval = 1;
+ let onDays = [2, 3, 4, 5, 6];
+ recRule.setComponent("BYDAY", onDays.length, onDays);
+ }
+ break;
+ }
+ case 1: {
+ recRule.type = "WEEKLY";
+ let ndays = Number(getElementValue("weekly-weeks"));
+ recRule.interval = ndays;
+ let onDays = document.getElementById("daypicker-weekday").days;
+ if (onDays.length > 0) {
+ recRule.setComponent("BYDAY", onDays.length, onDays);
+ }
+ break;
+ }
+ case 2: {
+ recRule.type = "MONTHLY";
+ let monthInterval = Number(getElementValue("monthly-interval"));
+ recRule.interval = monthInterval;
+ let monthlyGroup = document.getElementById("monthly-group");
+ if (monthlyGroup.selectedIndex == 0) {
+ let monthlyOrdinal = Number(getElementValue("monthly-ordinal"));
+ let monthlyDOW = Number(getElementValue("monthly-weekday"));
+ if (monthlyDOW < 0) {
+ if (monthlyOrdinal == 0) {
+ // Monthly rule "Every day of the month".
+ recRule.setComponent("BYDAY", 7, ALL_WEEKDAYS);
+ } else {
+ // One of the first five days or the last day of the month.
+ recRule.setComponent("BYMONTHDAY", 1, [monthlyOrdinal]);
+ }
+ } else {
+ let sign = monthlyOrdinal < 0 ? -1 : 1;
+ let onDays = [(Math.abs(monthlyOrdinal) * 8 + monthlyDOW) * sign];
+ recRule.setComponent("BYDAY", onDays.length, onDays);
+ }
+ } else {
+ let monthlyDays = document.getElementById("monthly-days").days;
+ if (monthlyDays.length > 0) {
+ recRule.setComponent("BYMONTHDAY", monthlyDays.length, monthlyDays);
+ }
+ }
+ break;
+ }
+ case 3: {
+ recRule.type = "YEARLY";
+ let yearInterval = Number(getElementValue("yearly-interval"));
+ recRule.interval = yearInterval;
+ let yearlyGroup = document.getElementById("yearly-group");
+ if (yearlyGroup.selectedIndex == 0) {
+ let yearlyByMonth = [Number(getElementValue("yearly-month-ordinal"))];
+ recRule.setComponent("BYMONTH", yearlyByMonth.length, yearlyByMonth);
+ let yearlyByDay = [Number(getElementValue("yearly-days"))];
+ recRule.setComponent("BYMONTHDAY", yearlyByDay.length, yearlyByDay);
+ } else {
+ let yearlyByMonth = [Number(getElementValue("yearly-month-rule"))];
+ recRule.setComponent("BYMONTH", yearlyByMonth.length, yearlyByMonth);
+ let yearlyOrdinal = Number(getElementValue("yearly-ordinal"));
+ let yearlyDOW = Number(getElementValue("yearly-weekday"));
+ if (yearlyDOW < 0) {
+ if (yearlyOrdinal == 0) {
+ // Yearly rule "Every day of a month".
+ recRule.setComponent("BYDAY", 7, ALL_WEEKDAYS);
+ } else {
+ // One of the first five days or the last of a month.
+ recRule.setComponent("BYMONTHDAY", 1, [yearlyOrdinal]);
+ }
+ } else {
+ let sign = yearlyOrdinal < 0 ? -1 : 1;
+ let onDays = [(Math.abs(yearlyOrdinal) * 8 + yearlyDOW) * sign];
+ recRule.setComponent("BYDAY", onDays.length, onDays);
+ }
+ }
+ break;
+ }
+ }
+
+ // Figure out how long this event is supposed to last
+ switch (document.getElementById("recurrence-duration").selectedItem.value) {
+ case "forever": {
+ recRule.count = -1;
+ break;
+ }
+ case "ntimes": {
+ recRule.count = Math.max(1, getElementValue("repeat-ntimes-count"));
+ break;
+ }
+ case "until": {
+ let untilDate = cal.jsDateToDateTime(getElementValue("repeat-until-date"), gStartTime.timezone);
+ untilDate.isDate = gStartTime.isDate; // enforce same value type as DTSTART
+ if (!gStartTime.isDate) {
+ // correct UNTIL to exactly match start date's hour, minute, second:
+ untilDate.hour = gStartTime.hour;
+ untilDate.minute = gStartTime.minute;
+ untilDate.second = gStartTime.second;
+ }
+ recRule.untilDate = untilDate;
+ break;
+ }
+ }
+
+ if (recRule.interval < 1) {
+ return null;
+ }
+
+ recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
+ return recurrenceInfo;
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ args.onOk(onSave(item));
+ // Don't close the dialog if a warning must be showed.
+ return !checkUntilDate.warning;
+}
+
+/**
+ * Handler function to be called when the Cancel button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onCancel() {
+ // Don't show any warning if the dialog must be closed.
+ checkUntilDate.warning = false;
+ return true;
+}
+
+/**
+ * Handler function called when the calendar is changed (also for initial
+ * setup).
+ *
+ * XXX we don't change the calendar in this dialog, this function should be
+ * consolidated or renamed.
+ *
+ * @param calendar The calendar to use for setup.
+ */
+function onChangeCalendar(calendar) {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+
+ // Set 'gIsReadOnly' if the calendar is read-only
+ gIsReadOnly = false;
+ if (calendar && calendar.readOnly) {
+ gIsReadOnly = true;
+ }
+
+ // Disable or enable controls based on a set or rules
+ // - whether this item is a stand-alone item or an occurrence
+ // - whether or not this item is read-only
+ // - whether or not the state of the item allows recurrence rules
+ // - tasks without an entrydate are invalid
+ disableOrEnable(item);
+
+ updateRecurrenceControls();
+}
+
+/**
+ * Disable or enable certain controls based on the given item:
+ * Uses the following attribute:
+ *
+ * - disable-on-occurrence
+ * - disable-on-readonly
+ *
+ * A task without a start time is also considered readonly.
+ *
+ * @param item The item to check.
+ */
+function disableOrEnable(item) {
+ if (item.parentItem != item) {
+ disableRecurrenceFields("disable-on-occurrence");
+ } else if (gIsReadOnly) {
+ disableRecurrenceFields("disable-on-readonly");
+ } else if (isToDo(item) && !gStartTime) {
+ disableRecurrenceFields("disable-on-readonly");
+ } else {
+ enableRecurrenceFields("disable-on-readonly");
+ }
+}
+
+/**
+ * Disables all fields that have an attribute that matches the argument and is
+ * set to "true".
+ *
+ * @param aAttributeName The attribute to search for.
+ */
+function disableRecurrenceFields(aAttributeName) {
+ let disableElements = document.getElementsByAttribute(aAttributeName, "true");
+ for (let i = 0; i < disableElements.length; i++) {
+ disableElements[i].setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Enables all fields that have an attribute that matches the argument and is
+ * set to "true".
+ *
+ * @param aAttributeName The attribute to search for.
+ */
+function enableRecurrenceFields(aAttributeName) {
+ let enableElements = document.getElementsByAttribute(aAttributeName, "true");
+ for (let i = 0; i < enableElements.length; i++) {
+ enableElements[i].removeAttribute("disabled");
+ }
+}
+
+/**
+ * Split rules into negative and positive rules.
+ *
+ * XXX This function is duplicate from calendar-dialog-utils.js, which we may
+ * want to include in this dialog.
+ *
+ * @param recurrenceInfo An item's recurrence info to parse.
+ * @return An array with two elements: an array of positive
+ * rules and an array of negative rules.
+ */
+function splitRecurrenceRules(recurrenceInfo) {
+ let recItems = recurrenceInfo.getRecurrenceItems({});
+ let rules = [];
+ let exceptions = [];
+ for (let recItem of recItems) {
+ if (recItem.isNegative) {
+ exceptions.push(recItem);
+ } else {
+ rules.push(recItem);
+ }
+ }
+ return [rules, exceptions];
+}
+
+/**
+ * Handler function to update the period-deck when an item from the period-list
+ * is selected. Also updates the controls on that deck.
+ */
+function updateRecurrenceDeck() {
+ document.getElementById("period-deck")
+ .selectedIndex = Number(getElementValue("period-list"));
+ updateRecurrenceControls();
+}
+
+/**
+ * Updates the controls regarding ranged controls (i.e repeat forever, repeat
+ * until, repeat n times...)
+ */
+function updateRecurrenceRange() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item || gIsReadOnly) {
+ return;
+ }
+
+ let radioRangeForever =
+ document.getElementById("recurrence-range-forever");
+ let radioRangeFor =
+ document.getElementById("recurrence-range-for");
+ let radioRangeUntil =
+ document.getElementById("recurrence-range-until");
+ let rangeTimesCount =
+ document.getElementById("repeat-ntimes-count");
+ let rangeUntilDate =
+ document.getElementById("repeat-until-date");
+ let rangeAppointmentsLabel =
+ document.getElementById("repeat-appointments-label");
+
+ radioRangeForever.removeAttribute("disabled");
+ radioRangeFor.removeAttribute("disabled");
+ radioRangeUntil.removeAttribute("disabled");
+ rangeAppointmentsLabel.removeAttribute("disabled");
+
+ let durationSelection = document.getElementById("recurrence-duration")
+ .selectedItem.value;
+
+ if (durationSelection == "ntimes") {
+ rangeTimesCount.removeAttribute("disabled");
+ } else {
+ rangeTimesCount.setAttribute("disabled", "true");
+ }
+
+ if (durationSelection == "until") {
+ rangeUntilDate.removeAttribute("disabled");
+ } else {
+ rangeUntilDate.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Updates the recurrence preview calendars using the window's item.
+ */
+function updatePreview() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item) {
+ item = item.parentItem;
+ }
+
+ // TODO: We should better start the whole dialog with a newly cloned item
+ // and always pump changes immediately into it. This would eliminate the
+ // need to break the encapsulation, as we do it here. But we need the item
+ // to contain the startdate in order to calculate the recurrence preview.
+ item = item.clone();
+ let kDefaultTimezone = calendarDefaultTimezone();
+ if (isEvent(item)) {
+ let startDate = gStartTime.getInTimezone(kDefaultTimezone);
+ let endDate = gEndTime.getInTimezone(kDefaultTimezone);
+ if (startDate.isDate) {
+ endDate.day--;
+ }
+
+ item.startDate = startDate;
+ item.endDate = endDate;
+ }
+ if (isToDo(item)) {
+ let entryDate = gStartTime;
+ if (entryDate) {
+ entryDate = entryDate.getInTimezone(kDefaultTimezone);
+ } else {
+ item.recurrenceInfo = null;
+ }
+ item.entryDate = entryDate;
+ let dueDate = gEndTime;
+ if (dueDate) {
+ dueDate = dueDate.getInTimezone(kDefaultTimezone);
+ }
+ item.dueDate = dueDate;
+ }
+
+ let recInfo = onSave(item);
+ let preview = document.getElementById("recurrence-preview");
+ preview.updatePreview(recInfo);
+}
+
+/**
+ * Checks the until date just entered in the datepicker in order to avoid
+ * setting a date earlier than the start date.
+ * Restores the previous correct date, shows a warning and prevents to close the
+ * dialog when the user enters a wrong until date.
+ */
+function checkUntilDate() {
+ let untilDate = cal.jsDateToDateTime(getElementValue("repeat-until-date"), gStartTime.timezone);
+ let startDate = gStartTime.clone();
+ startDate.isDate = true;
+ if (untilDate.compare(startDate) < 0) {
+ let repeatDate = cal.dateTimeToJsDate((gUntilDate || gStartTime).getInTimezone(cal.floating()));
+ setElementValue("repeat-until-date", repeatDate);
+ checkUntilDate.warning = true;
+ let callback = function() {
+ // No warning when the dialog is being closed with the Cancel button.
+ if (!checkUntilDate.warning) {
+ return;
+ }
+ Services.prompt.alert(null, document.title,
+ cal.calGetString("calendar", "warningUntilDateBeforeStart"));
+ checkUntilDate.warning = false;
+ };
+ setTimeout(callback, 1);
+ } else {
+ gUntilDate = untilDate;
+ updateRecurrenceControls();
+ }
+}
+
+/**
+ * Update all recurrence controls on the dialog.
+ */
+function updateRecurrenceControls() {
+ updateRecurrencePattern();
+ updateRecurrenceRange();
+ updatePreview();
+}
+
+/**
+ * Disables/enables controls related to the recurrence pattern.
+ * the status of the controls depends on which period entry is selected
+ * and which form of pattern rule is selected.
+ */
+function updateRecurrencePattern() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item || gIsReadOnly) {
+ return;
+ }
+
+ switch (Number(getElementValue("period-list"))) {
+ // daily
+ case 0: {
+ let dailyGroup = document.getElementById("daily-group");
+ let dailyDays = document.getElementById("daily-days");
+ dailyDays.removeAttribute("disabled");
+ if (dailyGroup.selectedIndex == 1) {
+ dailyDays.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ // weekly
+ case 1: {
+ break;
+ }
+ // monthly
+ case 2: {
+ let monthlyGroup = document.getElementById("monthly-group");
+ let monthlyOrdinal = document.getElementById("monthly-ordinal");
+ let monthlyWeekday = document.getElementById("monthly-weekday");
+ let monthlyDays = document.getElementById("monthly-days");
+ monthlyOrdinal.removeAttribute("disabled");
+ monthlyWeekday.removeAttribute("disabled");
+ monthlyDays.removeAttribute("disabled");
+ if (monthlyGroup.selectedIndex == 0) {
+ monthlyDays.setAttribute("disabled", "true");
+ } else {
+ monthlyOrdinal.setAttribute("disabled", "true");
+ monthlyWeekday.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ // yearly
+ case 3: {
+ let yearlyGroup = document.getElementById("yearly-group");
+ let yearlyDays = document.getElementById("yearly-days");
+ let yearlyMonthOrdinal = document.getElementById("yearly-month-ordinal");
+ let yearlyPeriodOfMonthLabel = document.getElementById("yearly-period-of-month-label");
+ let yearlyOrdinal = document.getElementById("yearly-ordinal");
+ let yearlyWeekday = document.getElementById("yearly-weekday");
+ let yearlyMonthRule = document.getElementById("yearly-month-rule");
+ let yearlyPeriodOfLabel = document.getElementById("yearly-period-of-label");
+ yearlyDays.removeAttribute("disabled");
+ yearlyMonthOrdinal.removeAttribute("disabled");
+ yearlyOrdinal.removeAttribute("disabled");
+ yearlyWeekday.removeAttribute("disabled");
+ yearlyMonthRule.removeAttribute("disabled");
+ yearlyPeriodOfLabel.removeAttribute("disabled");
+ yearlyPeriodOfMonthLabel.removeAttribute("disabled");
+ if (yearlyGroup.selectedIndex == 0) {
+ yearlyOrdinal.setAttribute("disabled", "true");
+ yearlyWeekday.setAttribute("disabled", "true");
+ yearlyMonthRule.setAttribute("disabled", "true");
+ yearlyPeriodOfLabel.setAttribute("disabled", "true");
+ } else {
+ yearlyDays.setAttribute("disabled", "true");
+ yearlyMonthOrdinal.setAttribute("disabled", "true");
+ yearlyPeriodOfMonthLabel.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ }
+}
+
+/**
+ * This function changes the order for certain elements using a locale string.
+ * This is needed for some locales that expect a different wording order.
+ *
+ * @param aPropKey The locale property key to get the order from
+ * @param aPropParams An array of ids to be passed to the locale property.
+ * These should be the ids of the elements to change
+ * the order for.
+ */
+function changeOrderForElements(aPropKey, aPropParams) {
+ let localeOrder;
+ let parents = {};
+
+ for (let key in aPropParams) {
+ // Save original parents so that the nodes to reorder get appended to
+ // the correct parent nodes.
+ parents[key] = document.getElementById(aPropParams[key]).parentNode;
+ }
+
+ try {
+ localeOrder = calGetString("calendar-event-dialog",
+ aPropKey,
+ aPropParams);
+
+ localeOrder = localeOrder.split(" ");
+ } catch (ex) {
+ let msg = "The key " + aPropKey + " in calendar-event-dialog.prop" +
+ "erties has incorrect number of params. Expected " +
+ aPropParams.length + " params.";
+ Components.utils.reportError(msg + " " + ex);
+ return;
+ }
+
+ // Add elements in the right order, removing them from their old parent
+ for (let i = 0; i < aPropParams.length; i++) {
+ let newEl = document.getElementById(localeOrder[i]);
+ if (newEl) {
+ parents[i].appendChild(newEl.parentNode.removeChild(newEl));
+ } else {
+ cal.ERROR("Localization error, could not find node '" + localeOrder[i] + "'. Please have your localizer check the string '" + aPropKey + "'");
+ }
+ }
+}
+
+/**
+ * Change locale-specific widget order for Edit Recurrence window
+ */
+function changeWidgetsOrder() {
+ changeOrderForElements("monthlyOrder",
+ ["monthly-ordinal",
+ "monthly-weekday"]);
+ changeOrderForElements("yearlyOrder",
+ ["yearly-days",
+ "yearly-period-of-month-label",
+ "yearly-month-ordinal"]);
+ changeOrderForElements("yearlyOrder2",
+ ["yearly-ordinal",
+ "yearly-weekday",
+ "yearly-period-of-label",
+ "yearly-month-rule"]);
+}
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-recurrence.xul b/calendar/base/content/dialogs/calendar-event-dialog-recurrence.xul
new file mode 100644
index 000000000..ea8a942e0
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-recurrence.xul
@@ -0,0 +1,514 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % dialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ %dialogDTD;
+]>
+
+<dialog id="calendar-event-dialog-recurrence"
+ title="&recurrence.title.label;"
+ windowtype="Calendar:EventDialog:Recurrence"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-event-dialog-recurrence.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/>
+
+ <!-- recurrence pattern -->
+ <groupbox id="recurrence-pattern-groupbox">
+ <caption id="recurrence-pattern-caption"
+ label="&event.recurrence.pattern.label;"/>
+ <grid id="recurrence-pattern-grid">
+ <columns id="recurrence-pattern-columns">
+ <column id="recurrence-pattern-description-column"/>
+ <column id="recurrence-pattern-controls-column"/>
+ </columns>
+ <rows id="recurrence-pattern-rows">
+ <row id="recurrence-pattern-repeat-row" align="center">
+ <label value="&event.recurrence.occurs.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ control="period-list"/>
+ <menulist id="period-list"
+ oncommand="updateRecurrenceDeck();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="period-list-menupopup">
+ <menuitem id="period-list-day-menuitem"
+ label="&event.recurrence.day.label;"
+ value="0"/>
+ <menuitem id="period-list-week-menuitem"
+ label="&event.recurrence.week.label;"
+ value="1"/>
+ <menuitem id="period-list-month-menuitem"
+ label="&event.recurrence.month.label;"
+ value="2"/>
+ <menuitem id="period-list-year-menuitem"
+ label="&event.recurrence.year.label;"
+ value="3"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="recurrence-pattern-period-row" align="top">
+ <spacer/>
+ <deck id="period-deck" oncommand="updateRecurrenceControls();">
+
+ <!-- Daily -->
+ <box id="period-deck-daily-box"
+ orient="vertical"
+ align="top">
+ <radiogroup id="daily-group">
+ <box id="daily-period-every-box" orient="horizontal" align="center">
+ <radio id="daily-group-every-radio"
+ label="&event.recurrence.pattern.every.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ selected="true"/>
+ <textbox id="daily-days"
+ type="number"
+ value="1"
+ min="1"
+ max="0x7FFF"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="daily-group-every-units-label"
+ value="&repeat.units.days.both;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <spacer id="daily-group-spacer" flex="1"/>
+ </box>
+ <radio id="daily-group-weekday-radio"
+ label="&event.recurrence.pattern.every.weekday.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </radiogroup>
+ </box>
+ <!-- Weekly -->
+ <vbox id="period-deck-weekly-box">
+ <hbox id="weekly-period-every-box" align="center">
+ <label id="weekly-period-every-label"
+ value="&event.recurrence.pattern.weekly.every.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ control="weekly-weeks"/>
+ <textbox id="weekly-weeks"
+ type="number"
+ value="1"
+ min="1"
+ max="0x7FFF"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="weekly-period-units-label"
+ value="&repeat.units.weeks.both;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </hbox>
+ <hbox align="center">
+ <label id="weekly-period-on-label"
+ value="&event.recurrence.on.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ control="daypicker-weekday"/>
+ <daypicker-weekday id="daypicker-weekday"
+ flex="1"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ onselect="updateRecurrenceControls();"/>
+ </hbox>
+ </vbox>
+
+ <!-- Monthly -->
+ <vbox id="period-deck-monthly-box">
+ <hbox id="montly-period-every-box" align="center">
+ <label id="monthly-period-every-label"
+ value="&event.recurrence.pattern.monthly.every.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ control="monthly-interval"/>
+ <textbox id="monthly-interval"
+ type="number"
+ value="1"
+ min="1"
+ max="0x7FFF"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="monthly-period-units-label"
+ value="&repeat.units.months.both;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </hbox>
+ <radiogroup id="monthly-group">
+ <box id="monthly-period-relative-date-box"
+ orient="horizontal" align="center">
+ <radio id="montly-period-relative-date-radio"
+ selected="true"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <menulist id="monthly-ordinal"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="montly-ordinal-menupopup">
+ <menuitem id="monthly-ordinal-every-label"
+ label="&event.recurrence.monthly.every.label;"
+ value="0"/>
+ <menuitem id="monthly-ordinal-first-label"
+ label="&event.recurrence.monthly.first.label;"
+ value="1"/>
+ <menuitem id="monthly-ordinal-second-label"
+ label="&event.recurrence.monthly.second.label;"
+ value="2"/>
+ <menuitem id="monthly-ordinal-third-label"
+ label="&event.recurrence.monthly.third.label;"
+ value="3"/>
+ <menuitem id="monthly-ordinal-fourth-label"
+ label="&event.recurrence.monthly.fourth.label;"
+ value="4"/>
+ <menuitem id="monthly-ordinal-fifth-label"
+ label="&event.recurrence.monthly.fifth.label;"
+ value="5"/>
+ <menuitem id="monthly-ordinal-last-label"
+ label="&event.recurrence.monthly.last.label;"
+ value="-1"/>
+ </menupopup>
+ </menulist>
+ <menulist id="monthly-weekday"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="monthly-weekday-menupopup">
+ <menuitem id="monthly-weekday-1"
+ label="&event.recurrence.pattern.monthly.week.1.label;"
+ value="1"/>
+ <menuitem id="monthly-weekday-2"
+ label="&event.recurrence.pattern.monthly.week.2.label;"
+ value="2"/>
+ <menuitem id="monthly-weekday-3"
+ label="&event.recurrence.pattern.monthly.week.3.label;"
+ value="3"/>
+ <menuitem id="monthly-weekday-4"
+ label="&event.recurrence.pattern.monthly.week.4.label;"
+ value="4"/>
+ <menuitem id="monthly-weekday-5"
+ label="&event.recurrence.pattern.monthly.week.5.label;"
+ value="5"/>
+ <menuitem id="monthly-weekday-6"
+ label="&event.recurrence.pattern.monthly.week.6.label;"
+ value="6"/>
+ <menuitem id="monthly-weekday-7"
+ label="&event.recurrence.pattern.monthly.week.7.label;"
+ value="7"/>
+ <menuitem id="monthly-weekday-dayofmonth"
+ label="&event.recurrence.repeat.dayofmonth.label;"
+ value="-1"/>
+ </menupopup>
+ </menulist>
+ </box>
+ <box id="monthly-period-specific-date-box"
+ orient="horizontal"
+ align="center">
+ <radio id="montly-period-specific-date-radio"
+ label="&event.recurrence.repeat.recur.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <daypicker-monthday id="monthly-days"
+ onselect="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </box>
+ </radiogroup>
+ </vbox>
+
+ <!-- Yearly -->
+ <box id="period-deck-yearly-box"
+ orient="vertical"
+ align="top">
+ <hbox id="yearly-period-every-box" align="center">
+ <label id="yearly-period-every-label"
+ value="&event.recurrence.every.label;"
+ control="yearly-interval"/>
+ <textbox id="yearly-interval"
+ type="number"
+ value="1"
+ min="1"
+ max="0x7FFF"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="yearly-period-units-label" value="&repeat.units.years.both;"/>
+ </hbox>
+ <radiogroup id="yearly-group">
+ <grid id="yearly-period-grid">
+ <columns id="yearly-period-columns">
+ <column id="yearly-period-radio-column"/>
+ <column id="yearly-period-controls-column"/>
+ </columns>
+ <rows id="yearly-period-rows">
+ <row id="yearly-period-absolute-row" align="center">
+ <radio id="yearly-period-absolute-radio"
+ label="&event.recurrence.pattern.yearly.every.month.label;"
+ selected="true"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <box id="yearly-period-absolute-controls"
+ orient="horizontal"
+ align="center">
+ <textbox id="yearly-days"
+ type="number"
+ value="1"
+ min="1"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="yearly-period-of-month-label"
+ value="&event.recurrence.pattern.yearly.of.label;"
+ control="yearly-month-ordinal"/>
+ <menulist id="yearly-month-ordinal"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="yearly-month-ordinal-menupopup">
+ <menuitem id="yearly-month-ordinal-1"
+ label="&event.recurrence.pattern.yearly.month.1.label;"
+ value="1"/>
+ <menuitem id="yearly-month-ordinal-2"
+ label="&event.recurrence.pattern.yearly.month.2.label;"
+ value="2"/>
+ <menuitem id="yearly-month-ordinal-3"
+ label="&event.recurrence.pattern.yearly.month.3.label;"
+ value="3"/>
+ <menuitem id="yearly-month-ordinal-4"
+ label="&event.recurrence.pattern.yearly.month.4.label;"
+ value="4"/>
+ <menuitem id="yearly-month-ordinal-5"
+ label="&event.recurrence.pattern.yearly.month.5.label;"
+ value="5"/>
+ <menuitem id="yearly-month-ordinal-6"
+ label="&event.recurrence.pattern.yearly.month.6.label;"
+ value="6"/>
+ <menuitem id="yearly-month-ordinal-7"
+ label="&event.recurrence.pattern.yearly.month.7.label;"
+ value="7"/>
+ <menuitem id="yearly-month-ordinal-8"
+ label="&event.recurrence.pattern.yearly.month.8.label;"
+ value="8"/>
+ <menuitem id="yearly-month-ordinal-9"
+ label="&event.recurrence.pattern.yearly.month.9.label;"
+ value="9"/>
+ <menuitem id="yearly-month-ordinal-10"
+ label="&event.recurrence.pattern.yearly.month.10.label;"
+ value="10"/>
+ <menuitem id="yearly-month-ordinal-11"
+ label="&event.recurrence.pattern.yearly.month.11.label;"
+ value="11"/>
+ <menuitem id="yearly-month-ordinal-12"
+ label="&event.recurrence.pattern.yearly.month.12.label;"
+ value="12"/>
+ </menupopup>
+ </menulist>
+ </box>
+ </row>
+ <row id="yearly-period-relative-row" align="center">
+ <radio id="yearly-period-relative-radio"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <box id="yearly-period-relative-controls"
+ orient="horizontal"
+ align="center">
+ <menulist id="yearly-ordinal"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="yearly-ordinal-menupopup">
+ <menuitem id="yearly-ordinal-every"
+ label="&event.recurrence.yearly.every.label;"
+ value="0"/>
+ <menuitem id="yearly-ordinal-first"
+ label="&event.recurrence.yearly.first.label;"
+ value="1"/>
+ <menuitem id="yearly-ordinal-second"
+ label="&event.recurrence.yearly.second.label;"
+ value="2"/>
+ <menuitem id="yearly-ordinal-third"
+ label="&event.recurrence.yearly.third.label;"
+ value="3"/>
+ <menuitem id="yearly-ordinal-fourth"
+ label="&event.recurrence.yearly.fourth.label;"
+ value="4"/>
+ <menuitem id="yearly-ordinal-fifth"
+ label="&event.recurrence.yearly.fifth.label;"
+ value="5"/>
+ <menuitem id="yearly-ordinal-last"
+ label="&event.recurrence.yearly.last.label;"
+ value="-1"/>
+ </menupopup>
+ </menulist>
+ <menulist id="yearly-weekday"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="yearly-weekday-menupopup">
+ <menuitem id="yearly-weekday-1"
+ label="&event.recurrence.pattern.yearly.week.1.label;"
+ value="1"/>
+ <menuitem id="yearly-weekday-2"
+ label="&event.recurrence.pattern.yearly.week.2.label;"
+ value="2"/>
+ <menuitem id="yearly-weekday-3"
+ label="&event.recurrence.pattern.yearly.week.3.label;"
+ value="3"/>
+ <menuitem id="yearly-weekday-4"
+ label="&event.recurrence.pattern.yearly.week.4.label;"
+ value="4"/>
+ <menuitem id="yearly-weekday-5"
+ label="&event.recurrence.pattern.yearly.week.5.label;"
+ value="5"/>
+ <menuitem id="yearly-weekday-6"
+ label="&event.recurrence.pattern.yearly.week.6.label;"
+ value="6"/>
+ <menuitem id="yearly-weekday-7"
+ label="&event.recurrence.pattern.yearly.week.7.label;"
+ value="7"/>
+ <menuitem id="yearly-weekday--1"
+ label="&event.recurrence.pattern.yearly.day.label;"
+ value="-1"/>
+ </menupopup>
+ </menulist>
+ </box>
+ </row>
+ <row id="yearly-period-monthname-row" align="center">
+ <label id="yearly-period-of-label"
+ value="&event.recurrence.of.label;"
+ control="yearly-month-rule"/>
+ <menulist id="yearly-month-rule"
+ disable-on-readonly="true"
+ disable-on-occurrence="true">
+ <menupopup id="yearly-month-rule-menupopup">
+ <menuitem id="yearly-month-rule-1"
+ label="&event.recurrence.pattern.yearly.month2.1.label;"
+ value="1"/>
+ <menuitem id="yearly-month-rule-2"
+ label="&event.recurrence.pattern.yearly.month2.2.label;"
+ value="2"/>
+ <menuitem id="yearly-month-rule-3"
+ label="&event.recurrence.pattern.yearly.month2.3.label;"
+ value="3"/>
+ <menuitem id="yearly-month-rule-4"
+ label="&event.recurrence.pattern.yearly.month2.4.label;"
+ value="4"/>
+ <menuitem id="yearly-month-rule-5"
+ label="&event.recurrence.pattern.yearly.month2.5.label;"
+ value="5"/>
+ <menuitem id="yearly-month-rule-6"
+ label="&event.recurrence.pattern.yearly.month2.6.label;"
+ value="6"/>
+ <menuitem id="yearly-month-rule-7"
+ label="&event.recurrence.pattern.yearly.month2.7.label;"
+ value="7"/>
+ <menuitem id="yearly-month-rule-8"
+ label="&event.recurrence.pattern.yearly.month2.8.label;"
+ value="8"/>
+ <menuitem id="yearly-month-rule-9"
+ label="&event.recurrence.pattern.yearly.month2.9.label;"
+ value="9"/>
+ <menuitem id="yearly-month-rule-10"
+ label="&event.recurrence.pattern.yearly.month2.10.label;"
+ value="10"/>
+ <menuitem id="yearly-month-rule-11"
+ label="&event.recurrence.pattern.yearly.month2.11.label;"
+ value="11"/>
+ <menuitem id="yearly-month-rule-12"
+ label="&event.recurrence.pattern.yearly.month2.12.label;"
+ value="12"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </radiogroup>
+ </box>
+ </deck>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <!-- range of recurrence -->
+ <groupbox id="recurrence-range-groupbox">
+ <caption id="recurrence-range-caption"
+ label="&event.recurrence.range.label;"/>
+ <vbox>
+ <radiogroup id="recurrence-duration"
+ oncommand="updateRecurrenceControls()">
+ <radio id="recurrence-range-forever"
+ label="&event.recurrence.forever.label;"
+ value="forever"
+ selected="true"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <box id="recurrence-range-count-box"
+ orient="horizontal"
+ align="center">
+ <radio id="recurrence-range-for"
+ label="&event.recurrence.repeat.for.label;"
+ value="ntimes"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <textbox id="repeat-ntimes-count"
+ type="number"
+ value="5"
+ min="1"
+ max="0x7FFF"
+ size="3"
+ onkeyup="updateRecurrenceControls();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ <label id="repeat-appointments-label"
+ value="&event.recurrence.appointments.label;"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </box>
+ <box id="recurrence-range-until-box"
+ orient="horizontal"
+ align="center">
+ <radio id="recurrence-range-until"
+ label="&event.repeat.until.label;"
+ value="until"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"
+ control="repeat-until-date"/>
+ <datepicker id="repeat-until-date"
+ onchange="checkUntilDate();"
+ disable-on-readonly="true"
+ disable-on-occurrence="true"/>
+ </box>
+ </radiogroup>
+ </vbox>
+ </groupbox>
+
+ <!-- preview -->
+ <groupbox id="preview-border" flex="1">
+ <label id="recurrence-preview-label"
+ value="&event.recurrence.preview.label;"
+ control="recurrence-preview"/>
+ <recurrence-preview id="recurrence-preview" flex="1"/>
+ </groupbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-reminder.js b/calendar/base/content/dialogs/calendar-event-dialog-reminder.js
new file mode 100644
index 000000000..4f22889b9
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-reminder.js
@@ -0,0 +1,446 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onReminderSelected, updateReminder, onNewReminder,
+ * onRemoveReminder, onAccept, onCancel
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var allowedActionsMap = {};
+
+/**
+ * Sets up the reminder dialog.
+ */
+function onLoad() {
+ let calendar = window.arguments[0].calendar;
+
+ // Make sure the origin menulist uses the right labels, depending on if the
+ // dialog is showing an event or task.
+ function _sn(x) { return cal.calGetString("calendar-alarms", getItemBundleStringName(x)); }
+
+ setElementValue("reminder-before-start-menuitem",
+ _sn("reminderCustomOriginBeginBefore"),
+ "label");
+
+ setElementValue("reminder-after-start-menuitem",
+ _sn("reminderCustomOriginBeginAfter"),
+ "label");
+
+ setElementValue("reminder-before-end-menuitem",
+ _sn("reminderCustomOriginEndBefore"),
+ "label");
+
+ setElementValue("reminder-after-end-menuitem",
+ _sn("reminderCustomOriginEndAfter"),
+ "label");
+
+
+ // Set up the action map
+ let supportedActions = calendar.getProperty("capabilities.alarms.actionValues") ||
+ ["DISPLAY"]; // TODO email support, "EMAIL"
+ for (let action of supportedActions) {
+ allowedActionsMap[action] = true;
+ }
+
+ // Hide all actions that are not supported by this provider
+ let firstAvailableItem;
+ let actionNodes = document.getElementById("reminder-actions-menupopup").childNodes;
+ for (let actionNode of actionNodes) {
+ let shouldHide = !(actionNode.value in allowedActionsMap) ||
+ (actionNode.hasAttribute("provider") &&
+ actionNode.getAttribute("provider") != calendar.type);
+ setElementValue(actionNode, shouldHide && "true", "hidden");
+ if (!firstAvailableItem && !shouldHide) {
+ firstAvailableItem = actionNode;
+ }
+ }
+
+ // Correct the selected item on the supported actions list. This will be
+ // changed when reminders are loaded, but in case there are none we need to
+ // provide a sensible default.
+ if (firstAvailableItem) {
+ document.getElementById("reminder-actions-menulist").selectedItem = firstAvailableItem;
+ }
+
+ loadReminders();
+ opener.setCursor("auto");
+}
+
+/**
+ * Load Reminders from the window's arguments and set up dialog controls to
+ * their initial values.
+ */
+function loadReminders() {
+ let args = window.arguments[0];
+ let listbox = document.getElementById("reminder-listbox");
+ let reminders = args.reminders || args.item.getAlarms({});
+
+ // This dialog should not be shown if the calendar doesn't support alarms at
+ // all, so the case of maxCount = 0 breaking this logic doesn't apply.
+ let maxReminders = args.calendar.getProperty("capabilities.alarms.maxCount");
+ let count = Math.min(reminders.length, maxReminders || reminders.length);
+ for (let i = 0; i < count; i++) {
+ if (reminders[i].action in allowedActionsMap) {
+ // Set up the listitem and add it to the listbox, but only if the
+ // action is actually supported by the calendar.
+ listbox.appendChild(setupListItem(null, reminders[i].clone(), args.item));
+ }
+ }
+
+ // Set up a default absolute date. This will be overridden if the selected
+ // alarm is absolute.
+ let absDate = document.getElementById("reminder-absolute-date");
+ absDate.value = cal.dateTimeToJsDate(getDefaultStartDate());
+
+ if (listbox.childNodes.length) {
+ // We have reminders, select the first by default. For some reason,
+ // setting the selected index in a load handler makes the selection
+ // break for the set item, therefore we need a setTimeout.
+ setupMaxReminders();
+ setTimeout(() => { listbox.selectedIndex = 0; }, 0);
+ } else {
+ // Make sure the fields are disabled if we have no alarms
+ setupRadioEnabledState(true);
+ }
+}
+
+/**
+ * Sets up the enabled state of the reminder details controls. Used when
+ * switching between absolute and relative alarms to disable and enable the
+ * needed controls.
+ *
+ * @param aDisableAll Disable all relation controls. Used when no alarms
+ * are added yet.
+ */
+function setupRadioEnabledState(aDisableAll) {
+ let relationItem = document.getElementById("reminder-relation-radiogroup").selectedItem;
+ let relativeDisabled, absoluteDisabled;
+
+ // Note that the mix of string/boolean here is not a mistake.
+ // setElementValue removes the attribute from the node if the second
+ // parameter is === false, otherwise sets the attribute value to the given
+ // string (i.e "true").
+ if (aDisableAll) {
+ relativeDisabled = "true";
+ absoluteDisabled = "true";
+ } else if (relationItem) {
+ // This is not a mistake, when this function is called from onselect,
+ // the value has not been set.
+ relativeDisabled = (relationItem.value == "absolute") && "true";
+ absoluteDisabled = (relationItem.value == "relative") && "true";
+ } else {
+ relativeDisabled = false;
+ absoluteDisabled = false;
+ }
+
+ setElementValue("reminder-length", relativeDisabled, "disabled");
+ setElementValue("reminder-unit", relativeDisabled, "disabled");
+ setElementValue("reminder-relation-origin", relativeDisabled, "disabled");
+
+ setElementValue("reminder-absolute-date", absoluteDisabled, "disabled");
+
+ let disableAll = (aDisableAll ? "true" : false);
+ setElementValue("reminder-relative-radio", disableAll, "disabled");
+ setElementValue("reminder-absolute-radio", disableAll, "disabled");
+ setElementValue("reminder-actions-menulist", disableAll, "disabled");
+}
+
+/**
+ * Sets up the max reminders notification. Shows or hides the notification
+ * depending on if the max reminders limit has been hit or not.
+ */
+function setupMaxReminders() {
+ let args = window.arguments[0];
+ let listbox = document.getElementById("reminder-listbox");
+ let notificationbox = document.getElementById("reminder-notifications");
+ let maxReminders = args.calendar.getProperty("capabilities.alarms.maxCount");
+
+ // != null is needed here to ensure cond to be true/false, instead of
+ // true/null. The former is needed for setElementValue.
+ let cond = (maxReminders != null && listbox.childNodes.length >= maxReminders);
+
+ // If we hit the maximum number of reminders, show the error box and
+ // disable the new button.
+ setElementValue("reminder-new-button", cond && "true", "disabled");
+
+ if (!setupMaxReminders.notification) {
+ let notification = createXULElement("notification");
+ let localeErrorString =
+ calGetString("calendar-alarms",
+ getItemBundleStringName("reminderErrorMaxCountReached"),
+ [maxReminders]);
+ let pluralErrorLabel = PluralForm.get(maxReminders, localeErrorString)
+ .replace("#1", maxReminders);
+
+ notification.setAttribute("label", pluralErrorLabel);
+ notification.setAttribute("type", "warning");
+ notification.setAttribute("hideclose", "true");
+ setupMaxReminders.notification = notification;
+ }
+
+ if (cond) {
+ notificationbox.appendChild(setupMaxReminders.notification);
+ } else {
+ try {
+ notificationbox.removeNotification(setupMaxReminders.notification);
+ } catch (e) {
+ // It's only ok to swallow this if the notification element hasn't been
+ // added. Then the call will throw a DOM NOT_FOUND_ERR.
+ if (e.code != e.NOT_FOUND_ERR) {
+ throw e;
+ }
+ }
+ }
+}
+
+/**
+ * Sets up a reminder listitem for the list of reminders applied to this item.
+ *
+ * @param aListItem (optional) A reference listitem to set up. If not
+ * passed, a new listitem will be created.
+ * @param aReminder The calIAlarm to display in this listitem
+ * @param aItem The item the alarm is set up on.
+ * @return The XUL listitem node showing the passed reminder.
+ */
+function setupListItem(aListItem, aReminder, aItem) {
+ let listitem = aListItem || createXULElement("listitem");
+
+ // Create a random id to be used for accessibility
+ let reminderId = cal.getUUID();
+ let ariaLabel = "reminder-action-" + aReminder.action + " " + reminderId;
+
+ listitem.reminder = aReminder;
+ listitem.setAttribute("id", reminderId);
+ listitem.setAttribute("label", aReminder.toString(aItem));
+ listitem.setAttribute("aria-labelledby", ariaLabel);
+ listitem.setAttribute("class", "reminder-icon listitem-iconic");
+ listitem.setAttribute("value", aReminder.action);
+ return listitem;
+}
+
+/**
+ * Handler function to be called when a reminder is selected in the listbox.
+ * Sets up remaining controls to show the selected alarm.
+ */
+function onReminderSelected() {
+ let length = document.getElementById("reminder-length");
+ let unit = document.getElementById("reminder-unit");
+ let relationOrigin = document.getElementById("reminder-relation-origin");
+ let absDate = document.getElementById("reminder-absolute-date");
+ let actionType = document.getElementById("reminder-actions-menulist");
+ let relationType = document.getElementById("reminder-relation-radiogroup");
+
+ let listbox = document.getElementById("reminder-listbox");
+ let listitem = listbox.selectedItem;
+
+ if (listitem) {
+ let reminder = listitem.reminder;
+
+ // Action
+ actionType.value = reminder.action;
+
+ // Absolute/relative things
+ if (reminder.related == Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE) {
+ relationType.value = "absolute";
+
+ // Date
+ absDate.value = cal.dateTimeToJsDate(reminder.alarmDate || cal.getDefaultStartDate());
+ } else {
+ relationType.value = "relative";
+
+ // Unit and length
+ let alarmlen = Math.abs(reminder.offset.inSeconds / 60);
+ if (alarmlen % 1440 == 0) {
+ unit.value = "days";
+ length.value = alarmlen / 1440;
+ } else if (alarmlen % 60 == 0) {
+ unit.value = "hours";
+ length.value = alarmlen / 60;
+ } else {
+ unit.value = "minutes";
+ length.value = alarmlen;
+ }
+
+ // Relation
+ let relation = (reminder.offset.isNegative ? "before" : "after");
+
+ // Origin
+ let origin;
+ if (reminder.related == Components.interfaces.calIAlarm.ALARM_RELATED_START) {
+ origin = "START";
+ } else if (reminder.related == Components.interfaces.calIAlarm.ALARM_RELATED_END) {
+ origin = "END";
+ }
+
+ relationOrigin.value = [relation, origin].join("-");
+ }
+ } else {
+ // no list item is selected, disable elements
+ setupRadioEnabledState(true);
+ }
+}
+
+/**
+ * Handler function to be called when an aspect of the alarm has been changed
+ * using the dialog controls.
+ *
+ * @param event The DOM event caused by the change.
+ */
+function updateReminder(event) {
+ if (event.explicitOriginalTarget.localName == "listitem" ||
+ event.explicitOriginalTarget.id == "reminder-remove-button" ||
+ !document.commandDispatcher.focusedElement) {
+ // Do not set things if the select came from selecting or removing an
+ // alarm from the list, or from setting when the dialog initially loaded.
+ // XXX Quite fragile hack since radio/radiogroup doesn't have the
+ // supressOnSelect stuff.
+ return;
+ }
+ let listbox = document.getElementById("reminder-listbox");
+ let relationItem = document.getElementById("reminder-relation-radiogroup").selectedItem;
+ let listitem = listbox.selectedItem;
+ if (!listitem || !relationItem) {
+ return;
+ }
+ let reminder = listitem.reminder;
+ let length = document.getElementById("reminder-length");
+ let unit = document.getElementById("reminder-unit");
+ let relationOrigin = document.getElementById("reminder-relation-origin");
+ let [relation, origin] = relationOrigin.value.split("-");
+ let absDate = document.getElementById("reminder-absolute-date");
+ let action = document.getElementById("reminder-actions-menulist").selectedItem.value;
+
+ // Action
+ reminder.action = action;
+
+ if (relationItem.value == "relative") {
+ if (origin == "START") {
+ reminder.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+ } else if (origin == "END") {
+ reminder.related = Components.interfaces.calIAlarm.ALARM_RELATED_END;
+ }
+
+ // Set up offset, taking units and before/after into account
+ let offset = cal.createDuration();
+ offset[unit.value] = length.value;
+ offset.normalize();
+ offset.isNegative = (relation == "before");
+ reminder.offset = offset;
+ } else if (relationItem.value == "absolute") {
+ reminder.related = Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE;
+
+ if (absDate.value) {
+ reminder.alarmDate = cal.jsDateToDateTime(absDate.value,
+ window.arguments[0].timezone);
+ } else {
+ reminder.alarmDate = null;
+ }
+ }
+
+ setupListItem(listitem, reminder, window.arguments[0].item);
+}
+
+/**
+ * Gets the locale stringname that is dependant on the item type. This function
+ * appends the item type, i.e |aPrefix + "Event"|.
+ *
+ * @param aPrefix The prefix to prepend to the item type
+ * @return The full string name.
+ */
+function getItemBundleStringName(aPrefix) {
+ if (isEvent(window.arguments[0].item)) {
+ return aPrefix + "Event";
+ } else {
+ return aPrefix + "Task";
+ }
+}
+
+/**
+ * Handler function to be called when the "new" button is pressed, to create a
+ * new reminder item.
+ */
+function onNewReminder() {
+ let itemType = (isEvent(window.arguments[0].item) ? "event" : "todo");
+ let listbox = document.getElementById("reminder-listbox");
+
+ let reminder = cal.createAlarm();
+ let alarmlen = Preferences.get("calendar.alarms." + itemType + "alarmlen", 15);
+
+ // Default is a relative DISPLAY alarm, |alarmlen| minutes before the event.
+ // If DISPLAY is not supported by the provider, then pick the provider's
+ // first alarm type.
+ let offset = cal.createDuration();
+ offset.minutes = alarmlen;
+ offset.normalize();
+ offset.isNegative = true;
+ reminder.related = reminder.ALARM_RELATED_START;
+ reminder.offset = offset;
+ if ("DISPLAY" in allowedActionsMap) {
+ reminder.action = "DISPLAY";
+ } else {
+ let calendar = window.arguments[0].calendar;
+ let actions = calendar.getProperty("capabilities.alarms.actionValues") || [];
+ reminder.action = actions[0];
+ }
+
+ // Set up the listbox
+ let listitem = setupListItem(null, reminder, window.arguments[0].item);
+ listbox.appendChild(listitem);
+ listbox.selectItem(listitem);
+
+ // Since we've added an item, its safe to always enable the button
+ enableElement("reminder-remove-button");
+
+ // Set up the enabled state and max reminders
+ setupRadioEnabledState();
+ setupMaxReminders();
+}
+
+/**
+ * Handler function to be called when the "remove" button is pressed to remove
+ * the selected reminder item and advance the selection.
+ */
+function onRemoveReminder() {
+ let listbox = document.getElementById("reminder-listbox");
+ let listitem = listbox.selectedItem;
+ let newSelection = listitem ? listitem.nextSibling || listitem.previousSibling
+ : null;
+
+ listbox.clearSelection();
+ listitem.remove();
+ listbox.selectItem(newSelection);
+
+ setElementValue("reminder-remove-button",
+ listbox.childNodes.length < 1 && "true",
+ "disabled");
+ setupMaxReminders();
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ let listbox = document.getElementById("reminder-listbox");
+ let reminders = Array.from(listbox.childNodes).map(node => node.reminder);
+ if (window.arguments[0].onOk) {
+ window.arguments[0].onOk(reminders);
+ }
+
+ return true;
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ */
+function onCancel() {
+ if (window.arguments[0].onCancel) {
+ window.arguments[0].onCancel();
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-reminder.xul b/calendar/base/content/dialogs/calendar-event-dialog-reminder.xul
new file mode 100644
index 000000000..6ee951af0
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-reminder.xul
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-bindings.css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://calendar/locale/dialogs/calendar-event-dialog-reminder.dtd" >
+
+<dialog id="calendar-event-dialog-reminder"
+ title="&reminderdialog.title;"
+ windowtype="Calendar:EventDialog:Reminder"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-event-dialog-reminder.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+
+ <notificationbox id="reminder-notifications"/>
+
+ <!-- Listbox with custom reminders -->
+ <vbox flex="1">
+ <listbox id="reminder-listbox"
+ seltype="single"
+ class="event-dialog-listbox"
+ onselect="onReminderSelected()"
+ flex="1"/>
+ <hbox id="reminder-action-buttons-box" pack="end">
+ <button id="reminder-new-button"
+ label="&reminder.add.label;"
+ accesskey="&reminder.add.accesskey;"
+ oncommand="onNewReminder()"/>
+ <button id="reminder-remove-button"
+ label="&reminder.remove.label;"
+ accesskey="&reminder.remove.accesskey;"
+ oncommand="onRemoveReminder()"/>
+ </hbox>
+ </vbox>
+
+ <!-- Custom reminder details -->
+ <calendar-caption id="reminder-details-caption" label="&reminder.reminderDetails.label;"/>
+ <radiogroup id="reminder-relation-radiogroup"
+ onselect="setupRadioEnabledState(); updateReminder(event)">
+ <hbox id="reminder-relative-box" align="top" flex="1">
+ <radio id="reminder-relative-radio"
+ value="relative"
+ aria-labeledby="reminder-length reminder-unit reminder-relation reminder-origin"/>
+ <vbox id="reminder-relative-box" flex="1">
+ <hbox id="reminder-relative-length-unit-relation" flex="1">
+ <textbox id="reminder-length"
+ type="number"
+ size="1"
+ min="0"
+ onkeyup="updateReminder(event)"/>
+ <menulist id="reminder-unit" oncommand="updateReminder(event)" flex="1">
+ <menupopup id="reminder-unit-menupopup">
+ <menuitem id="reminder-minutes-menuitem"
+ label="&alarm.units.minutes;"
+ value="minutes"/>
+ <menuitem id="reminder-hours-menuitem"
+ label="&alarm.units.hours;"
+ value="hours"/>
+ <menuitem id="reminder-days-menuitem"
+ label="&alarm.units.days;"
+ value="days"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <menulist id="reminder-relation-origin" oncommand="updateReminder(event)">
+ <menupopup id="reminder-relation-origin-menupopup">
+ <!-- The labels here will be set in calendar-event-dialog-reminder.js -->
+ <menuitem id="reminder-before-start-menuitem"
+ value="before-START"/>
+ <menuitem id="reminder-after-start-menuitem"
+ value="after-START"/>
+ <menuitem id="reminder-before-end-menuitem"
+ value="before-END"/>
+ <menuitem id="reminder-after-end-menuitem"
+ value="after-END"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ <hbox id="reminder-absolute-box" flex="1">
+ <radio id="reminder-absolute-radio"
+ control="reminder-absolute-date"
+ value="absolute"/>
+ <datetimepicker id="reminder-absolute-date"/>
+ </hbox>
+ </radiogroup>
+
+ <!-- Custom reminder action -->
+ <calendar-caption id="reminder-actions-caption"
+ control="reminder-actions-menulist"
+ label="&reminder.action.label;"/>
+ <menulist id="reminder-actions-menulist"
+ oncommand="updateReminder(event)"
+ class="reminder-icon">
+ <!-- Make sure the id is formatted "reminder-action-<VALUE>", for accessibility -->
+ <!-- TODO provider specific -->
+ <menupopup id="reminder-actions-menupopup">
+ <menuitem id="reminder-action-DISPLAY"
+ class="reminder-icon menuitem-iconic"
+ value="DISPLAY"
+ label="&reminder.action.alert.label;"/>
+ <menuitem id="reminder-action-EMAIL"
+ class="reminder-icon menuitem-iconic"
+ value="EMAIL"
+ label="&reminder.action.email.label;"/>
+ </menupopup>
+ </menulist>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-timezone.js b/calendar/base/content/dialogs/calendar-event-dialog-timezone.js
new file mode 100644
index 000000000..998d0a3b5
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-timezone.js
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAccept, onCancel */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/**
+ * Sets up the timezone dialog from the window arguments, also setting up all
+ * dialog controls from the window's dates.
+ */
+function onLoad() {
+ let args = window.arguments[0];
+ window.time = args.time;
+ window.onAcceptCallback = args.onOk;
+
+ let tzProvider = args.calendar.getProperty("timezones.provider") ||
+ cal.getTimezoneService();
+ window.tzProvider = tzProvider;
+
+ let menulist = document.getElementById("timezone-menulist");
+ let tzMenuPopup = document.getElementById("timezone-menupopup");
+
+ // floating and UTC (if supported) at the top:
+ if (args.calendar.getProperty("capabilities.timezones.floating.supported") !== false) {
+ addMenuItem(tzMenuPopup, floating().displayName, floating().tzid);
+ }
+ if (args.calendar.getProperty("capabilities.timezones.UTC.supported") !== false) {
+ addMenuItem(tzMenuPopup, UTC().displayName, UTC().tzid);
+ }
+
+ let enumerator = tzProvider.timezoneIds;
+ let tzids = {};
+ let displayNames = [];
+ while (enumerator.hasMore()) {
+ let timezone = tzProvider.getTimezone(enumerator.getNext());
+ if (timezone && !timezone.isFloating && !timezone.isUTC) {
+ let displayName = timezone.displayName;
+ displayNames.push(displayName);
+ tzids[displayName] = timezone.tzid;
+ }
+ }
+ // the display names need to be sorted
+ displayNames.sort(String.localeCompare);
+ for (let i = 0; i < displayNames.length; ++i) {
+ let displayName = displayNames[i];
+ addMenuItem(tzMenuPopup, displayName, tzids[displayName]);
+ }
+
+ let index = findTimezone(window.time.timezone);
+ if (index < 0) {
+ index = findTimezone(calendarDefaultTimezone());
+ if (index < 0) {
+ index = 0;
+ }
+ }
+
+ menulist = document.getElementById("timezone-menulist");
+ menulist.selectedIndex = index;
+
+ updateTimezone();
+
+ opener.setCursor("auto");
+}
+
+/**
+ * Find the index of the timezone menuitem corresponding to the given timezone.
+ *
+ * @param timezone The calITimezone to look for.
+ * @return The index of the childnode below "timezone-menulist"
+ */
+function findTimezone(timezone) {
+ let tzid = timezone.tzid;
+ let menulist = document.getElementById("timezone-menulist");
+ let numChilds = menulist.childNodes[0].childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let menuitem = menulist.childNodes[0].childNodes[i];
+ if (menuitem.getAttribute("value") == tzid) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Handler function to call when the timezone selection has changed. Updates the
+ * timezone-time field and the timezone-stack.
+ */
+function updateTimezone() {
+ let menulist = document.getElementById("timezone-menulist");
+ let menuitem = menulist.selectedItem;
+ let timezone = window.tzProvider.getTimezone(menuitem.getAttribute("value"));
+
+ // convert the date/time to the currently selected timezone
+ // and display the result in the appropriate control.
+ // before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ let datetime = document.getElementById("timezone-time");
+ let time = window.time.getInTimezone(timezone);
+ time.timezone = cal.floating();
+ datetime.value = cal.dateTimeToJsDate(time);
+
+ // don't highlight any timezone in the map by default
+ let standardTZOffset = "none";
+ if (timezone.isUTC) {
+ standardTZOffset = "+0000";
+ } else if (!timezone.isFloating) {
+ let standard = timezone.icalComponent.getFirstSubcomponent("STANDARD");
+ // any reason why valueAsIcalString is used instead of plain value? xxx todo: ask mickey
+ standardTZOffset = standard.getFirstProperty("TZOFFSETTO").valueAsIcalString;
+ }
+
+ let image = document.getElementById("highlighter");
+ image.setAttribute("tzid", standardTZOffset);
+}
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ let menulist = document.getElementById("timezone-menulist");
+ let menuitem = menulist.selectedItem;
+ let timezoneString = menuitem.getAttribute("value");
+ let timezone = window.tzProvider.getTimezone(timezoneString);
+ let datetime = window.time.getInTimezone(timezone);
+ window.onAcceptCallback(datetime);
+ return true;
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ *
+ */
+function onCancel() {
+}
diff --git a/calendar/base/content/dialogs/calendar-event-dialog-timezone.xul b/calendar/base/content/dialogs/calendar-event-dialog-timezone.xul
new file mode 100644
index 000000000..bcdd1c7fa
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog-timezone.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-timezone-highlighter.css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %dtd3;
+ <!ENTITY % dtd4 SYSTEM "chrome://calendar/locale/preferences/timezones.dtd" > %dtd4;
+]>
+
+<dialog id="calendar-event-dialog-timezone"
+ title="&timezone.title.label;"
+ windowtype="Calendar:EventDialog:Timezone"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-event-dialog-timezone.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+
+ <hbox align="center">
+ <spacer flex="1"/>
+ <datetimepicker id="timezone-time" disabled="true"/>
+ </hbox>
+
+ <menulist id="timezone-menulist" oncommand="updateTimezone()">
+ <menupopup id="timezone-menupopup" style="height: 460px;"/>
+ </menulist>
+
+ <stack id="timezone-stack">
+ <image src="chrome://calendar-common/skin/timezone_map.png"/>
+ <image class="timezone-highlight" tzid="+0000" id="highlighter"/>
+ </stack>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-event-dialog.css b/calendar/base/content/dialogs/calendar-event-dialog.css
new file mode 100644
index 000000000..4b1093acd
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog.css
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+daypicker-weekday {
+ -moz-binding: url(chrome://calendar/content/calendar-daypicker.xml#daypicker-weekday);
+ -moz-user-focus: normal;
+}
+
+daypicker-monthday {
+ -moz-binding: url(chrome://calendar/content/calendar-daypicker.xml#daypicker-monthday);
+ -moz-user-focus: normal;
+}
+
+recurrence-preview {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-recurrence-preview.xml#recurrence-preview);
+ -moz-user-focus: normal;
+}
+
+/****************************************************************************************/
+
+attendees-list {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-attendees.xml#attendees-list);
+ -moz-user-focus: normal;
+}
+
+selection-bar {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-attendees.xml#selection-bar);
+ -moz-user-focus: normal;
+}
+
+/****************************************************************************************/
+
+scroll-container {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-freebusy.xml#scroll-container);
+ -moz-user-focus: normal;
+}
+
+freebusy-day {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-freebusy.xml#freebusy-day);
+ -moz-user-focus: normal;
+}
+
+freebusy-timebar {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-freebusy.xml#freebusy-timebar);
+ -moz-user-focus: normal;
+}
+
+freebusy-row {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-freebusy.xml#freebusy-row);
+ -moz-user-focus: normal;
+}
+
+freebusy-grid {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-freebusy.xml#freebusy-grid);
+ -moz-user-focus: normal;
+}
+
+/****************************************************************************************/
+
+timezone-page {
+ -moz-binding: url(chrome://calendar/content/calendar-event-dialog-timezone.xml#timezone-page);
+ -moz-user-focus: normal;
+}
diff --git a/calendar/base/content/dialogs/calendar-event-dialog.xul b/calendar/base/content/dialogs/calendar-event-dialog.xul
new file mode 100644
index 000000000..b639dd515
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-event-dialog.xul
@@ -0,0 +1,648 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/widgets/calendar-widget-bindings.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/primaryToolbar.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/messenger.css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd">
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ %brandDTD;
+ %globalDTD;
+ %calendarDTD;
+ %eventDialogDTD;
+]>
+
+<?xul-overlay href="chrome://lightning/content/lightning-item-toolbar.xul"?>
+
+<!-- Dialog id is changed during excution to allow different Window-icons
+ on this dialog. document.loadOverlay() will not work on this one. -->
+<dialog id="calendar-event-dialog"
+ title="&event.title.label;"
+ windowtype="Calendar:EventDialog"
+ onload="onLoadLightningItemPanel();"
+ onunload="onUnloadLightningItemPanel();"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-item-panel.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/toolbarIconColor.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="languageBundle"
+ src="chrome://global/locale/languageNames.properties"/>
+ </stringbundleset>
+
+ <!-- Command updater -->
+ <commandset id="globalEditMenuItems"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="goUpdateGlobalEditMenuItems()"/>
+ <commandset id="selectEditMenuItems"
+ commandupdater="true"
+ events="select"
+ oncommandupdate="goUpdateSelectEditMenuItems()"/>
+ <commandset id="undoEditMenuItems"
+ commandupdater="true"
+ events="undo"
+ oncommandupdate="goUpdateUndoEditMenuItems()"/>
+ <commandset id="clipboardEditMenuItems"
+ commandupdater="true"
+ events="clipboard"
+ oncommandupdate="goUpdatePasteMenuItems()"/>
+
+ <!-- Commands -->
+ <commandset id="itemCommands">
+
+ <!-- Item menu -->
+ <command id="cmd_item_new_event"
+ oncommand="openNewEvent()"/>
+ <command id="cmd_item_new_task"
+ oncommand="openNewTask()"/>
+ <command id="cmd_item_new_message"
+ oncommand="openNewMessage()"/>
+ <command id="cmd_item_new_card"
+ oncommand="openNewCardDialog()"/>
+ <command id="cmd_item_close"
+ oncommand="cancelDialog()"/>
+ <command id="cmd_save"
+ disable-on-readonly="true"
+ oncommand="onCommandSave()"/>
+ <command id="cmd_item_delete"
+ disable-on-readonly="true"
+ oncommand="onCommandDeleteItem()"/>
+ <command id="cmd_printSetup"
+ oncommand="PrintUtils.showPageSetup()"/>
+ <command id="cmd_print"
+ disabled="true"
+ oncommand="calPrint()"/>
+
+ <!-- Edit menu -->
+ <command id="cmd_undo"
+ disabled="true"
+ oncommand="goDoCommand('cmd_undo')"/>
+ <command id="cmd_redo"
+ disabled="true"
+ oncommand="goDoCommand('cmd_redo')"/>
+ <command id="cmd_cut"
+ disabled="true"
+ oncommand="goDoCommand('cmd_cut')"/>
+ <command id="cmd_copy"
+ disabled="true"
+ oncommand="goDoCommand('cmd_copy')"/>
+ <command id="cmd_paste"
+ disabled="true"
+ oncommand="goDoCommand('cmd_paste')"/>
+ <command id="cmd_selectAll"
+ disabled="true"
+ oncommand="goDoCommand('cmd_selectAll')"/>
+
+ <!-- View menu -->
+ <command id="cmd_toolbar"
+ oncommand="onCommandViewToolbar('event-toolbar',
+ 'view-toolbars-event-menuitem')"/>
+ <command id="cmd_customize"
+ oncommand="onCommandCustomize()"/>
+ <command id="cmd_toggle_link"
+ persist="checked"
+ oncommand="toggleLink()"/>
+
+ <!-- status -->
+ <command id="cmd_status_none"
+ oncommand="editStatus(event.target)"
+ hidden="true"
+ value="NONE"/>
+ <command id="cmd_status_tentative"
+ oncommand="editStatus(event.target)"
+ value="TENTATIVE"/>
+ <command id="cmd_status_confirmed"
+ oncommand="editStatus(event.target)"
+ value="CONFIRMED"/>
+ <command id="cmd_status_cancelled"
+ oncommand="editStatus(event.target)"
+ value="CANCELLED"/>
+
+ <!-- priority -->
+ <command id="cmd_priority_none"
+ oncommand="editPriority(event.target)"
+ value="0"/>
+ <command id="cmd_priority_low"
+ oncommand="editPriority(event.target)"
+ value="9"/>
+ <command id="cmd_priority_normal"
+ oncommand="editPriority(event.target)"
+ value="5"/>
+ <command id="cmd_priority_high"
+ oncommand="editPriority(event.target)"
+ value="1"/>
+
+ <!-- freebusy -->
+ <command id="cmd_showtimeas_busy"
+ oncommand="editShowTimeAs(event.target)"
+ value="OPAQUE"/>
+ <command id="cmd_showtimeas_free"
+ oncommand="editShowTimeAs(event.target)"
+ value="TRANSPARENT"/>
+
+ <!-- attendees -->
+ <command id="cmd_attendees"
+ oncommand="editAttendees();"/>
+ <command id="cmd_email"
+ oncommand="sendMailToAttendees(window.attendees);"/>
+ <command id="cmd_email_undecided"
+ oncommand="sendMailToUndecidedAttendees(window.attendees);"/>
+
+ <!-- accept, attachments, timezone -->
+ <command id="cmd_accept"
+ disable-on-readonly="true"
+ oncommand="acceptDialog();"/>
+ <command id="cmd_attach_url"
+ disable-on-readonly="true"
+ oncommand="attachURL()"/>
+ <command id="cmd_attach_cloud"
+ disable-on-readonly="true"/>
+ <command id="cmd_timezone"
+ persist="checked"
+ checked="false"
+ oncommand="toggleTimezoneLinks()"/>
+ </commandset>
+
+ <keyset id="calendar-event-dialog-keyset">
+ <key id="new-event-key"
+ modifiers="accel"
+ key="&event.dialog.new.event.key2;"
+ command="cmd_item_new_event"/>
+ <key id="new-task-key"
+ modifiers="accel"
+ key="&event.dialog.new.task.key2;"
+ command="cmd_item_new_task"/>
+ <key id="new-message-key"
+ modifiers="accel"
+ key="&event.dialog.new.message.key2;"
+ command="cmd_item_new_message"/>
+ <key id="close-key"
+ modifiers="accel"
+ key="&event.dialog.close.key;"
+ command="cmd_item_close"/>
+ <key id="save-key"
+ modifiers="accel"
+ key="&event.dialog.save.key;"
+ command="cmd_save"/>
+ <key id="saveandclose-key"
+ modifiers="accel"
+ key="&event.dialog.saveandclose.key;"
+ command="cmd_accept"/>
+ <key id="saveandclose-key2"
+ modifiers="accel"
+ keycode="VK_RETURN"
+ command="cmd_accept"/>
+ <key id="print-key"
+ modifiers="accel"
+ key="&event.dialog.print.key;"
+ command="cmd_print"/>
+ <key id="undo-key"
+ modifiers="accel"
+ key="&event.dialog.undo.key;"
+ command="cmd_undo"/>
+ <key id="redo-key"
+ modifiers="accel"
+ key="&event.dialog.redo.key;"
+ command="cmd_redo"/>
+ <key id="cut-key"
+ modifiers="accel"
+ key="&event.dialog.cut.key;"
+ command="cmd_cut"/>
+ <key id="copy-key"
+ modifiers="accel"
+ key="&event.dialog.copy.key;"
+ command="cmd_copy"/>
+ <key id="paste-key"
+ modifiers="accel"
+ key="&event.dialog.paste.key;"
+ command="cmd_paste"/>
+ <key id="select-all-key"
+ modifiers="accel"
+ key="&event.dialog.select.all.key;"
+ command="cmd_selectAll"/>
+ </keyset>
+
+ <menupopup id="event-dialog-toolbar-context-menu">
+ <menuitem id="CustomizeDialogToolbar"
+ label="&event.menu.view.toolbars.customize.label;"
+ command="cmd_customize"/>
+ </menupopup>
+
+ <!-- Toolbox contains the menubar -->
+ <toolbox id="event-toolbox"
+ class="mail-toolbox"
+ mode="full"
+ defaultmode="full"
+#ifdef XP_MACOSX
+ iconsize="small"
+ defaulticonsize="small"
+#endif
+ labelalign="end"
+ defaultlabelalign="end"
+ isNotMainWindow="true">
+
+ <!-- Menubar -->
+ <menubar id="event-menubar">
+
+ <!-- Item menu -->
+ <!-- These 2 Strings are placeholders, values are set at runtime -->
+ <menu label="Item"
+ accesskey="I"
+ id="item-menu">
+ <menupopup id="item-menupopup">
+ <menu id="item-new-menu"
+ label="&event.menu.item.new.label;"
+ accesskey="&event.menu.item.new.accesskey;">
+ <menupopup id="item-new-menupopup">
+ <menuitem id="item-new-message-menuitem"
+ label="&event.menu.item.new.message.label;"
+ accesskey="&event.menu.item.new.message.accesskey;"
+ key="new-message-key"
+ command="cmd_item_new_message"
+ disable-on-readonly="true"/>
+ <menuitem id="item-new-event-menuitem"
+ label="&event.menu.item.new.event.label;"
+ accesskey="&event.menu.item.new.event.accesskey;"
+ key="new-event-key"
+ command="cmd_item_new_event"
+ disable-on-readonly="true"/>
+ <menuitem id="item-new-task-menuitem"
+ label="&event.menu.item.new.task.label;"
+ accesskey="&event.menu.item.new.task.accesskey;"
+ key="new-task-key"
+ command="cmd_item_new_task"
+ disable-on-readonly="true"/>
+ <menuseparator id="item-new-menuseparator1"/>
+ <menuitem id="item-new-address-menuitem"
+ label="&event.menu.item.new.contact.label;"
+ accesskey="&event.menu.item.new.contact.accesskey;"
+ command="cmd_item_new_card"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="item-menuseparator1"/>
+ <menuitem id="item-save-menuitem"
+ label="&event.menu.item.save.label;"
+ accesskey="&event.menu.item.save.accesskey;"
+ key="save-key"
+ command="cmd_save"/>
+ <menuitem id="item-saveandclose-menuitem"
+ label="&event.menu.item.saveandclose.label;"
+ accesskey="&event.menu.item.saveandclose.accesskey;"
+ key="saveandclose-key"
+ command="cmd_accept"/>
+ <menuitem id="item-delete-menuitem"
+ label="&event.menu.item.delete.label;"
+ accesskey="&event.menu.item.delete.accesskey;"
+ command="cmd_item_delete"
+ disable-on-readonly="true"/>
+ <menuitem id="item-pagesetup-menuitem"
+ label="&event.menu.item.page.setup.label;"
+ accesskey="&event.menu.item.page.setup.accesskey;"
+ command="cmd_printSetup"
+ disable-on-readonly="true"/>
+ <menuitem id="item-print-menuitem"
+ label="&event.menu.item.print.label;"
+ accesskey="&event.menu.item.print.accesskey;"
+ key="print-key"
+ command="cmd_print"
+ disable-on-readonly="true"/>
+ <menuseparator id="item-menuseparator1"/>
+ <menuitem id="item-close-menuitem"
+ label="&event.menu.item.close.label;"
+ accesskey="&event.menu.item.close.accesskey;"
+ key="close-key"
+ command="cmd_item_close"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+
+ <!-- Edit menu -->
+ <menu id="edit-menu"
+ label="&event.menu.edit.label;"
+ accesskey="&event.menu.edit.accesskey;"
+ collapse-on-readonly="true">
+ <menupopup id="edit-menupopup">
+ <menuitem id="edit-undo-menuitem"
+ label="&event.menu.edit.undo.label;"
+ accesskey="&event.menu.edit.undo.accesskey;"
+ key="undo-key"
+ command="cmd_undo"/>
+ <menuitem id="edit-redo-menuitem"
+ label="&event.menu.edit.redo.label;"
+ accesskey="&event.menu.edit.redo.accesskey;"
+ key="redo-key"
+ command="cmd_redo"/>
+ <menuseparator id="edit-menuseparator1"/>
+ <menuitem id="edit-cut-menuitem"
+ label="&event.menu.edit.cut.label;"
+ accesskey="&event.menu.edit.cut.accesskey;"
+ key="cut-key"
+ command="cmd_cut"/>
+ <menuitem id="edit-copy-menuitem"
+ label="&event.menu.edit.copy.label;"
+ accesskey="&event.menu.edit.copy.accesskey;"
+ key="copy-key"
+ command="cmd_copy"/>
+ <menuitem id="edit-paste-menuitem"
+ label="&event.menu.edit.paste.label;"
+ accesskey="&event.menu.edit.paste.accesskey;"
+ key="paste-key"
+ command="cmd_paste"/>
+ <menuseparator id="edit-menuseparator2"/>
+ <menuitem id="edit-selectall-menuitem"
+ label="&event.menu.edit.select.all.label;"
+ accesskey="&event.menu.edit.select.all.accesskey;"
+ key="select-all-key"
+ command="cmd_selectAll"/>
+ </menupopup>
+ </menu>
+
+ <!-- View menu -->
+ <menu id="view-menu"
+ label="&event.menu.view.label;"
+ accesskey="&event.menu.view.accesskey;"
+ collapse-on-readonly="true">
+ <menupopup id="view-menupopup">
+ <menu id="view-toolbars-menu"
+ label="&event.menu.view.toolbars.label;"
+ accesskey="&event.menu.view.toolbars.accesskey;">
+ <menupopup id="view-toolbars-menupopup">
+ <menuitem id="view-toolbars-event-menuitem"
+ label="&event.menu.view.toolbars.event.label;"
+ accesskey="&event.menu.view.toolbars.event.accesskey;"
+ type="checkbox"
+ checked="true"
+ command="cmd_toolbar"/>
+ <menuseparator id="view-toolbars-menuseparator1"/>
+ <menuitem id="view-toolbars-customize-menuitem"
+ label="&event.menu.view.toolbars.customize.label;"
+ accesskey="&event.menu.view.toolbars.customize.accesskey;"
+ command="cmd_customize"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="view-menu-toolbars-separator"/>
+ <menuitem id="view-show-link-menuitem"
+ label="&event.menu.view.showlink.label;"
+ accesskey="&event.menu.view.showlink.accesskey;"
+ type="checkbox"
+ command="cmd_toggle_link"
+ observes="cmd_toggle_link"/>
+ </menupopup>
+ </menu>
+
+ <!-- Options menu -->
+ <menu id="options-menu"
+ label="&event.menu.options.label;"
+ accesskey="&event.menu.options.accesskey;">
+ <menupopup id="options-menupopup">
+ <menuitem id="options-attendees-menuitem"
+ label="&event.menu.options.attendees.label;"
+ accesskey="&event.menu.options.attendees.accesskey;"
+ command="cmd_attendees"
+ disable-on-readonly="true"/>
+ <menu id="options-attachments-menu"
+ label="&event.attachments.menubutton.label;"
+ accesskey="&event.attachments.menubutton.accesskey;">
+ <menupopup id="options-attachments-menupopup">
+ <menuitem id="options-attachments-url-menuitem"
+ label="&event.attachments.url.label;"
+ accesskey="&event.attachments.url.accesskey;"
+ command="cmd_attach_url"/>
+ </menupopup>
+ </menu>
+ <menuitem id="options-timezones-menuitem"
+ label="&event.menu.options.timezone2.label;"
+ accesskey="&event.menu.options.timezone2.accesskey;"
+ type="checkbox"
+ command="cmd_timezone"
+ disable-on-readonly="true"/>
+ <menuseparator id="options-menuseparator1"/>
+ <menu id="options-priority-menu"
+ label="&event.menu.options.priority2.label;"
+ accesskey="&event.menu.options.priority2.accesskey;"
+ disable-on-readonly="true">
+ <menupopup id="options-priority-menupopup">
+ <menuitem id="options-priority-none-menuitem"
+ label="&event.menu.options.priority.notspecified.label;"
+ accesskey="&event.menu.options.priority.notspecified.accesskey;"
+ type="radio"
+ command="cmd_priority_none"
+ disable-on-readonly="true"/>
+ <menuitem id="options-priority-low-menuitem"
+ label="&event.menu.options.priority.low.label;"
+ accesskey="&event.menu.options.priority.low.accesskey;"
+ type="radio"
+ command="cmd_priority_low"
+ disable-on-readonly="true"/>
+ <menuitem id="options-priority-normal-label"
+ label="&event.menu.options.priority.normal.label;"
+ accesskey="&event.menu.options.priority.normal.accesskey;"
+ type="radio"
+ command="cmd_priority_normal"
+ disable-on-readonly="true"/>
+ <menuitem id="options-priority-high-label"
+ label="&event.menu.options.priority.high.label;"
+ accesskey="&event.menu.options.priority.high.accesskey;"
+ type="radio"
+ command="cmd_priority_high"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+ <menu id="options-privacy-menu"
+ label="&event.menu.options.privacy.label;"
+ accesskey="&event.menu.options.privacy.accesskey;"
+ disable-on-readonly="true">
+ <menupopup id="options-privacy-menupopup">
+ <menuitem id="options-privacy-public-menuitem"
+ label="&event.menu.options.privacy.public.label;"
+ accesskey="&event.menu.options.privacy.public.accesskey;"
+ type="radio"
+ privacy="PUBLIC"
+ oncommand="editPrivacy(this, event)"
+ disable-on-readonly="true"/>
+ <menuitem id="options-privacy-confidential-menuitem"
+ label="&event.menu.options.privacy.confidential.label;"
+ accesskey="&event.menu.options.privacy.confidential.accesskey;"
+ type="radio"
+ privacy="CONFIDENTIAL"
+ oncommand="editPrivacy(this, event)"
+ disable-on-readonly="true"/>
+ <menuitem id="options-privacy-private-menuitem"
+ label="&event.menu.options.privacy.private.label;"
+ accesskey="&event.menu.options.privacy.private.accesskey;"
+ type="radio"
+ privacy="PRIVATE"
+ oncommand="editPrivacy(this, event)"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+ <menu id="options-status-menu"
+ label="&newevent.status.label;"
+ accesskey="&newevent.status.accesskey;"
+ class="event-only"
+ disable-on-readonly="true">
+ <menupopup id="options-status-menupopup">
+ <menuitem id="options-status-none-menuitem"
+ label="&newevent.eventStatus.none.label;"
+ accesskey="&newevent.eventStatus.none.accesskey;"
+ type="radio"
+ command="cmd_status_none"
+ disable-on-readonly="true"/>
+ <menuitem id="options-status-tentative-menuitem"
+ label="&newevent.status.tentative.label;"
+ accesskey="&newevent.status.tentative.accesskey;"
+ type="radio"
+ command="cmd_status_tentative"
+ disable-on-readonly="true"/>
+ <menuitem id="options-status-confirmed-menuitem"
+ label="&newevent.status.confirmed.label;"
+ accesskey="&newevent.status.confirmed.accesskey;"
+ type="radio"
+ command="cmd_status_confirmed"
+ disable-on-readonly="true"/>
+ <menuitem id="options-status-canceled-menuitem"
+ label="&newevent.eventStatus.cancelled.label;"
+ accesskey="&newevent.eventStatus.cancelled.accesskey;"
+ type="radio"
+ command="cmd_status_cancelled"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="options-menuseparator2" class="event-only"/>
+ <menu id="options-freebusy-menu"
+ class="event-only"
+ label="&event.menu.options.show.time.label;"
+ accesskey="&event.menu.options.show.time.accesskey;"
+ disable-on-readonly="true">
+ <menupopup id="options-freebusy-menupopup">
+ <menuitem id="options-freebusy-busy-menuitem"
+ label="&event.menu.options.show.time.busy.label;"
+ accesskey="&event.menu.options.show.time.busy.accesskey;"
+ type="radio"
+ command="cmd_showtimeas_busy"
+ disable-on-readonly="true"/>
+ <menuitem id="options-freebusy-free-menuitem"
+ label="&event.menu.options.show.time.free.label;"
+ accesskey="&event.menu.options.show.time.free.accesskey;"
+ type="radio"
+ command="cmd_showtimeas_free"
+ disable-on-readonly="true"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <!-- toolbarpalette items are added with an overlay -->
+ <toolbarpalette id="event-toolbarpalette"/>
+ <!-- toolboxid is set here since we move the toolbar around in tabs -->
+ <toolbar id="event-toolbar"
+ toolboxid="event-toolbox"
+ class="chromeclass-toolbar"
+ customizable="true"
+ labelalign="end"
+ defaultlabelalign="end"
+ context="event-dialog-toolbar-context-menu"
+ defaultset="button-saveandclose,button-attendees,button-privacy,button-url,button-delete"/>
+ <toolbarset id="custom-toolbars" context="event-dialog-toolbar-context-menu"/>
+ </toolbox>
+
+ <!-- the iframe is inserted here dynamically in the "load" handler function -->
+
+ <statusbar class="chromeclass-status" id="status-bar">
+ <statusbarpanel id="status-text"
+ flex="1"/>
+ <statusbarpanel id="status-privacy"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.statusbarpanel.privacy.label;"/>
+ <hbox id="status-privacy-public-box" privacy="PUBLIC">
+ <label value="&event.menu.options.privacy.public.label;"/>
+ </hbox>
+ <hbox id="status-privacy-confidential-box" privacy="CONFIDENTIAL">
+ <label value="&event.menu.options.privacy.confidential.label;"/>
+ </hbox>
+ <hbox id="status-privacy-private-box" privacy="PRIVATE">
+ <label value="&event.menu.options.privacy.private.label;"/>
+ </hbox>
+ </statusbarpanel>
+ <statusbarpanel id="status-priority"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.priority2.label;"/>
+ <image id="image-priority-low"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="low"/>
+ <image id="image-priority-normal"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="normal"/>
+ <image id="image-priority-high"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="high"/>
+ </statusbarpanel>
+ <statusbarpanel id="status-status"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&task.status.label;"/>
+ <label id="status-status-tentative-label"
+ value="&newevent.status.tentative.label;"
+ hidden="true"/>
+ <label id="status-status-confirmed-label"
+ value="&newevent.status.confirmed.label;"
+ hidden="true"/>
+ <label id="status-status-cancelled-label"
+ value="&newevent.eventStatus.cancelled.label;"
+ hidden="true"/>
+ </statusbarpanel>
+ <statusbarpanel id="status-freebusy"
+ class="event-only"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.statusbarpanel.freebusy.label;"/>
+ <label id="status-freebusy-free-label"
+ value="&event.freebusy.legend.free;"
+ hidden="true"/>
+ <label id="status-freebusy-busy-label"
+ value="&event.freebusy.legend.busy;"
+ hidden="true"/>
+ </statusbarpanel>
+ </statusbar>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-invitations-dialog.css b/calendar/base/content/dialogs/calendar-invitations-dialog.css
new file mode 100644
index 000000000..5a693fcb1
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-invitations-dialog.css
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+calendar-invitations-richlistbox {
+ -moz-binding: url(chrome://calendar/content/calendar-invitations-list.xml#calendar-invitations-richlistbox);
+}
+
+calendar-invitations-richlistitem {
+ -moz-binding: url(chrome://calendar/content/calendar-invitations-list.xml#calendar-invitations-richlistitem);
+}
+
+calendar-invitations-richlistitem[selected="true"] {
+ -moz-user-focus: normal;
+}
diff --git a/calendar/base/content/dialogs/calendar-invitations-dialog.js b/calendar/base/content/dialogs/calendar-invitations-dialog.js
new file mode 100644
index 000000000..ce699c805
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-invitations-dialog.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onUnload, onAccept, onCancel */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Sets up the invitations dialog from the window arguments, retrieves the
+ * invitations from the invitations manager.
+ */
+function onLoad() {
+ let operationListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ let updatingBox = document.getElementById("updating-box");
+ updatingBox.setAttribute("hidden", "true");
+ let richListBox = document.getElementById("invitations-listbox");
+ if (richListBox.getRowCount() > 0) {
+ richListBox.selectedIndex = 0;
+ } else {
+ let noInvitationsBox =
+ document.getElementById("noinvitations-box");
+ noInvitationsBox.removeAttribute("hidden");
+ }
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (!Components.isSuccessCode(aStatus)) {
+ return;
+ }
+ document.title = invitationsText + " (" + aCount + ")";
+ let updatingBox = document.getElementById("updating-box");
+ updatingBox.setAttribute("hidden", "true");
+ let richListBox = document.getElementById("invitations-listbox");
+ for (let item of aItems) {
+ richListBox.addCalendarItem(item);
+ }
+ }
+ };
+
+ let updatingBox = document.getElementById("updating-box");
+ updatingBox.removeAttribute("hidden");
+
+ let args = window.arguments[0];
+ args.invitationsManager.getInvitations(operationListener,
+ args.onLoadOperationListener);
+
+ opener.setCursor("auto");
+}
+
+/**
+ * Cleans up the invitations dialog, cancels pending requests.
+ */
+function onUnload() {
+ let args = window.arguments[0];
+ args.requestManager.cancelPendingRequests();
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ let args = window.arguments[0];
+ fillJobQueue(args.queue);
+ args.invitationsManager.processJobQueue(args.queue, args.finishedCallBack);
+ return true;
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ */
+function onCancel() {
+ let args = window.arguments[0];
+ if (args.finishedCallBack) {
+ args.finishedCallBack();
+ }
+}
+
+/**
+ * Fills the job queue from the invitations-listbox's items. The job queue
+ * contains objects for all items that have a modified participation status.
+ *
+ * @param queue The queue to fill.
+ */
+function fillJobQueue(queue) {
+ let richListBox = document.getElementById("invitations-listbox");
+ let rowCount = richListBox.getRowCount();
+ for (let i = 0; i < rowCount; i++) {
+ let richListItem = richListBox.getItemAtIndex(i);
+ let newStatus = richListItem.participationStatus;
+ let oldStatus = richListItem.initialParticipationStatus;
+ if (newStatus != oldStatus) {
+ let actionString = "modify";
+ let oldCalendarItem = richListItem.calendarItem;
+ let newCalendarItem = oldCalendarItem.clone();
+
+ // set default alarm on unresponded items that have not been declined:
+ if (!newCalendarItem.getAlarms({}).length &&
+ (oldStatus == "NEEDS-ACTION") &&
+ (newStatus != "DECLINED")) {
+ cal.alarms.setDefaultValues(newCalendarItem);
+ }
+
+ richListItem.setCalendarItemParticipationStatus(newCalendarItem,
+ newStatus);
+ let job = {
+ action: actionString,
+ oldItem: oldCalendarItem,
+ newItem: newCalendarItem
+ };
+ queue.push(job);
+ }
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-invitations-dialog.xul b/calendar/base/content/dialogs/calendar-invitations-dialog.xul
new file mode 100644
index 000000000..d8c6eb9c7
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-invitations-dialog.xul
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/calendar-invitations-dialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/calendar-invitations-dialog.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar-invitations-dialog.dtd" > %dtd1;
+]>
+
+<dialog
+ id="calendar-invitations-dialog"
+ title="&calendar.invitations.dialog.invitations.text;"
+ windowtype="Calendar:InvitationsDialog"
+ buttons="accept,cancel"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ onload="return onLoad();"
+ onunload="return onUnload();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-invitations-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <script type="application/javascript" >
+ var invitationsText = "&calendar.invitations.dialog.invitations.text;";
+ </script>
+
+ <vbox id="dialog-box" flex="1">
+ <stack flex="1">
+ <calendar-invitations-richlistbox id="invitations-listbox" flex="1"/>
+ <hbox id="updating-box" align="center" pack="center" hidden="true">
+ <label value="&calendar.invitations.dialog.statusmessage.updating.text;"
+ crop="end"/>
+ <image class="calendar-invitations-updating-icon"/>
+ </hbox>
+ <hbox id="noinvitations-box" align="center" pack="center" hidden="true">
+ <label value="&calendar.invitations.dialog.statusmessage.noinvitations.text;"
+ crop="end"/>
+ </hbox>
+ </stack>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-invitations-list.xml b/calendar/base/content/dialogs/calendar-invitations-list.xml
new file mode 100644
index 000000000..0ddd27993
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-invitations-list.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar-invitations-dialog.dtd" > %dtd1;
+]>
+
+<bindings id="calendar-invitations-list-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+ <binding id="calendar-invitations-richlistbox"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"
+ xbl:inherits="flex">
+ <implementation>
+ <!-- methods -->
+ <method name="addCalendarItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let newNode = createXULElement("calendar-invitations-richlistitem");
+ this.appendChild(newNode);
+ newNode.setAttribute("anonid", "invitations-listitem");
+ newNode.calendarItem = aItem;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-invitations-richlistitem"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox align="start" flex="1">
+ <xul:image anonid="icon" class="calendar-invitations-richlistitem-icon"/>
+ <xul:vbox flex="1">
+ <xul:label anonid="title" class="calendar-invitations-richlistitem-title"
+ crop="end"/>
+ <xul:label anonid="date" crop="end"/>
+ <xul:label anonid="recurrence" crop="end"/>
+ <xul:label anonid="location" crop="end"/>
+ <xul:label anonid="organizer" crop="end"/>
+ <xul:label anonid="attendee" crop="end"/>
+ <xul:label anonid="spacer" value="" hidden="true"/>
+ </xul:vbox>
+ <xul:vbox>
+ <xul:button anonid="accept"
+ xbl:inherits="group=itemId"
+ type="radio"
+ class="calendar-invitations-richlistitem-accept-button
+ calendar-invitations-richlistitem-button"
+ label="&calendar.invitations.list.accept.button.label;"
+ oncommand="accept();"/>
+ <xul:button anonid="decline"
+ xbl:inherits="group=itemId"
+ type="radio"
+ class="calendar-invitations-richlistitem-decline-button
+ calendar-invitations-richlistitem-button"
+ label="&calendar.invitations.list.decline.button.label;"
+ oncommand="decline();"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <!-- fields -->
+ <field name="mDateFormatter">null</field>
+ <field name="mCalendarItem">null</field>
+ <field name="mInitialParticipationStatus">null</field>
+ <field name="mParticipationStatus">null</field>
+
+ <property name="mStrings">
+ <getter>
+ return {
+ alldayEvent: "&calendar.invitations.list.alldayevent.text;",
+ recurrentEvent: "&calendar.invitations.list.recurrentevent.text;",
+ location: "&calendar.invitations.list.location.text;",
+ organizer: "&calendar.invitations.list.organizer.text;",
+ attendee: "&calendar.invitations.list.attendee.text;",
+ none: "&calendar.invitations.list.none.text;"
+ };
+ </getter>
+ </property>
+
+ <!-- properties -->
+ <property name="calendarItem">
+ <getter><![CDATA[
+ return this.mCalendarItem;
+ ]]></getter>
+ <setter><![CDATA[
+ this.setCalendarItem(val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="initialParticipationStatus">
+ <getter><![CDATA[
+ return this.mInitialParticipationStatus;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mInitialParticipationStatus = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="participationStatus">
+ <getter><![CDATA[
+ return this.mParticipationStatus;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mParticipationStatus = val;
+ let icon = document.getAnonymousElementByAttribute(
+ this, "anonid", "icon");
+ icon.setAttribute("status", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- constructor -->
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ this.mDateFormatter = getDateFormatter();
+ ]]></constructor>
+
+ <!-- methods -->
+ <method name="setCalendarItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ this.mCalendarItem = aItem;
+ this.mInitialParticipationStatus =
+ this.getCalendarItemParticipationStatus(aItem);
+ this.participationStatus = this.mInitialParticipationStatus;
+
+ let titleLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "title");
+ titleLabel.setAttribute("value", aItem.title);
+
+ let dateLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "date");
+ let dateString = this.mDateFormatter.formatItemInterval(aItem);
+ if (aItem.startDate.isDate) {
+ dateString += ", " + this.mStrings.alldayEvent;
+ }
+ dateLabel.setAttribute("value", dateString);
+
+ let recurrenceLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "recurrence");
+ if (aItem.recurrenceInfo) {
+ recurrenceLabel.setAttribute("value", this.mStrings.recurrentEvent);
+ } else {
+ recurrenceLabel.setAttribute("hidden", "true");
+ let spacer = document.getAnonymousElementByAttribute(
+ this, "anonid", "spacer");
+ spacer.removeAttribute("hidden");
+ }
+
+ let locationLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "location");
+ let locationString = this.mStrings.location;
+ let locationProperty = aItem.getProperty("LOCATION");
+ if (locationProperty && locationProperty.length > 0) {
+ locationString += locationProperty;
+ } else {
+ locationString += this.mStrings.none;
+ }
+ locationLabel.setAttribute("value", locationString);
+
+ let organizerLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "organizer");
+ let organizerString = this.mStrings.organizer;
+ let org = aItem.organizer;
+ if (org) {
+ if (org.commonName && org.commonName.length > 0) {
+ organizerString += org.commonName;
+ } else if (org.id) {
+ organizerString += org.id.replace(/^mailto:/i, "");
+ }
+ }
+ organizerLabel.setAttribute("value", organizerString);
+
+ let attendeeLabel = document.getAnonymousElementByAttribute(
+ this, "anonid", "attendee");
+ let attendeeString = this.mStrings.attendee;
+ let att = cal.getInvitedAttendee(aItem);
+ if (att) {
+ if (att.commonName && att.commonName.length > 0) {
+ attendeeString += att.commonName;
+ } else if (att.id) {
+ attendeeString += att.id.replace(/^mailto:/i, "");
+ }
+ }
+ attendeeLabel.setAttribute("value", attendeeString);
+ this.setAttribute("itemId", aItem.hashId);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getCalendarItemParticipationStatus">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let att = cal.getInvitedAttendee(aItem);
+ return (att ? att.participationStatus : null);
+ ]]></body>
+ </method>
+
+ <method name="setCalendarItemParticipationStatus">
+ <parameter name="aItem"/>
+ <parameter name="aStatus"/>
+ <body><![CDATA[
+ let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
+ if (calendar) {
+ let att = calendar.getInvitedAttendee(aItem);
+ if (att) {
+ let att_ = att.clone();
+ att_.participationStatus = aStatus;
+
+ // Update attendee
+ aItem.removeAttendee(att);
+ aItem.addAttendee(att_);
+ return true;
+ }
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="accept">
+ <body><![CDATA[
+ this.participationStatus = "ACCEPTED";
+ ]]></body>
+ </method>
+
+ <method name="decline">
+ <body><![CDATA[
+ this.participationStatus = "DECLINED";
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/dialogs/calendar-migration-dialog.js b/calendar/base/content/dialogs/calendar-migration-dialog.js
new file mode 100644
index 000000000..331debe89
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-migration-dialog.js
@@ -0,0 +1,647 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var SUNBIRD_UID = "{718e30fb-e89b-41dd-9da7-e25a45638b28}";
+var FIREFOX_UID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+//
+// The front-end wizard bits.
+//
+var gMigrateWizard = {
+ /**
+ * Called from onload of the migrator window. Takes all of the migrators
+ * that were passed in via window.arguments and adds them to checklist. The
+ * user can then check these off to migrate the data from those sources.
+ */
+ loadMigrators: function gmw_load() {
+ var listbox = document.getElementById("datasource-list");
+
+ //XXX Once we have branding for lightning, this hack can go away
+ var props = Services.strings.createBundle("chrome://calendar/locale/migration.properties");
+
+ var wizard = document.getElementById("migration-wizard");
+ var desc = document.getElementById("wizard-desc");
+ // Since we don't translate "Lightning"...
+ wizard.title = props.formatStringFromName("migrationTitle",
+ ["Lightning"],
+ 1);
+ desc.textContent = props.formatStringFromName("migrationDescription",
+ ["Lightning"],
+ 1);
+
+ migLOG("migrators: " + window.arguments.length);
+ for (var migrator of window.arguments[0]) {
+ var listItem = document.createElement("listitem");
+ listItem.setAttribute("type", "checkbox");
+ listItem.setAttribute("checked", true);
+ listItem.setAttribute("label", migrator.title);
+ listItem.migrator = migrator;
+ listbox.appendChild(listItem);
+ }
+ },
+
+ /**
+ * Called from the second page of the wizard. Finds all of the migrators
+ * that were checked and begins migrating their data. Also controls the
+ * progress dialog so the user can see what is happening. (somewhat)
+ */
+ migrateChecked: function gmw_migrate() {
+ var migrators = [];
+
+ // Get all the checked migrators into an array
+ var listbox = document.getElementById("datasource-list");
+ for (var i = listbox.childNodes.length-1; i >= 0; i--) {
+ if (listbox.childNodes[i].getAttribute("checked")) {
+ migrators.push(listbox.childNodes[i].migrator);
+ }
+ }
+
+ // If no migrators were checked, then we're done
+ if (migrators.length == 0) {
+ window.close();
+ }
+
+ // Don't let the user get away while we're migrating
+ //XXX may want to wire this into the 'cancel' function once that's
+ // written
+ var wizard = document.getElementById("migration-wizard");
+ wizard.canAdvance = false;
+ wizard.canRewind = false;
+
+ // We're going to need this for the progress meter's description
+ var props = Services.strings.createBundle("chrome://calendar/locale/migration.properties");
+ var label = document.getElementById("progress-label");
+ var meter = document.getElementById("migrate-progressmeter");
+
+ var i = 0;
+ // Because some of our migrators involve async code, we need this
+ // call-back function so we know when to start the next migrator.
+ function getNextMigrator() {
+ if (migrators[i]) {
+ var mig = migrators[i];
+
+ // Increment i to point to the next migrator
+ i++;
+ migLOG("starting migrator: " + mig.title);
+ label.value = props.formatStringFromName("migratingApp",
+ [mig.title],
+ 1);
+ meter.value = (i-1)/migrators.length*100;
+ mig.args.push(getNextMigrator);
+
+ try {
+ mig.migrate.apply(mig, mig.args);
+ } catch (e) {
+ migLOG("Failed to migrate: " + mig.title);
+ migLOG(e);
+ getNextMigrator();
+ }
+ } else {
+ migLOG("migration done");
+ wizard.canAdvance = true;
+ label.value = props.GetStringFromName("finished");
+ meter.value = 100;
+ gMigrateWizard.setCanRewindFalse();
+ }
+ }
+
+ // And get the first migrator
+ getNextMigrator();
+ },
+
+ /**
+ * Makes sure the wizard "back" button can not be pressed.
+ */
+ setCanRewindFalse: function gmw_finish() {
+ document.getElementById('migration-wizard').canRewind = false;
+ }
+};
+
+//
+// The more back-end data detection bits
+//
+
+
+/**
+ * A data migrator prototype, holding the information for migration
+ *
+ * @class
+ * @param aTitle The title of the migrator
+ * @param aMigrateFunction The function to call when migrating
+ * @param aArguments The arguments to pass in.
+ */
+function dataMigrator(aTitle, aMigrateFunction, aArguments) {
+ this.title = aTitle;
+ this.migrate = aMigrateFunction;
+ this.args = aArguments || [];
+}
+
+var gDataMigrator = {
+ mIsInFirefox: false,
+ mPlatform: null,
+ mDirService: null,
+ mIoService: null,
+
+ /**
+ * Cached getter for the directory service.
+ */
+ get dirService() {
+ if (!this.mDirService) {
+ this.mDirService = Services.dirsvc;
+ }
+ return this.mDirService;
+ },
+
+ /**
+ * Call to do a general data migration (for a clean profile) Will run
+ * through all of the known migrator-checkers. These checkers will return
+ * an array of valid dataMigrator objects, for each kind of data they find.
+ * If there is at least one valid migrator, we'll pop open the migration
+ * wizard, otherwise, we'll return silently.
+ */
+ checkAndMigrate: function gdm_migrate() {
+ if (Services.appinfo.ID == FIREFOX_UID) {
+ this.mIsInFirefox = true;
+ // We can't handle Firefox Lightning yet
+ migLOG("Holy cow, you're Firefox-Lightning! sorry, can't help.");
+ return;
+ }
+
+ this.mPlatform = Services.appinfo.OS.toLowerCase();
+
+ migLOG("mPlatform is: " + this.mPlatform);
+
+ var DMs = [];
+ var migrators = [this.checkOldCal,
+ this.checkEvolution,
+ this.checkWindowsMail,
+ this.checkIcal];
+ // XXX also define a category and an interface here for pluggability
+ for (var migrator of migrators) {
+ var migs = migrator.call(this);
+ for (var dm of migs) {
+ DMs.push(dm);
+ }
+ }
+
+ if (DMs.length == 0) {
+ // No migration available
+ return;
+ }
+ migLOG("DMs: " + DMs.length);
+
+ var url = "chrome://calendar/content/calendar-migration-dialog.xul";
+ openDialog(url, "migration", "modal,centerscreen,chrome,resizable=no,width=500,height=400", DMs);
+ },
+
+ /**
+ * Checks to see if we can find any traces of an older moz-cal program.
+ * This could be either the old calendar-extension, or Sunbird 0.2. If so,
+ * it offers to move that data into our new storage format.
+ */
+ checkOldCal: function gdm_calold() {
+ migLOG("Checking for the old calendar extension/app");
+
+ // This is the function that the migration wizard will call to actually
+ // migrate the data. It's defined here because we may use it multiple
+ // times (with different aProfileDirs), for instance if there is both
+ // a Thunderbird and Firefox cal-extension
+ function extMigrator(aProfileDir, aCallback) {
+ // Get the old datasource
+ var dataSource = aProfileDir.clone();
+ dataSource.append("CalendarManager.rdf");
+ if (!dataSource.exists()) {
+ return;
+ }
+
+ // Let this be a lesson to anyone designing APIs. The RDF API is so
+ // impossibly confusing that it's actually simpler/cleaner/shorter
+ // to simply parse as XML and use the better DOM APIs.
+ var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Components.interfaces.nsIXMLHttpRequest);
+ req.open('GET', "file://" + dataSource.path, true);
+ req.onreadystatechange = function calext_onreadychange() {
+ if (req.readyState == 4) {
+ migLOG(req.responseText);
+ parseAndMigrate(req.responseXML, aCallback)
+ }
+ };
+ req.send(null);
+ }
+
+ // Callback from the XHR above. Parses CalendarManager.rdf and imports
+ // the data describe therein.
+ function parseAndMigrate(aDoc, aCallback) {
+ // For duplicate detection
+ var calManager = getCalendarManager();
+ var uris = [];
+ for (var oldCal of calManager.getCalendars({})) {
+ uris.push(oldCal.uri);
+ }
+
+ function getRDFAttr(aNode, aAttr) {
+ return aNode.getAttributeNS("http://home.netscape.com/NC-rdf#",
+ aAttr);
+ }
+
+ const RDFNS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+ var nodes = aDoc.getElementsByTagNameNS(RDFNS, "Description");
+ migLOG("nodes: " + nodes.length);
+ for (var i = 0; i < nodes.length; i++) {
+ migLOG("Beginning calendar node");
+ var calendar;
+ var node = nodes[i];
+ if (getRDFAttr(node, "remote") == "false") {
+ migLOG("not remote");
+ var localFile = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ localFile.initWithPath(getRDFAttr(node, "path"));
+ calendar = gDataMigrator.importICSToStorage(localFile);
+ } else {
+ // Remote subscription
+ // XXX check for duplicates
+ var url = makeURL(getRDFAttr(node, "remotePath"));
+ calendar = calManager.createCalendar("ics", url);
+ }
+ calendar.name = getRDFAttr(node, "name");
+ calendar.setProperty("color", getRDFAttr(node, "color"));
+ calManager.registerCalendar(calendar);
+ getCompositeCalendar().addCalendar(calendar);
+ }
+ aCallback();
+ }
+
+ var migrators = [];
+
+ // Look in our current profile directory, in case we're upgrading in
+ // place
+ var profileDir = this.dirService.get("ProfD", Components.interfaces.nsILocalFile);
+ profileDir.append("Calendar");
+ if (profileDir.exists()) {
+ migLOG("Found old extension directory in current app");
+ let title = "Mozilla Calendar Extension";
+ migrators.push(new dataMigrator(title, extMigrator, [profileDir]));
+ }
+
+ // Check the profiles of the various other moz-apps for calendar data
+ var profiles = [];
+
+ // Do they use Firefox?
+ var ffProf, sbProf, tbProf;
+ if ((ffProf = this.getFirefoxProfile())) {
+ profiles.push(ffProf);
+ }
+
+ // We're lightning, check Sunbird
+ if ((sbProf = this.getSunbirdProfile())) {
+ profiles.push(sbProf);
+ }
+
+ // Now check all of the profiles in each of these folders for data
+ for (var prof of profiles) {
+ var dirEnum = prof.directoryEntries;
+ while (dirEnum.hasMoreElements()) {
+ var profile = dirEnum.getNext().QueryInterface(Components.interfaces.nsIFile);
+ if (profile.isFile()) {
+ continue;
+ } else {
+ profile.append("Calendar");
+ if (profile.exists()) {
+ migLOG("Found old extension directory at" + profile.path);
+ var title = "Mozilla Calendar";
+ migrators.push(new dataMigrator(title, extMigrator, [profile]));
+ }
+ }
+ }
+ }
+
+ return migrators;
+ },
+
+ /**
+ * Checks to see if Apple's iCal is installed and offers to migrate any data
+ * the user has created in it.
+ */
+ checkIcal: function gdm_ical() {
+ migLOG("Checking for ical data");
+
+ function icalMigrate(aDataDir, aCallback) {
+ aDataDir.append("Sources");
+ var dirs = aDataDir.directoryEntries;
+ var calManager = getCalendarManager();
+
+ var i = 1;
+ while(dirs.hasMoreElements()) {
+ var dataDir = dirs.getNext().QueryInterface(Components.interfaces.nsIFile);
+ var dataStore = dataDir.clone();
+ dataStore.append("corestorage.ics");
+ if (!dataStore.exists()) {
+ continue;
+ }
+
+ var chars = [];
+ var fileStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+
+ fileStream.init(dataStore, 0x01, parseInt("0444", 8), {});
+ var convStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
+ .getService(Components.interfaces.nsIConverterInputStream);
+ convStream.init(fileStream, 'UTF-8', 0, 0x0000);
+ var tmpStr = {};
+ var str = "";
+ while (convStream.readString(-1, tmpStr)) {
+ str += tmpStr.value;
+ }
+
+ // Strip out the timezone definitions, since it makes the file
+ // invalid otherwise
+ var index = str.indexOf(";TZID=");
+ while (index != -1) {
+ var endIndex = str.indexOf(':', index);
+ var otherEnd = str.indexOf(';', index+2);
+ if (otherEnd < endIndex) {
+ endIndex = otherEnd;
+ }
+ var sub = str.substring(index, endIndex);
+ str = str.split(sub).join("");
+ index = str.indexOf(";TZID=");
+ }
+ var tempFile = gDataMigrator.dirService.get("TmpD", Components.interfaces.nsIFile);
+ tempFile.append("icalTemp.ics");
+ tempFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE,
+ parseInt("0600", 8));
+ var tempUri = Services.io.newFileURI(tempFile);
+
+ var stream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Components.interfaces.nsIFileOutputStream);
+ stream.init(tempFile, 0x2A, parseInt("0600", 8), 0);
+ var convStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ convStream.init(stream, 'UTF-8', 0, 0x0000);
+ convStream.writeString(str);
+
+ var calendar = gDataMigrator.importICSToStorage(tempFile);
+ calendar.name = "iCalendar"+i;
+ i++;
+ calManager.registerCalendar(calendar);
+ getCompositeCalendar().addCalendar(calendar);
+ }
+ migLOG("icalMig making callback");
+ aCallback();
+ }
+ var profileDir = this.dirService.get("ProfD", Components.interfaces.nsILocalFile);
+ var icalSpec = profileDir.path;
+ var diverge = icalSpec.indexOf("Thunderbird");
+ if (diverge == -1) {
+ return [];
+ }
+ icalSpec = icalSpec.substr(0, diverge);
+ var icalFile = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ icalFile.initWithPath(icalSpec);
+ icalFile.append("Application Support");
+
+ icalFile.append("iCal");
+ if (icalFile.exists()) {
+ return [new dataMigrator("Apple iCal", icalMigrate, [icalFile])];
+ }
+
+ return [];
+ },
+
+ /**
+ * Checks to see if Evolution is installed and offers to migrate any data
+ * stored there.
+ */
+ checkEvolution: function gdm_evolution() {
+ function evoMigrate(aDataDir, aCallback) {
+ var i = 1;
+ function evoDataMigrate(dataStore) {
+ migLOG("Migrating evolution data file in " + dataStore.path);
+ if (dataStore.exists()) {
+ var calendar = gDataMigrator.importICSToStorage(dataStore);
+ calendar.name = "Evolution " + (i++);
+ calManager.registerCalendar(calendar);
+ getCompositeCalendar().addCalendar(calendar);
+ }
+ return dataStore.exists();
+ }
+
+ var calManager = getCalendarManager();
+ var dirs = aDataDir.directoryEntries;
+ while (dirs.hasMoreElements()) {
+ var dataDir = dirs.getNext().QueryInterface(Components.interfaces.nsIFile);
+ var dataStore = dataDir.clone();
+ dataStore.append("calendar.ics");
+ evoDataMigrate(dataStore);
+ }
+
+ aCallback();
+ }
+
+ var evoDir = this.dirService.get("Home", Components.interfaces.nsILocalFile);
+ evoDir.append(".evolution");
+ evoDir.append("calendar");
+ evoDir.append("local");
+ return (evoDir.exists() ? [new dataMigrator("Evolution", evoMigrate, [evoDir])] : []);
+ },
+
+ checkWindowsMail: function gdm_windowsMail() {
+ function doMigrate(aCalendarNodes, aMailDir, aCallback) {
+ let calManager = cal.getCalendarManager();
+
+ for (let node of aCalendarNodes) {
+ let name = node.getElementsByTagName("Name")[0].textContent;
+ let color = node.getElementsByTagName("Color")[0].textContent;
+ let enabled = node.getElementsByTagName("Enabled")[0].textContent == "True";
+
+ // The name is quoted, and the color also contains an alpha
+ // value. Lets just ignore the alpha value and take the
+ // color part.
+ name = name.replace(/(^'|'$)/g, "");
+ color = color.replace(/0x[0-9a-fA-F]{2}([0-9a-fA-F]{4})/, "#$1");
+
+ let calfile = aMailDir.clone();
+ calfile.append(name + ".ics");
+
+ if (calfile.exists()) {
+ let storage = gDataMigrator.importICSToStorage(calfile)
+ storage.name = name;
+
+ if (color) {
+ storage.setProperty("color", color);
+ }
+ calManager.registerCalendar(storage);
+
+ if (enabled) {
+ getCompositeCalendar().addCalendar(storage);
+ }
+ }
+ }
+ aCallback();
+ }
+
+ if (!this.dirService.has("LocalAppData")) {
+ // We are probably not on windows
+ return [];
+ }
+
+ let maildir = this.dirService.get("LocalAppData",
+ Components.interfaces.nsILocalFile);
+
+ maildir.append("Microsoft");
+ maildir.append("Windows Calendar");
+ maildir.append("Calendars");
+
+ let settingsxml = maildir.clone();
+ settingsxml.append("Settings.xml");
+
+ let migrators = [];
+ if (settingsxml.exists()) {
+ let settingsXmlUri = Services.io.newFileURI(settingsxml);
+
+ let req = new XMLHttpRequest();
+ req.open("GET", settingsXmlUri.spec, false);
+ req.send(null);
+ if (req.status == 0) {
+ // The file was found, it seems we are on windows vista.
+ let doc = req.responseXML;
+ let root = doc.documentElement;
+
+ // Get all calendar property tags and return the migrator.
+ let calendars = doc.getElementsByTagName("VCalendar");
+ if (calendars.length > 0) {
+ migrators = [new dataMigrator("Windows Calendar", doMigrate.bind(null, calendars, maildir))];
+ }
+ }
+ }
+ return migrators;
+ },
+
+ /**
+ * Creates and registers a storage calendar and imports the given ics file into it.
+ *
+ * @param icsFile The nsI(Local)File to import.
+ */
+ importICSToStorage: function migrateIcsStorage(icsFile) {
+ const uri = 'moz-storage-calendar://';
+ let calendar = cal.getCalendarManager().createCalendar("storage", makeURL(uri));
+ let icsImporter = Components.classes["@mozilla.org/calendar/import;1?type=ics"]
+ .getService(Components.interfaces.calIImporter);
+
+ let inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ let items = [];
+
+ calendar.id = cal.getUUID();
+
+ try {
+ inputStream.init(icsFile, MODE_RDONLY, parseInt("0444", 8), {});
+ items = icsImporter.importFromStream(inputStream, {});
+ } catch(ex) {
+ switch (ex.result) {
+ case Components.interfaces.calIErrors.INVALID_TIMEZONE:
+ showError(calGetString("calendar", "timezoneError", [icsFile.path]));
+ break;
+ default:
+ showError(calGetString("calendar", "unableToRead") + icsFile.path + "\n"+ ex);
+ }
+ } finally {
+ inputStream.close();
+ }
+
+ // Defined in import-export.js
+ putItemsIntoCal(calendar, items, icsFile.leafName);
+
+ return calendar;
+ },
+
+ /**
+ * Helper functions for getting the profile directory of various MozApps
+ * (Getting the profile dir is way harder than it should be.)
+ *
+ * Sunbird:
+ * Unix: ~jdoe/.mozilla/sunbird/
+ * Windows: %APPDATA%\Mozilla\Sunbird\Profiles
+ * Mac OS X: ~jdoe/Library/Application Support/Sunbird/Profiles
+ *
+ * Firefox:
+ * Unix: ~jdoe/.mozilla/firefox/
+ * Windows: %APPDATA%\Mozilla\Firefox\Profiles
+ * Mac OS X: ~jdoe/Library/Application Support/Firefox/Profiles
+ *
+ * Thunderbird:
+ * Unix: ~jdoe/.thunderbird/
+ * Windows: %APPDATA%\Thunderbird\Profiles
+ * Mac OS X: ~jdoe/Library/Thunderbird/Profiles
+ *
+ * Notice that Firefox and Sunbird follow essentially the same pattern, so
+ * we group them with getNormalProfile
+ */
+ getFirefoxProfile: function gdm_getFF() {
+ return this.getNormalProfile("Firefox");
+ },
+
+ /**
+ * @see getFirefoxProfile
+ */
+ getThunderbirdProfile: function gdm_getTB() {
+ let profileRoot = this.dirService.get("DefProfRt", Components.interfaces.nsILocalFile);
+ migLOG("searching for Thunderbird in " + profileRoot.path);
+ return profileRoot.exists() ? profileRoot : null;
+ },
+
+ /**
+ * @see getFirefoxProfile
+ */
+ getSunbirdProfile: function gdm_getSB() {
+ return this.getNormalProfile("Sunbird");
+ },
+
+ /**
+ * Common function to retrieve the profile directory for a given app.
+ * @see getFirefoxProfile
+ */
+ getNormalProfile: function gdm_getNorm(aAppName) {
+ var localFile;
+ var profileRoot = this.dirService.get("DefProfRt", Components.interfaces.nsILocalFile);
+ migLOG("profileRoot = " + profileRoot.path);
+
+ switch (this.mPlatform) {
+ case "winnt":
+ localFile = profileRoot.parent.parent;
+ localFile.append("Mozilla");
+ localFile.append(aAppName);
+ localFile.append("Profiles");
+ break;
+ default: // Unix
+ localFile = profileRoot.parent;
+ localFile.append(".mozilla");
+ localFile.append(aAppName.toLowerCase());
+ break;
+ }
+ migLOG("searching for " + aAppName + " in " + localFile.path);
+ return localFile.exists() ? localFile : null;
+ }
+};
+
+/**
+ * logs to system and error console, depending on the calendar.migration.log
+ * preference.
+ *
+ * XXX Use log4moz instead.
+ *
+ * @param aString The string to log
+ */
+function migLOG(aString) {
+ if (!Preferences.get("calendar.migration.log", false)) {
+ return;
+ }
+ Services.console.logStringMessage(aString);
+ dump(aString+"\n");
+}
diff --git a/calendar/base/content/dialogs/calendar-migration-dialog.xul b/calendar/base/content/dialogs/calendar-migration-dialog.xul
new file mode 100644
index 000000000..467eab05b
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-migration-dialog.xul
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Style sheets -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE dialog
+[
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % migrationDtd SYSTEM "chrome://calendar/locale/migration.dtd">
+ %migrationDtd;
+]>
+
+<wizard id="migration-wizard"
+ title="&migration.title;"
+ windowtype="Calendar:MigrationWizard"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="gMigrateWizard.loadMigrators()"
+ branded="true"
+ persist="screenX screenY">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-migration-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/import-export.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+
+ <wizardpage id="wizardPage1"
+ pageid="initialPage"
+ next="progressPage"
+ label="&migration.welcome;">
+ <label id="wizard-desc" control="datasource-list">&migration.list.description;</label>
+ <listbox id="datasource-list" flex="1">
+ </listbox>
+ </wizardpage>
+
+ <wizardpage id="wizardPage2"
+ pageid="progressPage"
+ label="&migration.importing;"
+ onpageshow="gMigrateWizard.migrateChecked()">
+ <label control="migrate-progressmeter">&migration.progress.description;</label>
+ <vbox flex="1">
+ <progressmeter id="migrate-progressmeter" mode="determined" value="0" />
+ <label value="" flex="1" id="progress-label"/>
+ </vbox>
+ </wizardpage>
+</wizard>
diff --git a/calendar/base/content/dialogs/calendar-occurrence-prompt.xul b/calendar/base/content/dialogs/calendar-occurrence-prompt.xul
new file mode 100644
index 000000000..9aea75baf
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-occurrence-prompt.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar-common/skin/calendar-occurrence-prompt.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://calendar/locale/calendar-occurrence-prompt.dtd">
+
+<dialog id="calendar-occurrence-prompt"
+ buttons="accept,cancel"
+ windowtype="Calendar:OccurrencePrompt"
+ ondialogcancel="return exitOccurrenceDialog(0)"
+ ondialogaccept="exitOccurrenceDialog(1)"
+ onload="onLoad()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xhtml2="http://www.w3.org/TR/xhtml2"
+ xmlns:wairole="http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#"
+ xhtml2:role="wairole:alertdialog">
+ <script type="application/javascript"><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ function exitOccurrenceDialog(aReturnValue) {
+ window.arguments[0].value = aReturnValue;
+ window.close();
+ return true;
+ }
+
+ function getDialogString(key) {
+ return cal.calGetString("calendar-occurrence-prompt", key);
+ }
+
+ function onLoad() {
+ var action = window.arguments[0].action || "edit";
+ var itemType = (cal.isEvent(window.arguments[0].item) ? "event" : "task");
+
+ // Set up title
+ document.title = getDialogString("windowtitle." + itemType + "." + action);
+ document.getElementById("title-label").value = window.arguments[0].item.title;
+
+ // Set up header
+ document.getElementById("isrepeating-label").value =
+ getDialogString("header.isrepeating." + itemType + ".label");
+
+ // Set up buttons
+ document.getElementById("accept-buttons-box")
+ .setAttribute("action", action);
+
+ document.getElementById("accept-occurrence-button").label =
+ getDialogString("buttons.occurrence." + action + ".label");
+
+ document.getElementById("accept-allfollowing-button").label =
+ getDialogString("buttons.allfollowing." + action + ".label");
+ document.getElementById("accept-parent-button").label =
+ getDialogString("buttons.parent." + action + ".label");
+ }
+ ]]></script>
+
+ <vbox id="occurrence-prompt-header" pack="center">
+ <label id="title-label" crop="end"/>
+ <label id="isrepeating-label"/>
+ </vbox>
+
+ <vbox id="accept-buttons-box" flex="1" pack="center">
+ <button id="accept-occurrence-button"
+ default="true"
+ dlgtype="accept"
+ class="occurrence-accept-buttons"
+ accesskey="&buttons.occurrence.accesskey;"
+ oncommand="exitOccurrenceDialog(1)"
+ pack="start"/>
+ <!-- XXXphilipp Button is hidden until all following is implemented -->
+ <button id="accept-allfollowing-button"
+ class="occurrence-accept-buttons"
+ accesskey="&buttons.allfollowing.accesskey;"
+ oncommand="exitOccurrenceDialog(2)"
+ hidden="true"
+ pack="start"/>
+ <button id="accept-parent-button"
+ class="occurrence-accept-buttons"
+ accesskey="&buttons.parent.accesskey;"
+ oncommand="exitOccurrenceDialog(3)"
+ pack="start"/>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-print-dialog.js b/calendar/base/content/dialogs/calendar-print-dialog.js
new file mode 100644
index 000000000..94afeeb0c
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-print-dialog.js
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported loadCalendarPrintDialog, printAndClose, onDatePick */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Gets the calendar view from the opening window
+ */
+function getCalendarView() {
+ let theView = window.opener.currentView();
+ if (!theView.startDay) {
+ theView = null;
+ }
+ return theView;
+}
+
+/**
+ * Loads the print dialog, setting up all needed elements.
+ */
+function loadCalendarPrintDialog() {
+ // set the datepickers to the currently selected dates
+ let theView = getCalendarView();
+ if (theView) {
+ document.getElementById("start-date-picker").value = cal.dateTimeToJsDate(theView.startDay);
+ document.getElementById("end-date-picker").value = cal.dateTimeToJsDate(theView.endDay);
+ } else {
+ document.getElementById("printCurrentViewRadio").setAttribute("disabled", true);
+ }
+ if (!theView || !theView.getSelectedItems({}).length) {
+ document.getElementById("selected").setAttribute("disabled", true);
+ }
+ document.getElementById(theView ? "printCurrentViewRadio" : "custom-range")
+ .setAttribute("selected", true);
+
+ // Get a list of formatters
+ let catman = Components.classes["@mozilla.org/categorymanager;1"]
+ .getService(Components.interfaces.nsICategoryManager);
+ let catenum = catman.enumerateCategory("cal-print-formatters");
+
+ // Walk the list, adding items to the layout menupopup
+ let layoutList = document.getElementById("layout-field");
+ while (catenum.hasMoreElements()) {
+ let entry = catenum.getNext();
+ entry = entry.QueryInterface(Components.interfaces.nsISupportsCString);
+ let contractid = catman.getCategoryEntry("cal-print-formatters", entry);
+ let formatter = Components.classes[contractid]
+ .getService(Components.interfaces.calIPrintFormatter);
+ // Use the contractid as value
+ layoutList.appendItem(formatter.name, contractid);
+ }
+ layoutList.selectedIndex = 0;
+
+ opener.setCursor("auto");
+
+ eventsAndTasksOptions("tasks");
+
+ refreshHtml();
+
+ self.focus();
+}
+
+/**
+ * Retrieves a settings object containing info on what to print. The
+ * receiverFunc will be called with the settings object containing various print
+ * settings.
+ *
+ * @param receiverFunc The callback function to call on completion.
+ */
+function getPrintSettings(receiverFunc) {
+ let tempTitle = document.getElementById("title-field").value;
+ let settings = {};
+ let requiresFetch = true;
+ settings.title = tempTitle || calGetString("calendar", "Untitled");
+ settings.layoutCId = document.getElementById("layout-field").value;
+ settings.start = null;
+ settings.end = null;
+ settings.eventList = [];
+ settings.printEvents = document.getElementById("events").checked;
+ settings.printTasks = document.getElementById("tasks").checked;
+ settings.printCompletedTasks = document.getElementById("completed-tasks").checked;
+ settings.printTasksWithNoDueDate = document.getElementById("tasks-with-no-due-date").checked;
+ let theView = getCalendarView();
+ switch (document.getElementById("view-field").selectedItem.value) {
+ case "currentView":
+ case "": { // just in case
+ settings.start = theView.startDay.clone();
+ settings.end = theView.endDay.clone();
+ settings.end.day += 1;
+ settings.start.isDate = false;
+ settings.end.isDate = false;
+ break;
+ }
+ case "selected": {
+ let selectedItems = theView.getSelectedItems({});
+ settings.eventList = selectedItems.filter((item) => {
+ if (cal.isEvent(item) && !settings.printEvents) {
+ return false;
+ }
+ if (cal.isToDo(item) && !settings.printTasks) {
+ return false;
+ }
+ return true;
+ });
+
+ // If tasks should be printed, also include selected tasks from the
+ // opening window.
+ if (settings.printTasks) {
+ let selectedTasks = window.opener.getSelectedTasks();
+ for (let task of selectedTasks) {
+ settings.eventList.push(task);
+ }
+ }
+
+ // We've set the event list above, no need to fetch items below.
+ requiresFetch = false;
+ break;
+ }
+ case "custom": {
+ // We return the time from the timepickers using the selected
+ // timezone, as not doing so in timezones with a positive offset
+ // from UTC may cause the printout to include the wrong days.
+ let currentTimezone = cal.calendarDefaultTimezone();
+ settings.start = cal.jsDateToDateTime(document.getElementById("start-date-picker").value);
+ settings.start = settings.start.getInTimezone(currentTimezone);
+ settings.end = cal.jsDateToDateTime(document.getElementById("end-date-picker").value);
+ settings.end = settings.end.getInTimezone(currentTimezone);
+ settings.end = settings.end.clone();
+ settings.end.day += 1;
+ break;
+ }
+ default: {
+ dump("Error : no case in printDialog.js::printCalendar()");
+ break;
+ }
+ }
+
+ // Some filters above might have filled the events list themselves. If not,
+ // then fetch the items here.
+ if (requiresFetch) {
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDateTime) {
+ receiverFunc(settings);
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ settings.eventList = settings.eventList.concat(aItems);
+ if (!settings.printTasksWithNoDueDate) {
+ eventWithDueDate = [];
+ for (let item of settings.eventList) {
+ if (item.dueDate || item.endDate) {
+ eventWithDueDate.push(item);
+ }
+ }
+ settings.eventList = eventWithDueDate;
+ }
+ }
+ };
+ let filter = getFilter(settings);
+ if (filter) {
+ window.opener.getCompositeCalendar().getItems(filter, 0, settings.start, settings.end, listener);
+ } else {
+ // No filter means no items, just complete with the empty list set above
+ receiverFunc(settings);
+ }
+ } else {
+ receiverFunc(settings);
+ }
+}
+
+/**
+ * Sets up the filter for a getItems call based on the javascript settings
+ * object
+ *
+ * @param settings The settings data to base upon
+ */
+function getFilter(settings) {
+ let filter = 0;
+ if (settings.printTasks) {
+ filter |= Components.interfaces.calICalendar.ITEM_FILTER_TYPE_TODO;
+ if (settings.printCompletedTasks) {
+ filter |= Components.interfaces.calICalendar.ITEM_FILTER_COMPLETED_ALL;
+ } else {
+ filter |= Components.interfaces.calICalendar.ITEM_FILTER_COMPLETED_NO;
+ }
+ }
+
+ if (settings.printEvents) {
+ filter |= Components.interfaces.calICalendar.ITEM_FILTER_TYPE_EVENT |
+ Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES;
+ }
+ return filter;
+}
+
+/**
+ * Looks at the selections the user has made (start date, layout, etc.), and
+ * updates the HTML in the iframe accordingly. This is also called when a
+ * dialog UI element has changed, since we'll want to refresh the preview.
+ */
+function refreshHtml(finishFunc) {
+ getPrintSettings((settings) => {
+ document.title = calGetString("calendar", "PrintPreviewWindowTitle", [settings.title]);
+
+ let printformatter = Components.classes[settings.layoutCId]
+ .createInstance(Components.interfaces.calIPrintFormatter);
+ let html = "";
+ try {
+ let pipe = Components.classes["@mozilla.org/pipe;1"]
+ .createInstance(Components.interfaces.nsIPipe);
+ const PR_UINT32_MAX = 4294967295; // signals "infinite-length"
+ pipe.init(true, true, 0, PR_UINT32_MAX, null);
+ printformatter.formatToHtml(pipe.outputStream,
+ settings.start,
+ settings.end,
+ settings.eventList.length,
+ settings.eventList,
+ settings.title);
+ pipe.outputStream.close();
+ // convert byte-array to UTF-8 string:
+ let convStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterInputStream);
+ convStream.init(pipe.inputStream, "UTF-8", 0,
+ Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+ try {
+ let portion = {};
+ while (convStream.readString(-1, portion)) {
+ html += portion.value;
+ }
+ } finally {
+ convStream.close();
+ }
+ } catch (e) {
+ Components.utils.reportError("Calendar print dialog:refreshHtml: " + e);
+ }
+
+ let iframeDoc = document.getElementById("content").contentDocument;
+ iframeDoc.documentElement.innerHTML = html;
+ iframeDoc.title = settings.title;
+
+ if (finishFunc) {
+ finishFunc();
+ }
+ }
+);
+}
+
+/**
+ * This is a nsIWebProgressListener that closes the dialog on completion, makes
+ * sure printing works without issues
+ */
+var closeOnComplete = {
+ onStateChange: function(aProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
+ // The request is complete, close the window.
+ document.documentElement.cancelDialog();
+ }
+ },
+
+ onProgressChange: function() {},
+ onLocationChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {}
+};
+
+/**
+ * Prints the document and then closes the window
+ */
+function printAndClose() {
+ refreshHtml(() => {
+ let webBrowserPrint = PrintUtils.getWebBrowserPrint();
+ let printSettings = PrintUtils.getPrintSettings();
+
+ // Evicts "about:blank" header
+ printSettings.docURL = " ";
+
+ // Start the printing, this is just what PrintUtils does, but we
+ // apply our own settings.
+ try {
+ webBrowserPrint.print(printSettings, closeOnComplete);
+ if (gPrintSettingsAreGlobal && gSavePrintSettings) {
+ let PSSVC = Components.classes["@mozilla.org/gfx/printsettings-service;1"]
+ .getService(Components.interfaces.nsIPrintSettingsService);
+ PSSVC.savePrintSettingsToPrefs(printSettings, true,
+ printSettings.kInitSaveAll);
+ PSSVC.savePrintSettingsToPrefs(printSettings, false,
+ printSettings.kInitSavePrinterName);
+ }
+ } catch (e) {
+ // Pressing cancel is expressed as an NS_ERROR_ABORT return value,
+ // causing an exception to be thrown which we catch here.
+ if (e.result != Components.results.NS_ERROR_ABORT) {
+ throw e;
+ }
+ }
+ });
+ return false; // leave open
+}
+
+/**
+ * Called when once a date has been selected in the datepicker.
+ */
+function onDatePick() {
+ calRadioGroupSelectItem("view-field", "custom-range");
+ setTimeout(refreshHtml, 0);
+}
+
+function eventsAndTasksOptions(targetId) {
+ let checkbox = document.getElementById(targetId);
+ let checked = checkbox.getAttribute("checked") == "true";
+ // Workaround to make the checkbox persistent (bug 15232).
+ checkbox.setAttribute("checked", checked ? "true" : "false");
+
+ if (targetId == "tasks") {
+ setElementValue("tasks-with-no-due-date", !checked, "disabled");
+ setElementValue("completed-tasks", !checked, "disabled");
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-print-dialog.xul b/calendar/base/content/dialogs/calendar-print-dialog.xul
new file mode 100644
index 000000000..2e179c7f8
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-print-dialog.xul
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/datetimepickers/datetimepickers.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+]>
+
+
+<dialog id="calendar-new-printwindow"
+ title="&calendar.print.window.title;"
+ windowtype="Calendar:PrintDialog"
+ onload="loadCalendarPrintDialog();"
+ buttons="accept,cancel"
+ buttonlabelaccept="&calendar.print.button.label;"
+ buttonaccesskeyaccept="&calendar.print.button.accesskey;"
+ defaultButton="accept"
+ ondialogaccept="return printAndClose();"
+ ondialogcancel="return true;"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-print-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
+
+ <hbox id="firstHbox" flex="1">
+ <vbox id="groupboxVbox">
+ <groupbox id="settingsGroup">
+ <caption label="&calendar.print.settingsGroup.label;"/>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <label control="title-field"
+ value="&calendar.print.title.label;"/>
+ <textbox id="title-field"
+ class="padded"
+ flex="1"
+ onchange="refreshHtml();"/>
+ </row>
+ <row align="center">
+ <label control="layout-field"
+ value="&calendar.print.layout.label;"/>
+ <hbox>
+ <menulist id="layout-field">
+ <!-- This menupopup will be populated by calendar-print-dialog.js! -->
+ <menupopup id="layout-menulist-menupopup"
+ oncommand="refreshHtml();"/>
+ </menulist>
+ <spacer flex="1"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox id="what-to-print-group">
+ <caption label="&calendar.print.range.label;"/>
+ <grid id="grid-events-and-tasks">
+ <columns id="columns-for-events-and-tasks">
+ <column id="column-event">
+ <checkbox id="events" label="&calendar.print.events.label;" checked="true"
+ oncommand="eventsAndTasksOptions(this.id); refreshHtml();" persist="checked" autocheck="false"/>
+ </column>
+ <column id="column-tasks">
+ <checkbox id="tasks" label="&calendar.print.tasks.label;" checked="true"
+ oncommand="eventsAndTasksOptions(this.id); refreshHtml();" persist="checked" autocheck="false"/>
+ </column>
+ </columns>
+ </grid>
+ <radiogroup id="view-field"
+ oncommand="refreshHtml();">
+ <radio id="printCurrentViewRadio"
+ label="&calendar.print.currentView2.label;"
+ value="currentView"/>
+ <radio id="selected"
+ label="&calendar.print.selectedEventsAndTasks.label;"
+ value="selected"/>
+ <radio id="custom-range"
+ label="&calendar.print.custom.label;"
+ value="custom"/>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <label control="start-date-picker"
+ value="&calendar.print.from.label;"/>
+ <datepicker id="start-date-picker"
+ onchange="onDatePick();"/>
+ </row>
+ <row align="center">
+ <label control="end-date-picker"
+ value="&calendar.print.to.label;"/>
+ <datepicker id="end-date-picker"
+ onchange="onDatePick();"/>
+ </row>
+ </rows>
+ </grid>
+ </radiogroup>
+ </groupbox>
+ <groupbox id="optionsGroup" label="&calendar.print.optionsGroup.label;">
+ <caption label="&calendar.print.optionsGroup.label;"/>
+ <checkbox id="tasks-with-no-due-date" label="&calendar.print.taskswithnoduedate.label;" checked="true" oncommand="refreshHtml();"/>
+ <checkbox id="completed-tasks" label="&calendar.print.completedtasks.label;" checked="true" oncommand="refreshHtml();"/>
+ </groupbox>
+ </vbox>
+
+ <splitter/>
+
+ <iframe src="about:blank"
+ id="content"
+ flex="1"
+ style="border: 2px solid #3c3c3c; width:30em; height:30em; background-color: white;"/>
+ </hbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-properties-dialog.js b/calendar/base/content/dialogs/calendar-properties-dialog.js
new file mode 100644
index 000000000..b52134c71
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-properties-dialog.js
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAcceptDialog, unsubscribeCalendar */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+
+/**
+ * The calendar to modify, is retrieved from window.arguments[0].calendar
+ */
+var gCalendar;
+
+/**
+ * This function gets called when the calendar properties dialog gets opened. To
+ * open the window, use an object as argument. The object needs a 'calendar'
+ * attribute that passes the calendar in question.
+ */
+function onLoad() {
+ gCalendar = window.arguments[0].calendar;
+ let calColor = gCalendar.getProperty("color");
+
+ document.getElementById("calendar-name").value = gCalendar.name;
+ document.getElementById("calendar-color").value = calColor || "#A8C2E1";
+ document.getElementById("calendar-uri").value = gCalendar.uri.spec;
+ document.getElementById("read-only").checked = gCalendar.readOnly;
+
+ // Set up refresh interval
+ initRefreshInterval();
+
+ // Set up the cache field
+ let cacheBox = document.getElementById("cache");
+ let canCache = (gCalendar.getProperty("cache.supported") !== false);
+ let alwaysCache = gCalendar.getProperty("cache.always");
+ if (!canCache || alwaysCache) {
+ cacheBox.setAttribute("disable-capability", "true");
+ cacheBox.hidden = true;
+ cacheBox.disabled = true;
+ }
+ cacheBox.checked = alwaysCache || (canCache && gCalendar.getProperty("cache.enabled"));
+
+ // Set up the show alarms row and checkbox
+ let suppressAlarmsRow = document.getElementById("calendar-suppressAlarms-row");
+ let suppressAlarms = gCalendar.getProperty("suppressAlarms");
+ document.getElementById("fire-alarms").checked = !suppressAlarms;
+
+ suppressAlarmsRow.hidden =
+ (gCalendar.getProperty("capabilities.alarms.popup.supported") === false);
+
+ // Set up the disabled checkbox
+ let calendarDisabled = false;
+ if (gCalendar.getProperty("force-disabled")) {
+ showElement("force-disabled-description");
+ disableElement("calendar-enabled-checkbox");
+ } else {
+ calendarDisabled = gCalendar.getProperty("disabled");
+ document.getElementById("calendar-enabled-checkbox").checked = !calendarDisabled;
+ hideElement(document.documentElement.getButton("extra1"));
+ }
+ setupEnabledCheckbox();
+
+ // start focus on title, unless we are disabled
+ if (!calendarDisabled) {
+ document.getElementById("calendar-name").focus();
+ }
+
+ sizeToContent();
+}
+
+/**
+ * Called when the dialog is accepted, to save settings.
+ *
+ * @return Returns true if the dialog should be closed.
+ */
+function onAcceptDialog() {
+ // Save calendar name
+ gCalendar.name = document.getElementById("calendar-name").value;
+
+ // Save calendar color
+ gCalendar.setProperty("color", document.getElementById("calendar-color").value);
+
+ // Save readonly state
+ gCalendar.readOnly = document.getElementById("read-only").checked;
+
+ // Save supressAlarms
+ gCalendar.setProperty("suppressAlarms", !document.getElementById("fire-alarms").checked);
+
+ // Save refresh interval
+ if (gCalendar.canRefresh) {
+ let value = getElementValue("calendar-refreshInterval-menulist");
+ gCalendar.setProperty("refreshInterval", value);
+ }
+
+ // Save cache options
+ let alwaysCache = gCalendar.getProperty("cache.always");
+ if (!alwaysCache) {
+ gCalendar.setProperty("cache.enabled", document.getElementById("cache").checked);
+ }
+
+ if (!gCalendar.getProperty("force-disabled")) {
+ // Save disabled option (should do this last), remove auto-enabled
+ gCalendar.setProperty("disabled", !document.getElementById("calendar-enabled-checkbox").checked);
+ gCalendar.deleteProperty("auto-enabled");
+ }
+
+ // tell standard dialog stuff to close the dialog
+ return true;
+}
+
+/**
+ * When the calendar is disabled, we need to disable a number of other elements
+ */
+function setupEnabledCheckbox() {
+ let isEnabled = document.getElementById("calendar-enabled-checkbox").checked;
+ let els = document.getElementsByAttribute("disable-with-calendar", "true");
+ for (let i = 0; i < els.length; i++) {
+ els[i].disabled = !isEnabled || (els[i].getAttribute("disable-capability") == "true");
+ }
+}
+
+/**
+ * Called to unsubscribe from a calendar. The button for this function is not
+ * shown unless the provider for the calendar is missing (i.e force-disabled)
+ */
+function unsubscribeCalendar() {
+ let calmgr = cal.getCalendarManager();
+
+ calmgr.unregisterCalendar(gCalendar);
+ window.close();
+}
+
+function initRefreshInterval() {
+ function createMenuItem(minutes) {
+ let menuitem = createXULElement("menuitem");
+ menuitem.setAttribute("value", minutes);
+
+ let everyMinuteString = cal.calGetString("calendar", "calendarPropertiesEveryMinute");
+ let label = PluralForm.get(minutes, everyMinuteString).replace("#1", minutes);
+ menuitem.setAttribute("label", label);
+
+ return menuitem;
+ }
+
+ setBooleanAttribute("calendar-refreshInterval-row", "hidden", !gCalendar.canRefresh);
+
+ if (gCalendar.canRefresh) {
+ let refreshInterval = gCalendar.getProperty("refreshInterval");
+ if (refreshInterval === null) {
+ refreshInterval = 30;
+ }
+
+ let foundValue = false;
+ let separator = document.getElementById("calendar-refreshInterval-manual-separator");
+ let menulist = document.getElementById("calendar-refreshInterval-menulist");
+ for (let min of [1, 5, 15, 30, 60]) {
+ let menuitem = createMenuItem(min);
+
+ separator.parentNode.insertBefore(menuitem, separator);
+ if (refreshInterval == min) {
+ menulist.selectedItem = menuitem;
+ foundValue = true;
+ }
+ }
+
+ if (refreshInterval == 0) {
+ menulist.selectedItem = document.getElementById("calendar-refreshInterval-manual");
+ foundValue = true;
+ }
+
+ if (!foundValue) {
+ // Special menuitem in case the user changed the value in the config editor.
+ let menuitem = createMenuItem(refreshInterval);
+ separator.parentNode.insertBefore(menuitem, separator.nextSibling);
+ menulist.selectedItem = menuitem;
+ }
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-properties-dialog.xul b/calendar/base/content/dialogs/calendar-properties-dialog.xul
new file mode 100644
index 000000000..f48d31a61
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-properties-dialog.xul
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar-common/skin/calendar-properties-dialog.css" type="text/css"?>
+
+<!DOCTYPE dialog
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+]>
+
+<dialog
+ id="calendar-properties-dialog-2"
+ windowtype="Calendar:PropertiesDialog"
+ title="&calendar.server.dialog.title.edit;"
+ buttons="accept,cancel,extra1"
+ buttonlabelextra1="&calendarproperties.unsubscribe.label;"
+ buttonaccesskeyextra1="&calendarproperties.unsubscribe.accesskey;"
+ ondialogextra1="unsubscribeCalendar()"
+ ondialogaccept="return onAcceptDialog();"
+ ondialogcancel="return true;"
+ onload="onLoad()"
+ persist="screenX screenY"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="500">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-properties-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <description id="force-disabled-description" hidden="true">&calendarproperties.forceDisabled.label;</description>
+
+ <checkbox id="calendar-enabled-checkbox"
+ label="&calendarproperties.enabled.label;"
+ oncommand="setupEnabledCheckbox()"/>
+
+ <grid id="calendar-properties-grid">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows id="calendar-properties-rows">
+ <row id="calendar-name-row"
+ align="center">
+ <label value="&calendar.server.dialog.name.label;"
+ disable-with-calendar="true"
+ control="calendar-name"/>
+ <textbox id="calendar-name"
+ flex="1"
+ disable-with-calendar="true"/>
+ </row>
+ <row id="calendar-color-row"
+ align="center">
+ <label value="&calendarproperties.color.label;"
+ disable-with-calendar="true"
+ control="calendar-color"/>
+ <hbox align="center">
+ <html:input id="calendar-color"
+ class="small-margin"
+ type="color"
+ disable-with-calendar="true"/>
+ </hbox>
+ </row>
+ <row id="calendar-uri-row" align="center">
+ <label value="&calendarproperties.location.label;"
+ disable-with-calendar="true"
+ control="calendar-uri"/>
+ <!-- XXX Make location field readonly until Bug 315307 is fixed -->
+ <textbox id="calendar-uri" readonly="true" disable-with-calendar="true"/>
+ </row>
+ <row id="calendar-refreshInterval-row" align="center">
+ <label value="&calendarproperties.refreshInterval.label;"
+ disable-with-calendar="true"
+ control="calendar-refreshInterval-textbox"/>
+ <menulist id="calendar-refreshInterval-menulist"
+ disable-with-calendar="true"
+ label="&calendarproperties.refreshInterval.label;">
+ <menupopup id="calendar-refreshInterval-menupopup">
+ <!-- This will be filled programatically to reduce the number of needed strings -->
+ <menuseparator id="calendar-refreshInterval-manual-separator"/>
+ <menuitem id="calendar-refreshInterval-manual"
+ value="0"
+ label="&calendarproperties.refreshInterval.manual.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="calendar-readOnly-row"
+ align="center">
+ <spacer/>
+ <checkbox id="read-only"
+ label="&calendarproperties.readonly.label;"
+ disable-with-calendar="true"/>
+ </row>
+ <row id="calendar-suppressAlarms-row"
+ align="center">
+ <spacer/>
+ <checkbox id="fire-alarms"
+ label="&calendarproperties.firealarms.label;"
+ disable-with-calendar="true"/>
+ </row>
+ <row id="calendar-cache-row"
+ align="center">
+ <spacer/>
+ <checkbox id="cache"
+ label="&calendarproperties.cache3.label;"
+ disable-with-calendar="true"/>
+ </row>
+ <spacer/>
+ </rows>
+ </grid>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-providerUninstall-dialog.js b/calendar/base/content/dialogs/calendar-providerUninstall-dialog.js
new file mode 100644
index 000000000..2231129c7
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-providerUninstall-dialog.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAccept, onCancel */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+function onLoad() {
+ let extension = window.arguments[0].extension;
+ document.getElementById("provider-name-label").value = extension.name;
+
+ let calendars = cal.getCalendarManager().getCalendars({})
+ .filter(x => x.providerID == extension.id);
+
+ document.getElementById("calendar-list-tree").calendars = calendars;
+}
+
+function onAccept() {
+ // Tell our caller that the extension should be uninstalled.
+ let args = window.arguments[0];
+ args.shouldUninstall = true;
+
+ // Unsubscribe from all selected calendars
+ let calendarList = document.getElementById("calendar-list-tree");
+ let calendars = calendarList.selectedCalendars || [];
+ let calMgr = cal.getCalendarManager();
+ calendars.forEach(calMgr.unregisterCalendar, calMgr);
+
+ return true;
+}
+
+function onCancel() {
+ let args = window.arguments[0];
+ args.shouldUninstall = false;
+
+ return true;
+}
diff --git a/calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul b/calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul
new file mode 100644
index 000000000..bdebaceb7
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-providerUninstall-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/widgets/calendar-widget-bindings.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-management.css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://calendar/locale/provider-uninstall.dtd" >
+
+<dialog id="calendar-provider-uninstall-dialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&providerUninstall.title;"
+ windowtype="Calendar:ProviderUninstall"
+ height="320"
+ width="480"
+ onload="onLoad()"
+ buttonlabelaccept="&providerUninstall.accept.label;"
+ buttonaccesskeyaccept="&providerUninstall.accept.accesskey;"
+ ondialogaccept="return onAccept()"
+ ondialogcancel="return onCancel()">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-providerUninstall-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <description id="pre-name-description">&providerUninstall.preName.label;</description>
+ <label id="provider-name-label"/>
+ <description id="post-name-description">&providerUninstall.postName.label;</description>
+ <description id="reinstall-note-description">&providerUninstall.reinstallNote.label;</description>
+
+ <calendar-list-tree id="calendar-list-tree"
+ hidecolumnpicker="true"
+ ignoredisabledstate="true"
+ flex="1"/>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-subscriptions-dialog.css b/calendar/base/content/dialogs/calendar-subscriptions-dialog.css
new file mode 100644
index 000000000..03ad1cc25
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-subscriptions-dialog.css
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+calendar-subscriptions-richlistbox {
+ -moz-binding: url("chrome://calendar/content/calendar-subscriptions-list.xml#calendar-subscriptions-richlistbox");
+ -moz-user-focus: normal;
+}
+
+calendar-subscriptions-richlistitem {
+ -moz-binding: url("chrome://calendar/content/calendar-subscriptions-list.xml#calendar-subscriptions-richlistitem");
+ -moz-user-focus: normal;
+}
diff --git a/calendar/base/content/dialogs/calendar-subscriptions-dialog.js b/calendar/base/content/dialogs/calendar-subscriptions-dialog.js
new file mode 100644
index 000000000..f1fd6ac61
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-subscriptions-dialog.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onUnload, onKeyPress, onTextBoxKeyPress, onAccept,
+ * onCancel, onSubscribe, onUnsubscribe
+ */
+
+/**
+ * Cancels any pending search operations.
+ */
+var gCurrentSearchOperation = null;
+function cancelPendingSearchOperation() {
+ if (gCurrentSearchOperation && gCurrentSearchOperation.isPending) {
+ gCurrentSearchOperation.cancel(Components.interfaces.calIErrors.OPERATION_CANCELLED);
+ }
+ gCurrentSearchOperation = null;
+}
+
+/**
+ * Sets up the subscriptions dialog.
+ */
+function onLoad() {
+ opener.setCursor("auto");
+}
+
+/**
+ * Cleans up the subscriptions dialog.
+ */
+function onUnload() {
+ cancelPendingSearchOperation();
+}
+
+/**
+ * Handler function to handle dialog keypress events.
+ * (Cancels the search when pressing escape)
+ */
+function onKeyPress(event) {
+ switch (event.keyCode) {
+ case 27: /* ESC */
+ if (gCurrentSearchOperation) {
+ cancelPendingSearchOperation();
+ document.getElementById("status-deck").selectedIndex = 0;
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ break;
+ }
+}
+
+/**
+ * Handler function to handle keypress events in the textbox.
+ * (Starts the search when hitting enter)
+ */
+function onTextBoxKeyPress(event) {
+ switch (event.keyCode) {
+ case 13: /* RET */
+ onSearch();
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ let richListBox = document.getElementById("subscriptions-listbox");
+ let rowCount = richListBox.getRowCount();
+ for (let i = 0; i < rowCount; i++) {
+ let richListItem = richListBox.getItemAtIndex(i);
+ let checked = richListItem.checked;
+ if (checked != richListItem.subscribed) {
+ let calendar = richListItem.calendar;
+ if (checked) {
+ getCalendarManager().registerCalendar(calendar);
+ } else {
+ getCalendarManager().unregisterCalendar(calendar);
+ }
+ }
+ }
+ return true;
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ */
+function onCancel() {
+}
+
+/**
+ * Performs the search for subscriptions, canceling any pending searches.
+ */
+function onSearch() {
+ cancelPendingSearchOperation();
+
+ let richListBox = document.getElementById("subscriptions-listbox");
+ richListBox.clear();
+
+ let registeredCals = {};
+ for (let calendar of getCalendarManager().getCalendars({})) {
+ registeredCals[calendar.id] = true;
+ }
+
+ let opListener = {
+ onResult: function(operation, result) {
+ if (result) {
+ for (let calendar of result) {
+ richListBox.addCalendar(calendar, registeredCals[calendar.id]);
+ }
+ }
+ if (!operation.isPending) {
+ let statusDeck = document.getElementById("status-deck");
+ if (richListBox.getRowCount() > 0) {
+ statusDeck.selectedIndex = 0;
+ } else {
+ statusDeck.selectedIndex = 2;
+ }
+ }
+ }
+ };
+
+ let operation = getCalendarSearchService().searchForCalendars(document.getElementById("search-textbox").value,
+ 0 /* hints */, 50, opListener);
+ if (operation && operation.isPending) {
+ gCurrentSearchOperation = op;
+ document.getElementById("status-deck").selectedIndex = 1;
+ }
+}
+
+/**
+ * Markes the selected item in the subscriptions-listbox for subscribing. The
+ * actual subscribe happens when the window is closed.
+ */
+function onSubscribe() {
+ let item = document.getElementById("subscriptions-listbox").selectedItem;
+ if (item && !item.disabled) {
+ item.checked = true;
+ }
+}
+
+/**
+ * Unmarkes the selected item in the subscriptions-listbox for subscribing. The
+ * actual subscribe happens when the window is closed.
+ */
+function onUnsubscribe() {
+ let item = document.getElementById("subscriptions-listbox").selectedItem;
+ if (item && !item.disabled) {
+ item.checked = false;
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-subscriptions-dialog.xul b/calendar/base/content/dialogs/calendar-subscriptions-dialog.xul
new file mode 100644
index 000000000..02790c650
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-subscriptions-dialog.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/calendar-subscriptions-dialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar-common/skin/calendar-subscriptions-dialog.css" type="text/css"?>
+
+<!DOCTYPE dialog
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar-subscriptions-dialog.dtd" > %dtd1;
+]>
+
+<dialog
+ id="calendar-subscriptions-dialog"
+ title="&calendar.subscriptions.dialog.title;"
+ windowtype="Calendar:SubscriptionsDialog"
+ buttons="accept,cancel"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ onload="return onLoad();"
+ onunload="return onUnload();"
+ onkeypress="onKeyPress(event);"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-subscriptions-dialog.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <vbox flex="1">
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <label value="&calendar.subscriptions.dialog.search.label.value;"
+ crop="end"/>
+ </row>
+ <row>
+ <textbox id="search-textbox" onkeypress="onTextBoxKeyPress(event);"/>
+ <button label="&calendar.subscriptions.dialog.search.button.label;"
+ oncommand="onSearch();"/>
+ </row>
+ <row>
+ <label class="calendar-subscriptions-select-label"
+ value="&calendar.subscriptions.dialog.select.label.value;"
+ crop="end"/>
+ </row>
+ <row flex="1">
+ <calendar-subscriptions-richlistbox id="subscriptions-listbox" flex="1"/>
+ <vbox>
+ <button id="subscribe-button"
+ label="&calendar.subscriptions.dialog.subscribe.button.label;"
+ oncommand="onSubscribe();"/>
+ <button id="unsubscribe-button"
+ label="&calendar.subscriptions.dialog.unsubscribe.button.label;"
+ oncommand="onUnsubscribe();"/>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+ <deck id="status-deck" selectedIndex="0">
+ <hbox class="calendar-subscriptions-status-box">
+ <image class="calendar-subscriptions-status-icon"/>
+ </hbox>
+ <hbox class="calendar-subscriptions-status-box">
+ <image class="calendar-subscriptions-status-icon" busy="true"/>
+ <label value="&calendar.subscriptions.dialog.statusmessage.busy.label;"
+ crop="end"/>
+ </hbox>
+ <hbox class="calendar-subscriptions-status-box">
+ <image class="calendar-subscriptions-status-icon"/>
+ <label value="&calendar.subscriptions.dialog.statusmessage.nomatches.label;"
+ crop="end"/>
+ </hbox>
+ </deck>
+ <separator class="groove"/>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/dialogs/calendar-summary-dialog.js b/calendar/base/content/dialogs/calendar-summary-dialog.js
new file mode 100644
index 000000000..eb94b04db
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.js
@@ -0,0 +1,401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoad, onAccept, onCancel, updatePartStat, browseDocument,
+ * sendMailToOrganizer
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calItipUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+
+/**
+ * Sets up the summary dialog, setting all needed fields on the dialog from the
+ * item received in the window arguments.
+ */
+function onLoad() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ item = item.clone(); // use an own copy of the passed item
+ window.calendarItem = item;
+
+ // the calling entity provides us with an object that is responsible
+ // for recording details about the initiated modification. the 'finalize'-property
+ // is our hook in order to receive a notification in case the operation needs
+ // to be terminated prematurely. this function will be called if the calling
+ // entity needs to immediately terminate the pending modification. in this
+ // case we serialize the item and close the window.
+ if (args.job) {
+ // store the 'finalize'-functor in the provided job-object.
+ args.job.finalize = () => {
+ // store any pending modifications...
+ this.onAccept();
+
+ let calendarItem = window.calendarItem;
+
+ // ...and close the window.
+ window.close();
+
+ return calendarItem;
+ };
+ }
+
+ // set the dialog-id to enable the right window-icon to be loaded.
+ if (cal.isEvent(item)) {
+ setDialogId(document.documentElement, "calendar-event-summary-dialog");
+ } else if (cal.isToDo(item)) {
+ setDialogId(document.documentElement, "calendar-task-summary-dialog");
+ }
+
+ window.attendees = item.getAttendees({});
+
+ let calendar = cal.wrapInstance(item.calendar, Components.interfaces.calISchedulingSupport);
+ window.readOnly = !(isCalendarWritable(calendar) &&
+ (userCanModifyItem(item) ||
+ (calendar &&
+ item.calendar.isInvitation(item) &&
+ userCanRespondToInvitation(item))));
+ if (!window.readOnly && calendar) {
+ let attendee = calendar.getInvitedAttendee(item);
+ if (attendee) {
+ // if this is an unresponded invitation, preset our default alarm values:
+ if (!item.getAlarms({}).length &&
+ (attendee.participationStatus == "NEEDS-ACTION")) {
+ cal.alarms.setDefaultValues(item);
+ }
+
+ window.attendee = attendee.clone();
+ // Since we don't have API to update an attendee in place, remove
+ // and add again. Also, this is needed if the attendee doesn't exist
+ // (i.e REPLY on a mailing list)
+ item.removeAttendee(attendee);
+ item.addAttendee(window.attendee);
+
+ // make partstat NEEDS-ACTION only available as a option to change to,
+ // if the user hasn't ever made a decision prior to opening the dialog
+ let partStat = window.attendee.participationStatus || "NEEDS-ACTION";
+ if (partStat == "NEEDS-ACTION" && cal.isEvent(item)) {
+ document.getElementById("item-participation-needs-action").removeAttribute("hidden");
+ }
+ }
+ }
+
+ document.getElementById("item-title").value = item.title;
+
+ document.getElementById("item-start-row").Item = item;
+ document.getElementById("item-end-row").Item = item;
+
+ updateInvitationStatus();
+
+ // show reminder if this item is *not* readonly.
+ // this case happens for example if this is an invitation.
+ let argCalendar = window.arguments[0].calendarEvent.calendar;
+ let supportsReminders =
+ (argCalendar.getProperty("capabilities.alarms.oninvitations.supported") !== false);
+ if (!window.readOnly && supportsReminders) {
+ document.getElementById("reminder-row").removeAttribute("hidden");
+ loadReminders(window.calendarItem.getAlarms({}));
+ updateReminder();
+ }
+
+ updateRepeatDetails();
+ updateAttendees();
+ updateLink();
+
+ let location = item.getProperty("LOCATION");
+ if (location && location.length) {
+ document.getElementById("location-row").removeAttribute("hidden");
+ document.getElementById("item-location").value = location;
+ }
+
+ let categories = item.getCategories({});
+ if (categories.length > 0) {
+ document.getElementById("category-row").removeAttribute("hidden");
+ document.getElementById("item-category").value = categories.join(", "); // TODO l10n-unfriendly
+ }
+
+ let organizer = item.organizer;
+ if (organizer && organizer.id) {
+ document.getElementById("organizer-row").removeAttribute("hidden");
+ let cell = document.getElementsByClassName("item-organizer-cell")[0];
+ let text = cell.getElementsByTagName("label")[0];
+ let icon = cell.getElementsByTagName("img")[0];
+
+ let role = organizer.role || "REQ-PARTICIPANT";
+ let userType = organizer.userType || "INDIVIDUAL";
+ let partstat = organizer.participationStatus || "NEEDS-ACTION";
+ let orgName = (organizer.commonName && organizer.commonName.length)
+ ? organizer.commonName : organizer.toString();
+ let userTypeString = cal.calGetString("calendar", "dialog.tooltip.attendeeUserType2." + userType,
+ [organizer.toString()]);
+ let roleString = cal.calGetString("calendar", "dialog.tooltip.attendeeRole2." + role,
+ [userTypeString]);
+ let partstatString = cal.calGetString("calendar", "dialog.tooltip.attendeePartStat2." + partstat,
+ [orgName]);
+ let tooltip = cal.calGetString("calendar", "dialog.tooltip.attendee.combined",
+ [roleString, partstatString]);
+
+ text.setAttribute("value", orgName);
+ cell.setAttribute("tooltiptext", tooltip);
+ icon.setAttribute("partstat", partstat);
+ icon.setAttribute("usertype", userType);
+ icon.setAttribute("role", role);
+ }
+
+ let status = item.getProperty("STATUS");
+ if (status && status.length) {
+ let statusRow = document.getElementById("status-row");
+ for (let i = 0; i < statusRow.childNodes.length; i++) {
+ if (statusRow.childNodes[i].getAttribute("status") == status) {
+ statusRow.removeAttribute("hidden");
+ if (status == "CANCELLED" && cal.isToDo(item)) {
+ // There are two labels for CANCELLED, the second one is for
+ // todo items. Increment the counter here.
+ i++;
+ }
+ statusRow.childNodes[i].removeAttribute("hidden");
+ break;
+ }
+ }
+ }
+
+ if (item.hasProperty("DESCRIPTION")) {
+ let description = item.getProperty("DESCRIPTION");
+ if (description && description.length) {
+ document.getElementById("item-description-box")
+ .removeAttribute("hidden");
+ let textbox = document.getElementById("item-description");
+ textbox.value = description;
+ textbox.inputField.readOnly = true;
+ }
+ }
+
+ document.title = item.title;
+
+ let attachments = item.getAttachments({});
+ if (attachments.length) {
+ // we only want to display uri type attachments and no ones received inline with the
+ // invitation message (having a CID: prefix results in about:blank) here
+ let attCounter = 0;
+ attachments.forEach(aAttachment => {
+ if (aAttachment.uri && aAttachment.uri.spec != "about:blank") {
+ let attachment = document.getElementById("attachment-template").cloneNode(true);
+ attachment.removeAttribute("id");
+ attachment.removeAttribute("hidden");
+
+ let label = attachment.getElementsByTagName("label")[0];
+ label.setAttribute("value", aAttachment.uri.spec);
+ label.setAttribute("hashid", aAttachment.hashId);
+
+ let icon = attachment.getElementsByTagName("image")[0];
+ let iconSrc = aAttachment.uri.spec.length ? aAttachment.uri.spec : "dummy.html";
+ icon.setAttribute("src", "moz-icon://" + iconSrc);
+
+ document.getElementById("item-attachment-cell").appendChild(attachment);
+ attCounter++;
+ }
+ });
+ if (attCounter > 0) {
+ document.getElementById("attachments-row").removeAttribute("hidden");
+ }
+ }
+ // If this item is read only we remove the 'cancel' button as users
+ // can't modify anything, thus we go ahead with an 'ok' button only.
+ if (window.readOnly) {
+ document.documentElement.getButton("cancel").setAttribute("collapsed", "true");
+ }
+
+ window.focus();
+ opener.setCursor("auto");
+}
+
+/**
+ * Saves any changed information to the item.
+ *
+ * @return Returns true if the dialog
+ */
+function onAccept() {
+ dispose();
+ if (window.readOnly) {
+ return true;
+ }
+ let args = window.arguments[0];
+ let oldItem = args.calendarEvent;
+ let newItem = window.calendarItem;
+ let calendar = newItem.calendar;
+ saveReminder(newItem);
+ args.onOk(newItem, calendar, oldItem);
+ window.calendarItem = newItem;
+ return true;
+}
+
+/**
+ * Called when closing the dialog and any changes should be thrown away.
+ */
+function onCancel() {
+ dispose();
+ return true;
+}
+
+/**
+ * Sets the dialog's invitation status dropdown to the value specified by the
+ * user's invitation status.
+ */
+function updateInvitationStatus() {
+ if (!window.readOnly) {
+ if (window.attendee) {
+ let invitationRow = document.getElementById("invitation-row");
+ invitationRow.removeAttribute("hidden");
+ let statusElement = document.getElementById("item-participation");
+ statusElement.value = window.attendee.participationStatus || "NEEDS-ACTION";
+ }
+ }
+}
+
+/**
+ * When the summary dialog is showing an invitation, this function updates the
+ * user's invitation status from the value chosen in the dialog.
+ */
+function updatePartStat() {
+ let statusElement = document.getElementById("item-participation");
+ if (window.attendee) {
+ let item = window.arguments[0];
+ let aclEntry = item.calendar.aclEntry;
+ if (aclEntry) {
+ let userAddresses = aclEntry.getUserAddresses({});
+ if (userAddresses.length > 0 &&
+ !cal.attendeeMatchesAddresses(window.attendee, userAddresses)) {
+ window.attendee.setProperty("SENT-BY", "mailto:" + userAddresses[0]);
+ }
+ }
+
+ window.attendee.participationStatus = statusElement.value;
+ }
+}
+
+/**
+ * Updates the dialog w.r.t recurrence, i.e shows a text describing the item's
+ * recurrence)
+ */
+function updateRepeatDetails() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+
+ // step to the parent (in order to show the
+ // recurrence info which is stored at the parent).
+ item = item.parentItem;
+
+ // retrieve a valid recurrence rule from the currently
+ // set recurrence info. bail out if there's more
+ // than a single rule or something other than a rule.
+ let recurrenceInfo = item.recurrenceInfo;
+ if (!recurrenceInfo) {
+ return;
+ }
+
+ document.getElementById("repeat-row").removeAttribute("hidden");
+
+ // First of all collapse the details text. If we fail to
+ // create a details string, we simply don't show anything.
+ // this could happen if the repeat rule is something exotic
+ // we don't have any strings prepared for.
+ let repeatDetails = document.getElementById("repeat-details");
+ repeatDetails.setAttribute("collapsed", "true");
+
+ // Try to create a descriptive string from the rule(s).
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let startDate = item.startDate || item.entryDate;
+ let endDate = item.endDate || item.dueDate;
+ startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null;
+ endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null;
+ let detailsString = recurrenceRule2String(recurrenceInfo, startDate,
+ endDate, startDate.isDate);
+
+ if (!detailsString) {
+ detailsString = cal.calGetString("calendar-event-dialog", "ruleTooComplexSummary");
+ }
+
+ // Now display the string...
+ let lines = detailsString.split("\n");
+ repeatDetails.removeAttribute("collapsed");
+ while (repeatDetails.childNodes.length > lines.length) {
+ repeatDetails.lastChild.remove();
+ }
+ let numChilds = repeatDetails.childNodes.length;
+ for (let i = 0; i < lines.length; i++) {
+ if (i >= numChilds) {
+ let newNode = repeatDetails.firstChild
+ .cloneNode(true);
+ repeatDetails.appendChild(newNode);
+ }
+ repeatDetails.childNodes[i].value = lines[i];
+ repeatDetails.childNodes[i].setAttribute("tooltiptext", detailsString);
+ }
+}
+
+/**
+ * Updates the attendee listbox, displaying all attendees invited to the
+ * window's item.
+ */
+function updateAttendees() {
+ if (window.attendees && window.attendees.length) {
+ document.getElementById("item-attendees").removeAttribute("hidden");
+ setupAttendees();
+ }
+}
+
+/**
+ * Updates the reminder, called when a reminder has been selected in the
+ * menulist.
+ */
+function updateReminder() {
+ commonUpdateReminder();
+}
+
+/**
+ * Browse the item's attached URL.
+ *
+ * XXX This function is broken, should be fixed in bug 471967
+ */
+function browseDocument() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ let url = item.getProperty("URL");
+ launchBrowser(url);
+}
+
+/**
+ * Extracts the item's organizer and opens a compose window to send the
+ * organizer an email.
+ */
+function sendMailToOrganizer() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ let organizer = item.organizer;
+ let email = cal.getAttendeeEmail(organizer, true);
+ let emailSubject = cal.calGetString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+ let identity = item.calendar.getProperty("imip.identity");
+ sendMailTo(email, emailSubject, null, identity);
+}
+
+/**
+ * Opens an attachment
+ *
+ * @param {AUTF8String} aAttachmentId The hashId of the attachment to open
+ */
+function openAttachment(aAttachmentId) {
+ if (!aAttachmentId) {
+ return;
+ }
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ let attachments = item.getAttachments({})
+ .filter(aAttachment => aAttachment.hashId == aAttachmentId);
+ if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") {
+ let externalLoader = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Components.interfaces.nsIExternalProtocolService);
+ externalLoader.loadUrl(attachments[0].uri);
+ }
+}
diff --git a/calendar/base/content/dialogs/calendar-summary-dialog.xul b/calendar/base/content/dialogs/calendar-summary-dialog.xul
new file mode 100644
index 000000000..734a6ad8a
--- /dev/null
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.xul
@@ -0,0 +1,300 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/dialogs/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-bindings.css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd" >
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" >
+ <!ENTITY % dialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" >
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %globalDTD;
+ %calendarDTD;
+ %dialogDTD;
+ %brandDTD;
+]>
+
+<!-- Dialog id is changed during excution to allow different Window-icons
+ on this dialog. document.loadOverlay() will not work on this one. -->
+<dialog id="calendar-summary-dialog"
+ windowtype="Calendar:EventSummaryDialog"
+ onload="onLoad()"
+ ondialogaccept="return onAccept();"
+ ondialogcancel="return onCancel();"
+ onresize="rearrangeAttendees();"
+ persist="screenX screenY width height"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-summary-dialog.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-item-editing.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calApplicationUtils.js"/>
+
+ <!-- General -->
+ <box id="item-general-box" orient="vertical">
+ <calendar-caption label="&read.only.general.label;"/>
+ <box orient="horizontal">
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="top">
+ <label value="&read.only.title.label;"/>
+ <label id="item-title" crop="end"/>
+ </row>
+ <row class="item-date-row"
+ id="item-start-row"
+ mode="start"
+ taskStartLabel="&read.only.task.start.label;"
+ eventStartLabel="&read.only.event.start.label;"
+ align="start"/>
+ <row class="item-date-row"
+ id="item-end-row"
+ mode="end"
+ taskDueLabel="&read.only.task.due.label;"
+ eventEndLabel="&read.only.event.end.label;"
+ align="start"/>
+ <row id="repeat-row" align="top" hidden="true">
+ <label value="&read.only.repeat.label;"/>
+ <box id="repeat-details" orient="vertical">
+ <label/>
+ </box>
+ </row>
+ <row id="location-row" align="top" hidden="true">
+ <label value="&read.only.location.label;"/>
+ <label id="item-location" crop="end"/>
+ </row>
+ <row id="category-row" align="top" hidden="true">
+ <label value="&read.only.category.label;"/>
+ <label id="item-category" crop="end"/>
+ </row>
+ <row id="organizer-row" align="top" hidden="true" class="item-attendees-row">
+ <label value="&read.only.organizer.label;"/>
+ <hbox class="item-organizer-cell">
+ <img class="itip-icon"/>
+ <label id="item-organizer"
+ class="text-link item-attendees-cell-label"
+ crop="end"
+ onclick="sendMailToOrganizer()"/>
+ <spacer flex="1"/>
+ </hbox>
+ </row>
+ <row id="status-row" align="top" hidden="true">
+ <label value="&task.status.label;"/>
+ <label value="&newevent.status.tentative.label;" hidden="true" status="TENTATIVE"/>
+ <label value="&newevent.status.confirmed.label;" hidden="true" status="CONFIRMED"/>
+ <label value="&newevent.eventStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
+ <label value="&newevent.todoStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
+ <label value="&newevent.status.needsaction.label;" hidden="true" status="NEEDS-ACTION"/>
+ <label value="&newevent.status.inprogress.label;" hidden="true" status="IN-PROCESS"/>
+ <label value="&newevent.status.completed.label;" hidden="true" status="COMPLETED"/>
+ </row>
+ <separator id="item-main-separator" flex="1" class="groove" hidden="true"/>
+ <row id="invitation-row" hidden="true" align="center">
+ <label value="&read.only.reply.label;" control="item-participation"/>
+ <hbox pack="start">
+ <menulist id="item-participation" oncommand="updatePartStat()">
+ <menupopup>
+ <menuitem label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem label="&read.only.needs.action.label;" value="NEEDS-ACTION"
+ hidden="true" id="item-participation-needs-action"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </row>
+ <row id="reminder-row" hidden="true" align="center">
+ <label value="&read.only.reminder.label;" control="item-alarm"/>
+ <hbox id="event-grid-alarm-picker-box"
+ align="center">
+ <menulist id="item-alarm"
+ disable-on-readonly="true"
+ oncommand="updateReminder()">
+ <menupopup id="item-alarm-menupopup">
+ <menuitem id="reminder-none-menuitem"
+ label="&event.reminder.none.label;"
+ selected="true"
+ value="none"/>
+ <menuseparator id="reminder-none-separator"/>
+ <menuitem id="reminder-0minutes-menuitem"
+ label="&event.reminder.0minutes.before.label;"
+ length="0"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-5minutes-menuitem"
+ label="&event.reminder.5minutes.before.label;"
+ length="5"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-15minutes-menuitem"
+ label="&event.reminder.15minutes.before.label;"
+ length="15"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-30minutes-menuitem"
+ label="&event.reminder.30minutes.before.label;"
+ length="30"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuseparator id="reminder-minutes-separator"/>
+ <menuitem id="reminder-1hour-menuitem"
+ label="&event.reminder.1hour.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuitem id="reminder-2hours-menuitem"
+ label="&event.reminder.2hours.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuitem id="reminder-12hours-menuitem"
+ label="&event.reminder.12hours.before.label;"
+ length="12"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuseparator id="reminder-hours-separator"/>
+ <menuitem id="reminder-1day-menuitem"
+ label="&event.reminder.1day.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuitem id="reminder-2days-menuitem"
+ label="&event.reminder.2days.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuitem id="reminder-1week-menuitem"
+ label="&event.reminder.1week.before.label;"
+ length="7"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuseparator id="reminder-custom-separator"/>
+ <menuitem id="reminder-custom-menuitem"
+ label="&event.reminder.custom.label;"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ <hbox id="reminder-details">
+ <hbox id="reminder-icon-box"
+ class="alarm-icons-box"
+ align="center"/>
+ <!-- TODO oncommand? onkeypress? -->
+ <label id="reminder-multiple-alarms-label"
+ hidden="true"
+ value="&event.reminder.multiple.label;"
+ class="text-link"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"/>
+ <label id="reminder-single-alarms-label"
+ hidden="true"
+ class="text-link"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"/>
+ </hbox>
+ </hbox>
+ </row>
+ <row id="attachments-row"
+ align="top"
+ hidden="true"
+ class="item-attachments-row">
+ <label value="&read.only.attachments.label;"
+ control="item-attachment-cell" />
+ <vbox id="item-attachment-cell">
+ <!-- attachment box template -->
+ <hbox id="attachment-template"
+ hidden="true"
+ align="center"
+ disable-on-readonly="true">
+ <image class="attachment-icon"/>
+ <label class="text-link item-attachment-cell-label"
+ onclick="openAttachment(this.getAttribute('hashid'), event)"
+ crop="end"
+ flex="1" />
+ </hbox>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+ </box>
+ </box>
+
+ <!-- attendee box template -->
+ <vbox id="item-attendees-box-template">
+ <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
+ <box class="item-attendees-cell" hidden="true" flex="1">
+ <img class="itip-icon"/>
+ <label class="item-attendees-cell-label" crop="end" flex="1"/>
+ </box>
+ <box hidden="true" flex="1"/>
+ </hbox>
+ </vbox>
+
+ <!-- Attendees -->
+ <box id="item-attendees" orient="vertical" hidden="true" flex="1">
+ <spacer class="default-spacer"/>
+ <calendar-caption label="&read.only.attendees.label;"
+ control="item-attendees-box"/>
+ <vbox id="item-attendees-box" flex="1" />
+ </box>
+
+ <!-- Description -->
+ <box id="item-description-box" hidden="true" orient="vertical" flex="1">
+ <spacer class="default-spacer"/>
+ <calendar-caption label="&read.only.description.label;"
+ control="item-description"/>
+ <box orient="horizontal" flex="1">
+ <textbox id="item-description"
+ multiline="true"
+ rows="6"
+ flex="1"/>
+ </box>
+ </box>
+
+ <!-- URL link -->
+ <box id="event-grid-link-row" hidden="true" orient="vertical">
+ <spacer class="default-spacer"/>
+ <calendar-caption label="&read.only.link.label;"
+ control="url-link"/>
+ <label id="url-link"
+ class="text-link default-indent"
+ onclick="launchBrowser(this.getAttribute('href'), event)"
+ oncommand="launchBrowser(this.getAttribute('href'), event)"
+ crop="end"/>
+ </box>
+
+</dialog>
diff --git a/calendar/base/content/dialogs/chooseCalendarDialog.xul b/calendar/base/content/dialogs/chooseCalendarDialog.xul
new file mode 100644
index 000000000..1fb85eade
--- /dev/null
+++ b/calendar/base/content/dialogs/chooseCalendarDialog.xul
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!-- DTD File with all strings specific to the file -->
+<!DOCTYPE page
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd1;
+]>
+
+<dialog id="chooseCalendar"
+ title="&calendar.select.dialog.title;"
+ windowtype="Calendar:CalendarPicker"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="accept,cancel"
+ onload="setTimeout('loadCalendars()',0);"
+ ondialogaccept="return doOK();"
+ persist="screenX screenY height width">
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"><![CDATA[
+ function loadCalendars() {
+ const calendarManager = Components.classes["@mozilla.org/calendar/manager;1"]
+ .getService(Components.interfaces.calICalendarManager);
+ var listbox = document.getElementById("calendar-list");
+ var composite = window.opener.getCompositeCalendar();
+ var selectedIndex = 0;
+ var calendars;
+
+ if (window.arguments[0].calendars) {
+ calendars = window.arguments[0].calendars;
+ } else {
+ calendars = calendarManager.getCalendars({});
+ }
+ calendars = sortCalendarArray(calendars);
+
+ for (var i = 0; i < calendars.length; i++) {
+ var calendar = calendars[i];
+ var listItem = document.createElement("listitem");
+
+ var colorCell = document.createElement("listcell");
+ try {
+ var calColor = calendar.getProperty('color');
+ colorCell.style.background = calColor || "#a8c2e1";
+ } catch(e) {}
+ listItem.appendChild(colorCell);
+
+ var nameCell = document.createElement("listcell");
+ nameCell.setAttribute("label", calendar.name);
+ listItem.appendChild(nameCell);
+
+ listItem.calendar = calendar;
+ listbox.appendChild(listItem);
+ listItem.setAttribute("flex","1");
+
+ // Select the default calendar of the opening calendar window.
+ if (calendar.id == composite.defaultCalendar.id) {
+ selectedIndex = i;
+ }
+ }
+ document.getElementById("prompt").textContent = window.arguments[0].promptText;
+
+ if (calendars.length) {
+ listbox.ensureIndexIsVisible(selectedIndex);
+ var selItem = listbox.getItemAtIndex(selectedIndex);
+ listbox.timedSelect(selItem, 0);
+ } else {
+ // If there are no calendars, then disable the accept button
+ document.documentElement.getButton("accept").setAttribute("disabled", "true");
+ }
+ }
+
+ function doOK() {
+ var listbox = document.getElementById("calendar-list");
+ window.arguments[0].onOk(listbox.selectedItem.calendar);
+ }
+ ]]></script>
+
+ <vbox id="dialog-box" flex="1">
+ <label id="prompt" control="calendar-list"/>
+ <listbox id="calendar-list" rows="5" flex="1" seltype="single">
+ <listcols>
+ <listcol/>
+ <listcol flex="1"/>
+ </listcols>
+ </listbox>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/import-export.js b/calendar/base/content/import-export.js
new file mode 100644
index 000000000..c4bd1d54c
--- /dev/null
+++ b/calendar/base/content/import-export.js
@@ -0,0 +1,358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/* exported loadEventsFromFile, exportEntireCalendar */
+
+// File constants copied from file-utils.js
+var MODE_RDONLY = 0x01;
+var MODE_WRONLY = 0x02;
+var MODE_CREATE = 0x08;
+var MODE_TRUNCATE = 0x20;
+
+/**
+ * Shows a file dialog, reads the selected file(s) and tries to parse events from it.
+ *
+ * @param aCalendar (optional) If specified, the items will be imported directly
+ * into the calendar
+ */
+function loadEventsFromFile(aCalendar) {
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+
+ let picker = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ picker.init(window,
+ calGetString("calendar", "filepickerTitleImport"),
+ nsIFilePicker.modeOpen);
+ picker.defaultExtension = "ics";
+
+ // Get a list of importers
+ let contractids = [];
+ let catman = Components.classes["@mozilla.org/categorymanager;1"]
+ .getService(Components.interfaces.nsICategoryManager);
+ let catenum = catman.enumerateCategory("cal-importers");
+ let currentListLength = 0;
+ let defaultCIDIndex = 0;
+ while (catenum.hasMoreElements()) {
+ let entry = catenum.getNext();
+ entry = entry.QueryInterface(Components.interfaces.nsISupportsCString);
+ let contractid = catman.getCategoryEntry("cal-importers", entry);
+ let importer;
+ try {
+ importer = Components.classes[contractid]
+ .getService(Components.interfaces.calIImporter);
+ } catch (e) {
+ cal.WARN("Could not initialize importer: " + contractid + "\nError: " + e);
+ continue;
+ }
+ let types = importer.getFileTypes({});
+ for (let type of types) {
+ picker.appendFilter(type.description, type.extensionFilter);
+ if (type.extensionFilter == "*." + picker.defaultExtension) {
+ picker.filterIndex = currentListLength;
+ defaultCIDIndex = currentListLength;
+ }
+ contractids.push(contractid);
+ currentListLength++;
+ }
+ }
+
+ let rv = picker.show();
+
+ if (rv != nsIFilePicker.returnCancel &&
+ picker.file && picker.file.path && picker.file.path.length > 0) {
+ let filterIndex = picker.filterIndex;
+ if (picker.filterIndex < 0 || picker.filterIndex > contractids.length) {
+ // For some reason the wrong filter was selected, assume default extension
+ filterIndex = defaultCIDIndex;
+ }
+
+ let filePath = picker.file.path;
+ let importer = Components.classes[contractids[filterIndex]]
+ .getService(Components.interfaces.calIImporter);
+
+ const nsIFileInputStream = Components.interfaces.nsIFileInputStream;
+
+ let inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(nsIFileInputStream);
+ let items = [];
+ let exception;
+
+ try {
+ inputStream.init(picker.file, MODE_RDONLY, parseInt("0444", 8), {});
+ items = importer.importFromStream(inputStream, {});
+ } catch (ex) {
+ exception = ex;
+ switch (ex.result) {
+ case Components.interfaces.calIErrors.INVALID_TIMEZONE:
+ showError(cal.calGetString("calendar", "timezoneError", [filePath]));
+ break;
+ default:
+ showError(cal.calGetString("calendar", "unableToRead") + filePath + "\n" + ex);
+ }
+ } finally {
+ inputStream.close();
+ }
+
+ if (!items.length && !exception) {
+ // the ics did not contain any events, so there's no need to proceed. But we should
+ // notify the user about it, if we haven't before.
+ showError(cal.calGetString("calendar", "noItemsInCalendarFile", [filePath]));
+ return;
+ }
+
+ if (aCalendar) {
+ putItemsIntoCal(aCalendar, items);
+ return;
+ }
+
+ let calendars = cal.getCalendarManager().getCalendars({});
+ calendars = calendars.filter(isCalendarWritable);
+
+ if (calendars.length < 1) {
+ // XXX alert something?
+ return;
+ } else if (calendars.length == 1) {
+ // There's only one calendar, so it's silly to ask what calendar
+ // the user wants to import into.
+ putItemsIntoCal(calendars[0], items, filePath);
+ } else {
+ // Ask what calendar to import into
+ let args = {};
+ args.onOk = (aCal) => { putItemsIntoCal(aCal, items, filePath); };
+ args.calendars = calendars;
+ args.promptText = calGetString("calendar", "importPrompt");
+ openDialog("chrome://calendar/content/chooseCalendarDialog.xul",
+ "_blank", "chrome,titlebar,modal,resizable", args);
+ }
+ }
+}
+
+/**
+ * Put items into a certain calendar, catching errors and showing them to the
+ * user.
+ *
+ * @param destCal The destination calendar.
+ * @param aItems An array of items to put into the calendar.
+ * @param aFilePath The original file path, for error messages.
+ */
+function putItemsIntoCal(destCal, aItems, aFilePath) {
+ // Set batch for the undo/redo transaction manager
+ startBatchTransaction();
+
+ // And set batch mode on the calendar, to tell the views to not
+ // redraw until all items are imported
+ destCal.startBatch();
+
+ // This listener is needed to find out when the last addItem really
+ // finished. Using a counter to find the last item (which might not
+ // be the last item added)
+ let count = 0;
+ let failedCount = 0;
+ let duplicateCount = 0;
+ // Used to store the last error. Only the last error, because we don't
+ // wan't to bomb the user with thousands of error messages in case
+ // something went really wrong.
+ // (example of something very wrong: importing the same file twice.
+ // quite easy to trigger, so we really should do this)
+ let lastError;
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ count++;
+ if (!Components.isSuccessCode(aStatus)) {
+ if (aStatus == Components.interfaces.calIErrors.DUPLICATE_ID) {
+ duplicateCount++;
+ } else {
+ failedCount++;
+ lastError = aStatus;
+ }
+ }
+ // See if it is time to end the calendar's batch.
+ if (count == aItems.length) {
+ destCal.endBatch();
+ if (!failedCount && duplicateCount) {
+ showError(calGetString("calendar", "duplicateError", [duplicateCount, aFilePath]));
+ } else if (failedCount) {
+ showError(calGetString("calendar", "importItemsFailed", [failedCount, lastError.toString()]));
+ }
+ }
+ }
+ };
+
+ for (let item of aItems) {
+ // XXX prompt when finding a duplicate.
+ try {
+ destCal.addItem(item, listener);
+ } catch (e) {
+ failedCount++;
+ lastError = e;
+ // Call the listener's operationComplete, to increase the
+ // counter and not miss failed items. Otherwise, endBatch might
+ // never be called.
+ listener.onOperationComplete(null, null, null, null, null);
+ Components.utils.reportError("Import error: " + e);
+ }
+ }
+
+ // End transmgr batch
+ endBatchTransaction();
+}
+
+/**
+ * Save data to a file. Create the file or overwrite an existing file.
+ *
+ * @param calendarEventArray (required) Array of calendar events that should
+ * be saved to file.
+ * @param aDefaultFileName (optional) Initial filename shown in SaveAs dialog.
+ */
+function saveEventsToFile(calendarEventArray, aDefaultFileName) {
+ if (!calendarEventArray || !calendarEventArray.length) {
+ return;
+ }
+
+ // Show the 'Save As' dialog and ask for a filename to save to
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+
+ let picker = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+
+ picker.init(window,
+ calGetString("calendar", "filepickerTitleExport"),
+ nsIFilePicker.modeSave);
+
+ if (aDefaultFileName && aDefaultFileName.length && aDefaultFileName.length > 0) {
+ picker.defaultString = aDefaultFileName;
+ } else if (calendarEventArray.length == 1 && calendarEventArray[0].title) {
+ picker.defaultString = calendarEventArray[0].title;
+ } else {
+ picker.defaultString = calGetString("calendar", "defaultFileName");
+ }
+
+ picker.defaultExtension = "ics";
+
+ // Get a list of exporters
+ let contractids = [];
+ let catman = Components.classes["@mozilla.org/categorymanager;1"]
+ .getService(Components.interfaces.nsICategoryManager);
+ let catenum = catman.enumerateCategory("cal-exporters");
+ let currentListLength = 0;
+ let defaultCIDIndex = 0;
+ while (catenum.hasMoreElements()) {
+ let entry = catenum.getNext();
+ entry = entry.QueryInterface(Components.interfaces.nsISupportsCString);
+ let contractid = catman.getCategoryEntry("cal-exporters", entry);
+ let exporter;
+ try {
+ exporter = Components.classes[contractid]
+ .getService(Components.interfaces.calIExporter);
+ } catch (e) {
+ cal.WARN("Could not initialize exporter: " + contractid + "\nError: " + e);
+ continue;
+ }
+ let types = exporter.getFileTypes({});
+ for (let type of types) {
+ picker.appendFilter(type.description, type.extensionFilter);
+ if (type.extensionFilter == "*." + picker.defaultExtension) {
+ picker.filterIndex = currentListLength;
+ defaultCIDIndex = currentListLength;
+ }
+ contractids.push(contractid);
+ currentListLength++;
+ }
+ }
+
+ let rv = picker.show();
+
+ // Now find out as what to save, convert the events and save to file.
+ if (rv != nsIFilePicker.returnCancel && picker.file && picker.file.path.length > 0) {
+ let filterIndex = picker.filterIndex;
+ if (picker.filterIndex < 0 || picker.filterIndex > contractids.length) {
+ // For some reason the wrong filter was selected, assume default extension
+ filterIndex = defaultCIDIndex;
+ }
+
+ let exporter = Components.classes[contractids[filterIndex]]
+ .getService(Components.interfaces.calIExporter);
+
+ let filePath = picker.file.path;
+ if (!filePath.includes(".")) {
+ filePath += "." + exporter.getFileTypes({})[0].defaultExtension;
+ }
+
+ const nsILocalFile = Components.interfaces.nsILocalFile;
+ const nsIFileOutputStream = Components.interfaces.nsIFileOutputStream;
+
+ let outputStream;
+ let localFileInstance = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(nsILocalFile);
+ localFileInstance.initWithPath(filePath);
+
+ outputStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(nsIFileOutputStream);
+ try {
+ outputStream.init(localFileInstance,
+ MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE,
+ parseInt("0664", 8),
+ 0);
+
+ // XXX Do the right thing with unicode and stuff. Or, again, should the
+ // exporter handle that?
+ exporter.exportToStream(outputStream,
+ calendarEventArray.length,
+ calendarEventArray,
+ null);
+ outputStream.close();
+ } catch (ex) {
+ showError(calGetString("calendar", "unableToWrite") + filePath);
+ }
+ }
+}
+
+/**
+ * Exports all the events and tasks in a calendar. If aCalendar is not specified,
+ * the user will be prompted with a list of calendars to choose which one to export.
+ *
+ * @param aCalendar (optional) A specific calendar to export
+ */
+function exportEntireCalendar(aCalendar) {
+ let itemArray = [];
+ let getListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aOpCalendar, aStatus, aOperationType, aId, aDetail) {
+ saveEventsToFile(itemArray, aOpCalendar.name);
+ },
+ onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ itemArray.push(item);
+ }
+ }
+ };
+
+ let getItemsFromCal = function(aCal) {
+ aCal.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0, null, null, getListener);
+ };
+
+ if (aCalendar) {
+ getItemsFromCal(aCalendar);
+ } else {
+ let count = {};
+ let calendars = getCalendarManager().getCalendars(count);
+
+ if (count.value == 1) {
+ // There's only one calendar, so it's silly to ask what calendar
+ // the user wants to import into.
+ getItemsFromCal(calendars[0]);
+ } else {
+ // Ask what calendar to import into
+ let args = {};
+ args.onOk = getItemsFromCal;
+ args.promptText = calGetString("calendar", "exportPrompt");
+ openDialog("chrome://calendar/content/chooseCalendarDialog.xul",
+ "_blank", "chrome,titlebar,modal,resizable", args);
+ }
+ }
+}
diff --git a/calendar/base/content/preferences/alarms.js b/calendar/base/content/preferences/alarms.js
new file mode 100644
index 000000000..aef659636
--- /dev/null
+++ b/calendar/base/content/preferences/alarms.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gAlarmsPane */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Global Object to hold methods for the alarms pref pane
+ */
+var gAlarmsPane = {
+ /**
+ * Initialize the alarms pref pane. Sets up dialog controls to match the
+ * values set in prefs.
+ */
+ init: function() {
+ // Enable/disable the alarm sound URL box and buttons
+ this.alarmsPlaySoundPrefChanged();
+
+ // Set the correct singular/plural for the time units
+ updateMenuLabelsPlural("eventdefalarmlen", "eventdefalarmunit");
+ updateMenuLabelsPlural("tododefalarmlen", "tododefalarmunit");
+ updateUnitLabelPlural("defaultsnoozelength", "defaultsnoozelengthunit", "minutes");
+ },
+
+ /**
+ * Converts the given file url to a nsILocalFile
+ *
+ * @param aFileURL A string with a file:// url.
+ * @return The corresponding nsILocalFile.
+ */
+ convertURLToLocalFile: function(aFileURL) {
+ // Convert the file url into a nsILocalFile
+ if (aFileURL) {
+ let fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+ return fph.getFileFromURLSpec(aFileURL);
+ } else {
+ return null;
+ }
+ },
+
+ /**
+ * Handler function to be called when the calendar.alarms.soundURL pref has
+ * changed. Updates the label in the dialog.
+ */
+ readSoundLocation: function() {
+ let soundUrl = document.getElementById("alarmSoundFileField");
+ soundUrl.value = document.getElementById("calendar.alarms.soundURL").value;
+ if (soundUrl.value.startsWith("file://")) {
+ soundUrl.label = this.convertURLToLocalFile(soundUrl.value).leafName;
+ } else {
+ soundUrl.label = soundUrl.value;
+ }
+ soundUrl.image = "moz-icon://" + soundUrl.label + "?size=16";
+ return undefined;
+ },
+
+ /**
+ * Causes the default sound to be selected in the dialog controls
+ */
+ useDefaultSound: function() {
+ let defaultSoundUrl = "chrome://calendar/content/sound.wav";
+ document.getElementById("calendar.alarms.soundURL").value = defaultSoundUrl;
+ document.getElementById("alarmSoundCheckbox").checked = true;
+ this.readSoundLocation();
+ },
+
+ /**
+ * Opens a filepicker to open a local sound for the alarm.
+ */
+ browseAlarm: function() {
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+ let picker = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+
+ let bundlePreferences = document.getElementById("bundleCalendarPreferences");
+ let title = bundlePreferences.getString("Open");
+ let wildmat = "*.wav";
+ let label = bundlePreferences.getFormattedString("filterWav", [wildmat], 1);
+
+ picker.init(window, title, nsIFilePicker.modeOpen);
+ picker.appendFilter(label, wildmat);
+ picker.appendFilters(nsIFilePicker.filterAll);
+
+ let ret = picker.show();
+
+ if (ret == nsIFilePicker.returnOK) {
+ document.getElementById("calendar.alarms.soundURL").value = picker.fileURL.spec;
+ document.getElementById("alarmSoundCheckbox").checked = true;
+ this.readSoundLocation();
+ }
+ },
+
+ /**
+ * Plays the alarm sound currently selected.
+ */
+ previewAlarm: function() {
+ let soundUrl = document.getElementById("alarmSoundFileField").value;
+ let soundIfc = Components.classes["@mozilla.org/sound;1"]
+ .createInstance(Components.interfaces.nsISound);
+ let url;
+ try {
+ soundIfc.init();
+ if (soundUrl && soundUrl.length && soundUrl.length > 0) {
+ url = Services.io.newURI(soundUrl, null, null);
+ soundIfc.play(url);
+ } else {
+ soundIfc.beep();
+ }
+ } catch (ex) {
+ dump("alarms.js previewAlarm Exception caught! " + ex + "\n");
+ }
+ },
+
+ /**
+ * Handler function to call when the calendar.alarms.playsound preference
+ * has been changed. Updates the disabled state of fields that depend on
+ * playing a sound.
+ */
+ alarmsPlaySoundPrefChanged: function() {
+ let alarmsPlaySoundPref =
+ document.getElementById("calendar.alarms.playsound");
+
+ let items = [document.getElementById("alarmSoundFileField"),
+ document.getElementById("calendar.prefs.alarm.sound.useDefault"),
+ document.getElementById("calendar.prefs.alarm.sound.browse"),
+ document.getElementById("calendar.prefs.alarm.sound.play")];
+
+ for (let i = 0; i < items.length; i++) {
+ items[i].disabled = !alarmsPlaySoundPref.value;
+ }
+ }
+};
diff --git a/calendar/base/content/preferences/alarms.xul b/calendar/base/content/preferences/alarms.xul
new file mode 100644
index 000000000..4f1f29316
--- /dev/null
+++ b/calendar/base/content/preferences/alarms.xul
@@ -0,0 +1,239 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % alarmsDTD SYSTEM "chrome://calendar/locale/preferences/alarms.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ %alarmsDTD;
+ %globalDTD;
+]>
+
+<overlay id="AlarmsPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="calPreferencesBoxAlarms">
+ <stringbundle id="bundleCalendarPreferences"
+ src="chrome://calendar/locale/calendar.properties"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/preferences/alarms.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <preferences>
+ <preference id="calendar.alarms.playsound"
+ name="calendar.alarms.playsound"
+ type="bool"
+ onchange="gAlarmsPane.alarmsPlaySoundPrefChanged();"/>
+ <preference id="calendar.alarms.soundURL"
+ name="calendar.alarms.soundURL"
+ type="string"
+ onchange="gAlarmsPane.readSoundLocation();"/>
+ <preference id="calendar.alarms.show"
+ name="calendar.alarms.show"
+ type="bool"/>
+ <preference id="calendar.alarms.showmissed"
+ name="calendar.alarms.showmissed"
+ type="bool"/>
+ <preference id="calendar.alarms.onforevents"
+ name="calendar.alarms.onforevents"
+ type="int"/>
+ <preference id="calendar.alarms.onfortodos"
+ name="calendar.alarms.onfortodos"
+ type="int"/>
+ <preference id="calendar.alarms.eventalarmlen"
+ name="calendar.alarms.eventalarmlen"
+ type="int"/>
+ <preference id="calendar.alarms.eventalarmunit"
+ name="calendar.alarms.eventalarmunit"
+ type="string"/>
+ <preference id="calendar.alarms.todoalarmlen"
+ name="calendar.alarms.todoalarmlen"
+ type="int"/>
+ <preference id="calendar.alarms.todoalarmunit"
+ name="calendar.alarms.todoalarmunit"
+ type="string"/>
+ <preference id="calendar.alarms.defaultsnoozelength"
+ name="calendar.alarms.defaultsnoozelength"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.alarmgoesoff.label;"/>
+ <grid id="alarm-sound-grid">
+ <columns id="alarm-sound-columns">
+ <column id="alarm-sound-label-column"/>
+ <column id="alarm-sound-content-column" flex="1"/>
+ </columns>
+ <rows id="alarm-sound-rows">
+ <row id="alarm-sound-soundfile-row" align="center">
+ <checkbox id="alarmSoundCheckbox"
+ preference="calendar.alarms.playsound"
+ label="&pref.playasound;"
+ accesskey="&pref.calendar.alarms.playsound.accessKey;"/>
+ <filefield id="alarmSoundFileField"
+ flex="1"
+ preference="calendar.alarms.soundURL"
+ preference-editable="true"
+ onsyncfrompreference="return gAlarmsPane.readSoundLocation();"/>
+ </row>
+ <row id="alarm-sound-buttons-row">
+ <spacer id="alarm-sound-spacer"/>
+ <hbox id="alarm-sound-buttons-box">
+ <button id="calendar.prefs.alarm.sound.useDefault"
+ flex="1"
+ label="&pref.calendar.alarms.sound.useDefault.label;"
+ accesskey="&pref.calendar.alarms.sound.useDefault.accessKey;"
+ oncommand="gAlarmsPane.useDefaultSound()"/>
+ <button id="calendar.prefs.alarm.sound.browse"
+ flex="1"
+ label="&pref.calendar.alarms.sound.browse.label;"
+ accesskey="&pref.calendar.alarms.sound.browse.accessKey;"
+ oncommand="gAlarmsPane.browseAlarm()"/>
+ <button id="calendar.prefs.alarm.sound.play"
+ flex="1"
+ label="&pref.calendar.alarms.sound.play.label;"
+ accesskey="&pref.calendar.alarms.sound.play.accessKey;"
+ oncommand="gAlarmsPane.previewAlarm()"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ <hbox align="center" flex="1">
+ <checkbox id="alarmshow"
+ preference="calendar.alarms.show"
+ label="&pref.showalarmbox;"
+ accesskey="&pref.calendar.alarms.showAlarmBox.accessKey;"/>
+ </hbox>
+ <hbox align="center" flex="1">
+ <checkbox id="missedalarms"
+ preference="calendar.alarms.showmissed"
+ label="&pref.missedalarms;"
+ accesskey="&pref.calendar.alarms.missedAlarms.accessKey;"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.calendar.alarms.defaults.label;"/>
+ <grid>
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&pref.defaultsnoozelength.label;"
+ accesskey="&pref.defaultsnoozelength.accesskey;"
+ control="defaultsnoozelength"/>
+ <hbox align="center">
+ <textbox id="defaultsnoozelength"
+ preference="calendar.alarms.defaultsnoozelength"
+ type="number"
+ min="0"
+ maxlength="4"
+ size="3"
+ onselect="updateUnitLabelPlural('defaultsnoozelength','defaultsnoozelengthunit','minutes')"
+ oninput="updateUnitLabelPlural('defaultsnoozelength','defaultsnoozelengthunit','minutes')"/>
+ <label id="defaultsnoozelengthunit"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <label value="&pref.defalarm4events.label;"
+ accesskey="&pref.defalarm4events.accesskey;"
+ control="eventdefalarm"/>
+ <menulist id="eventdefalarm"
+ crop="none"
+ preference="calendar.alarms.onforevents">
+ <menupopup id="eventdefalarmpopup">
+ <menuitem id="eventdefalarmon"
+ label="&pref.alarm.on;"
+ value="1"/>
+ <menuitem id="eventdefalarmoff"
+ label="&pref.alarm.off;"
+ value="0"
+ selected="true"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row align="center">
+ <label value="&pref.defalarm4todos.label;"
+ accesskey="&pref.defalarm4todos.accesskey;"
+ control="tododefalarm"/>
+ <menulist id="tododefalarm"
+ crop="none"
+ preference="calendar.alarms.onfortodos">
+ <menupopup id="tododefalarmpopup">
+ <menuitem id="tododefalarmon"
+ label="&pref.alarm.on;"
+ value="1"/>
+ <menuitem id="tododefalarmoff"
+ label="&pref.alarm.off;"
+ value="0"
+ selected="true"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row align="center">
+ <label value="&pref.defalarmlen4events.label;"
+ accesskey="&pref.defalarmlen4events.accesskey;"
+ control="eventdefalarmlen"/>
+ <hbox align="center">
+ <textbox id="eventdefalarmlen"
+ preference="calendar.alarms.eventalarmlen"
+ type="number"
+ min="0"
+ size="3"
+ onselect="updateMenuLabelsPlural('eventdefalarmlen','eventdefalarmunit')"
+ oninput="updateMenuLabelsPlural('eventdefalarmlen','eventdefalarmunit')"/>
+ <menulist id="eventdefalarmunit"
+ crop="none"
+ preference="calendar.alarms.eventalarmunit">
+ <menupopup id="eventdefalarmunitpopup">
+ <menuitem id="eventdefalarmunitmin"
+ value="minutes"
+ selected="true"/>
+ <menuitem id="eventdefalarmunithour"
+ value="hours"/>
+ <menuitem id="eventdefalarmunitday"
+ value="days"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </row>
+ <row align="center">
+ <label value="&pref.defalarmlen4todos.label;"
+ accesskey="&pref.defalarmlen4todos.accesskey;"
+ control="tododefalarmlen"/>
+ <hbox align="center">
+ <textbox id="tododefalarmlen"
+ preference="calendar.alarms.todoalarmlen"
+ type="number"
+ min="0"
+ size="3"
+ onselect="updateMenuLabelsPlural('tododefalarmlen','tododefalarmunit')"
+ oninput="updateMenuLabelsPlural('tododefalarmlen','tododefalarmunit')"/>
+ <menulist id="tododefalarmunit"
+ crop="none"
+ preference="calendar.alarms.todoalarmunit">
+ <menupopup id="tododefalarmunitpopup">
+ <menuitem id="tododefalarmunitmin"
+ value="minutes"
+ selected="true"/>
+ <menuitem id="tododefalarmunithour"
+ value="hours"/>
+ <menuitem id="tododefalarmunitday"
+ value="days"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+
+ </groupbox>
+
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/preferences/categories.js b/calendar/base/content/preferences/categories.js
new file mode 100644
index 000000000..bf70f4acd
--- /dev/null
+++ b/calendar/base/content/preferences/categories.js
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gCategoriesPane */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+var gCategoryList;
+var categoryPrefBranch = Services.prefs.getBranch("calendar.category.color.");
+
+/**
+ * Global Object to hold methods for the categories pref pane
+ */
+var gCategoriesPane = {
+
+ mCategoryDialog: null,
+ mWinProp: null,
+ mLoadInContent: false,
+
+ /**
+ * Initialize the categories pref pane. Sets up dialog controls to show the
+ * categories saved in preferences.
+ */
+ init: function() {
+ // On non-instant-apply platforms, once this pane has been loaded,
+ // attach our "revert all changes" function to the parent prefwindow's
+ // "ondialogcancel" event.
+ let parentPrefWindow = document.documentElement;
+ if (!parentPrefWindow.instantApply) {
+ let existingOnDialogCancel = parentPrefWindow.getAttribute("ondialogcancel");
+ parentPrefWindow.setAttribute("ondialogcancel",
+ "gCategoriesPane.panelOnCancel(); " +
+ existingOnDialogCancel);
+ }
+
+ // A list of preferences to be reverted when the dialog is cancelled.
+ // It needs to be a property of the parent to be visible onCancel
+ if (!("backupPrefList" in parent)) {
+ parent.backupPrefList = [];
+ }
+
+ let categories = document.getElementById("calendar.categories.names").value;
+
+ // If no categories are configured load a default set from properties file
+ if (!categories) {
+ categories = cal.setupDefaultCategories();
+ document.getElementById("calendar.categories.names").value = categories;
+ }
+
+ gCategoryList = categoriesStringToArray(categories);
+
+ // When categories is empty, split returns an array containing one empty
+ // string, rather than an empty array. This results in an empty listbox
+ // child with no corresponding category.
+ if (gCategoryList.length == 1 && !gCategoryList[0].length) {
+ gCategoryList.pop();
+ }
+
+ this.updateCategoryList();
+
+ this.mCategoryDialog = "chrome://calendar/content/preferences/editCategory.xul";
+
+ // Workaround for Bug 1151440 - the HTML color picker won't work
+ // in linux when opened from modal dialog
+ this.mWinProp = "centerscreen, chrome, resizable=no";
+ if (AppConstants.platform != "linux") {
+ this.mWinProp += ", modal";
+ }
+
+ this.mLoadInContent = Preferences.get(
+ "mail.preferences.inContent",
+ false
+ );
+ if (this.mLoadInContent) {
+ gSubDialog.init();
+ }
+ },
+
+ /**
+ * Updates the listbox containing the categories from the categories saved
+ * in preferences.
+ */
+
+ updatePrefs: function() {
+ cal.sortArrayByLocaleCollator(gCategoryList);
+ document.getElementById("calendar.categories.names").value =
+ categoriesArrayToString(gCategoryList);
+ },
+
+ updateCategoryList: function() {
+ this.updatePrefs();
+ let listbox = document.getElementById("categorieslist");
+
+ listbox.clearSelection();
+ this.updateButtons();
+
+
+ while (listbox.lastChild.id != "categoryColumns") {
+ listbox.lastChild.remove();
+ }
+
+ for (let i = 0; i < gCategoryList.length; i++) {
+ let newListItem = document.createElement("listitem");
+ let categoryName = document.createElement("listcell");
+ categoryName.setAttribute("id", gCategoryList[i]);
+ categoryName.setAttribute("label", gCategoryList[i]);
+ let categoryNameFix = formatStringForCSSRule(gCategoryList[i]);
+ let categoryColor = document.createElement("listcell");
+ try {
+ let colorCode = categoryPrefBranch.getCharPref(categoryNameFix);
+ categoryColor.setAttribute("id", colorCode);
+ categoryColor.setAttribute("style", "background-color: " + colorCode + ";");
+ } catch (ex) {
+ categoryColor.setAttribute("label", noneLabel);
+ }
+
+ newListItem.appendChild(categoryName);
+ newListItem.appendChild(categoryColor);
+ listbox.appendChild(newListItem);
+ }
+ },
+
+ /**
+ * Adds a category, opening the edit category dialog to prompt the user to
+ * set up the category.
+ */
+ addCategory: function() {
+ let listbox = document.getElementById("categorieslist");
+ listbox.clearSelection();
+ this.updateButtons();
+ let params = {
+ title: newTitle,
+ category: "",
+ color: null
+ };
+ if (this.mLoadInContent) {
+ gSubDialog.open(this.mCategoryDialog, "resizable=no", params);
+ } else {
+ window.openDialog(this.mCategoryDialog, "addCategory", this.mWinProp, params);
+ }
+ },
+
+ /**
+ * Edits the currently selected category using the edit category dialog.
+ */
+ editCategory: function() {
+ let list = document.getElementById("categorieslist");
+ let categoryNameFix = formatStringForCSSRule(gCategoryList[list.selectedIndex]);
+ let currentColor = null;
+ try {
+ currentColor = categoryPrefBranch.getCharPref(categoryNameFix);
+ } catch (ex) {
+ // If the pref doesn't exist, don't bail out here.
+ }
+ let params = {
+ title: editTitle,
+ category: gCategoryList[list.selectedIndex],
+ color: currentColor
+ };
+ if (list.selectedItem) {
+ if (this.mLoadInContent) {
+ gSubDialog.open(this.mCategoryDialog, "resizable=no", params);
+ } else {
+ window.openDialog(this.mCategoryDialog, "editCategory", this.mWinProp, params);
+ }
+ }
+ },
+
+ /**
+ * Removes the selected category.
+ */
+ deleteCategory: function() {
+ let list = document.getElementById("categorieslist");
+ if (list.selectedCount < 1) {
+ return;
+ }
+
+ let categoryNameFix = formatStringForCSSRule(gCategoryList[list.selectedIndex]);
+ this.backupData(categoryNameFix);
+ try {
+ categoryPrefBranch.clearUserPref(categoryNameFix);
+ } catch (ex) {
+ // If the pref doesn't exist, don't bail out here.
+ }
+
+ // Remove category entry from listbox and gCategoryList.
+ let newSelection = list.selectedItem.nextSibling ||
+ list.selectedItem.previousSibling;
+ let selectedItems = Array.slice(list.selectedItems).concat([]);
+ for (let i = list.selectedCount - 1; i >= 0; i--) {
+ let item = selectedItems[i];
+ if (item == newSelection) {
+ newSelection = newSelection.nextSibling ||
+ newSelection.previousSibling;
+ }
+ gCategoryList.splice(list.getIndexOfItem(item), 1);
+ item.remove();
+ }
+ list.selectedItem = newSelection;
+ this.updateButtons();
+
+ // Update the prefs from gCategoryList
+ this.updatePrefs();
+ },
+
+ /**
+ * Saves the given category to the preferences.
+ *
+ * @param categoryName The name of the category.
+ * @param categoryColor The color of the category
+ */
+ saveCategory: function(categoryName, categoryColor) {
+ let list = document.getElementById("categorieslist");
+ // Check to make sure another category doesn't have the same name
+ let toBeDeleted = -1;
+ for (let i = 0; i < gCategoryList.length; i++) {
+ if (i == list.selectedIndex) {
+ continue;
+ }
+
+ if (categoryName.toLowerCase() == gCategoryList[i].toLowerCase()) {
+ if (Services.prompt.confirm(null, overwriteTitle, overwrite)) {
+ if (list.selectedIndex != -1) {
+ // Don't delete the old category yet. It will mess up indices.
+ toBeDeleted = list.selectedIndex;
+ }
+ list.selectedIndex = i;
+ } else {
+ return;
+ }
+ }
+ }
+
+ if (categoryName.length == 0) {
+ Services.prompt.alert(null, null, noBlankCategories);
+ return;
+ }
+
+ let categoryNameFix = formatStringForCSSRule(categoryName);
+ if (list.selectedIndex == -1) {
+ this.backupData(categoryNameFix);
+ gCategoryList.push(categoryName);
+ if (categoryColor) {
+ categoryPrefBranch.setCharPref(categoryNameFix, categoryColor);
+ }
+ } else {
+ this.backupData(categoryNameFix);
+ gCategoryList.splice(list.selectedIndex, 1, categoryName);
+ if (categoryColor) {
+ categoryPrefBranch.setCharPref(categoryNameFix, categoryColor);
+ } else {
+ try {
+ categoryPrefBranch.clearUserPref(categoryNameFix);
+ } catch (ex) {
+ dump("Exception caught in 'saveCategory': " + ex + "\n");
+ }
+ }
+ }
+
+ // If 'Overwrite' was chosen, delete category that was being edited
+ if (toBeDeleted != -1) {
+ list.selectedIndex = toBeDeleted;
+ this.deleteCategory();
+ }
+
+ this.updateCategoryList();
+
+ let updatedCategory = gCategoryList.indexOf(categoryName);
+ list.ensureIndexIsVisible(updatedCategory);
+ list.selectedIndex = updatedCategory;
+ },
+
+ /**
+ * Enable the edit and delete category buttons.
+ */
+ updateButtons: function() {
+ let categoriesList = document.getElementById("categorieslist");
+ document.getElementById("deleteCButton").disabled = (categoriesList.selectedCount <= 0);
+ document.getElementById("editCButton").disabled = (categoriesList.selectedCount != 1);
+ },
+
+ /**
+ * Backs up the category name in case the dialog is canceled.
+ *
+ * @see formatStringForCSSRule
+ * @param categoryNameFix The formatted category name.
+ */
+ backupData: function(categoryNameFix) {
+ let currentColor;
+ try {
+ currentColor = categoryPrefBranch.getCharPref(categoryNameFix);
+ } catch (ex) {
+ dump("Exception caught in 'backupData': " + ex + "\n");
+ currentColor = "##NEW";
+ }
+
+ for (let i = 0; i < parent.backupPrefList.length; i++) {
+ if (categoryNameFix == parent.backupPrefList[i].name) {
+ return;
+ }
+ }
+ parent.backupPrefList[parent.backupPrefList.length] =
+ { name: categoryNameFix, color: currentColor };
+ },
+
+ /**
+ * Event Handler function to be called on doubleclick of the categories
+ * list. If the edit function is enabled and the user doubleclicked on a
+ * list item, then edit the selected category.
+ */
+ listOnDblClick: function(event) {
+ if (event.target.localName == "listitem" &&
+ !document.getElementById("editCButton").disabled) {
+ this.editCategory();
+ }
+ },
+
+ /**
+ * Reverts category preferences in case the cancel button is pressed.
+ */
+ panelOnCancel: function() {
+ for (let i = 0; i < parent.backupPrefList.length; i++) {
+ if (parent.backupPrefList[i].color == "##NEW") {
+ try {
+ categoryPrefBranch.clearUserPref(parent.backupPrefList[i].name);
+ } catch (ex) {
+ dump("Exception caught in 'panelOnCancel': " + ex + "\n");
+ }
+ } else {
+ categoryPrefBranch.setCharPref(parent.backupPrefList[i].name,
+ parent.backupPrefList[i].color);
+ }
+ }
+ }
+};
diff --git a/calendar/base/content/preferences/categories.xul b/calendar/base/content/preferences/categories.xul
new file mode 100644
index 000000000..23339e273
--- /dev/null
+++ b/calendar/base/content/preferences/categories.xul
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://calendar/locale/preferences/categories.dtd">
+
+<overlay id="CategoriesPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="calPreferencesBoxCategories">
+ <script type="application/javascript"
+ src="chrome://calendar/content/preferences/categories.js"/>
+
+ <!-- Get the localized text for use in the .js -->
+ <script type="application/javascript">
+ var noneLabel = "&pref.categories.none.label;";
+ var newTitle = "&pref.categories.new.title;";
+ var editTitle = "&pref.categories.edit.title;";
+ var overwrite = "&pref.categories.overwrite;";
+ var overwriteTitle = "&pref.categories.overwrite.title;";
+ var noBlankCategories = "&pref.categories.noBlankCategories;";
+ </script>
+
+ <preferences>
+ <preference id="calendar.categories.names"
+ name="calendar.categories.names"
+ type="string"/>
+ </preferences>
+
+ <listbox id="categorieslist"
+ flex="1"
+ seltype="multiple"
+ onselect="gCategoriesPane.updateButtons()"
+ ondblclick="gCategoriesPane.listOnDblClick(event)">
+ <listhead>
+ <listheader label="&pref.categories.name.label;"/>
+ <listheader label="&pref.categories.color.label;"/>
+ </listhead>
+ <listcols id="categoryColumns">
+ <listcol flex="3"/>
+ <listcol flex="1"/>
+ </listcols>
+ </listbox>
+ <hbox pack="end">
+ <button label="&pref.categories.newButton.label;"
+ accesskey="&pref.categories.newButton.accesskey;"
+ oncommand="gCategoriesPane.addCategory()"/>
+ <button id="editCButton"
+ label="&pref.categories.editButton.label;"
+ accesskey="&pref.categories.editButton.accesskey;"
+ oncommand="gCategoriesPane.editCategory()"/>
+ <button id="deleteCButton"
+ label="&pref.categories.removeButton.label;"
+ accesskey="&pref.categories.removeButton.accesskey;"
+ oncommand="gCategoriesPane.deleteCategory()"/>
+ </hbox>
+
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/preferences/editCategory.js b/calendar/base/content/preferences/editCategory.js
new file mode 100644
index 000000000..24f523c4c
--- /dev/null
+++ b/calendar/base/content/preferences/editCategory.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported editCategoryLoad, doOK, categoryNameChanged, clickColor, delay */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+// Global variable, set to true if the user has picked a custom color.
+var customColorSelected = false;
+
+/**
+ * Load Handler, called when the edit category dialog is loaded
+ */
+function editCategoryLoad() {
+ let winArg = window.arguments[0];
+ let color = winArg.color || cal.hashColor(winArg.category);
+ let hasColor = (winArg.color != null);
+ document.getElementById("categoryName").value = winArg.category;
+ document.getElementById("categoryColor").value = color;
+ document.getElementById("useColor").checked = hasColor;
+ customColorSelected = hasColor;
+ document.title = winArg.title;
+
+ toggleColor();
+}
+
+/**
+ * Handler function to be called when the category dialog is accepted and
+ * the opener should further process the selected name and color
+ */
+function doOK() {
+ let color = document.getElementById("useColor").checked
+ ? document.getElementById("categoryColor").value
+ : null;
+
+ let categoryName = document.getElementById("categoryName").value;
+ window.opener.gCategoriesPane.saveCategory(categoryName, color);
+ return true;
+}
+
+/**
+ * Handler function to be called when the category name changed
+ */
+function categoryNameChanged() {
+ let newValue = document.getElementById("categoryName").value;
+
+ // The user removed the category name, assign the color automatically again.
+ if (newValue == "") {
+ customColorSelected = false;
+ }
+
+ if (!customColorSelected && document.getElementById("useColor").checked) {
+ // Color is wanted, choose the color based on the category name's hash.
+ document.getElementById("categoryColor").value = cal.hashColor(newValue);
+ }
+}
+
+/**
+ * Handler function to be called when the color picker's color has been changed.
+ */
+function colorPickerChanged() {
+ document.getElementById("useColor").checked = true;
+ customColorSelected = true;
+}
+
+/**
+ * Handler called when the use color checkbox is toggled.
+ */
+function toggleColor() {
+ let useColor = document.getElementById("useColor").checked;
+ let categoryColor = document.getElementById("categoryColor");
+
+ if (useColor) {
+ categoryColor.setAttribute("type", "color");
+ if (toggleColor.lastColor) {
+ categoryColor.value = toggleColor.lastColor;
+ }
+ } else {
+ categoryColor.setAttribute("type", "button");
+ toggleColor.lastColor = categoryColor.value;
+ categoryColor.value = "";
+ }
+}
+
+/**
+ * Click handler for the color picker. Turns the button back into a colorpicker
+ * when clicked.
+ */
+function clickColor() {
+ let categoryColor = document.getElementById("categoryColor");
+ if (categoryColor.getAttribute("type") == "button") {
+ colorPickerChanged();
+ toggleColor();
+ categoryColor.click();
+ }
+}
+
+/**
+ * Call the function after the given timeout, resetting the timer if delay is
+ * called again with the same function.
+ *
+ * @param timeout The timeout interval.
+ * @param func The function to call after the timeout.
+ */
+function delay(timeout, func) {
+ if (func.timer) {
+ clearTimeout(func.timer);
+ }
+ func.timer = setTimeout(func, timeout);
+}
diff --git a/calendar/base/content/preferences/editCategory.xul b/calendar/base/content/preferences/editCategory.xul
new file mode 100644
index 000000000..64c868003
--- /dev/null
+++ b/calendar/base/content/preferences/editCategory.xul
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!-- DTD File with all strings specific to the file -->
+<!DOCTYPE dialog
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/preferences/categories.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd2;
+]>
+
+<dialog id="editCategory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ buttons="accept,cancel"
+ onload="editCategoryLoad();"
+ ondialogaccept="return doOK();">
+ <script type="application/javascript" src="chrome://calendar/content/preferences/editCategory.js"/>
+
+ <vbox id="dialog-box">
+ <label value="&pref.categories.name.label;"
+ control="categoryName"/>
+ <textbox id="categoryName"
+ flex="1"
+ onchange="categoryNameChanged()"
+ oninput="delay(500, categoryNameChanged)"/>
+ <hbox id="colorSelectRow">
+ <checkbox id="useColor"
+ label="&pref.categories.usecolor.label;"
+ oncommand="toggleColor(); categoryNameChanged()"/>
+ <html:input id="categoryColor"
+ type="color"
+ style="width: 64px; height: 23px"
+ onclick="clickColor()"
+ onchange="colorPickerChanged()"/>
+ </hbox>
+ </vbox>
+</dialog>
diff --git a/calendar/base/content/preferences/general.js b/calendar/base/content/preferences/general.js
new file mode 100644
index 000000000..166e45a6a
--- /dev/null
+++ b/calendar/base/content/preferences/general.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gCalendarGeneralPane */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/**
+ * Global Object to hold methods for the general pref pane
+ */
+var gCalendarGeneralPane = {
+ /**
+ * Initialize the general pref pane. Sets up dialog controls to match the
+ * values set in prefs.
+ */
+ init: function() {
+ let formatter = Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+
+ let dateFormattedLong = formatter.formatDateLong(now());
+ let dateFormattedShort = formatter.formatDateShort(now());
+
+ // menu items include examples of current date formats.
+ document.getElementById("dateformat-long-menuitem")
+ .setAttribute("label", labelLong + ": " + dateFormattedLong);
+ document.getElementById("dateformat-short-menuitem")
+ .setAttribute("label", labelShort + ": " + dateFormattedShort);
+
+ // deselect and reselect to update visible item title
+ updateSelectedLabel("dateformat");
+ updateUnitLabelPlural("defaultlength", "defaultlengthunit", "minutes");
+ this.updateDefaultTodoDates();
+
+ let tzMenuList = document.getElementById("calendar-timezone-menulist");
+ let tzMenuPopup = document.getElementById("calendar-timezone-menupopup");
+
+ let tzService = cal.getTimezoneService();
+ let enumerator = tzService.timezoneIds;
+ let tzids = {};
+ let displayNames = [];
+ // don't rely on what order the timezone-service gives you
+ while (enumerator.hasMore()) {
+ let timezone = tzService.getTimezone(enumerator.getNext());
+ if (timezone && !timezone.isFloating && !timezone.isUTC) {
+ let displayName = timezone.displayName;
+ displayNames.push(displayName);
+ tzids[displayName] = timezone.tzid;
+ }
+ }
+ // the display names need to be sorted
+ displayNames.sort(String.localeCompare);
+ for (let displayName of displayNames) {
+ addMenuItem(tzMenuPopup, displayName, tzids[displayName]);
+ }
+
+ let prefValue = document.getElementById("calendar-timezone-local").value;
+ if (!prefValue) {
+ prefValue = calendarDefaultTimezone().tzid;
+ }
+ tzMenuList.value = prefValue;
+
+ // Set the soondays menulist preference
+ this.initializeTodaypaneMenu();
+ },
+
+ updateDefaultTodoDates: function() {
+ let defaultDue = document.getElementById("default_task_due").value;
+ let defaultStart = document.getElementById("default_task_start").value;
+ let offsetValues = ["offsetcurrent", "offsetnexthour"];
+
+ document.getElementById("default_task_due_offset")
+ .style.visibility = offsetValues.includes(defaultDue) ? "" : "hidden";
+ document.getElementById("default_task_start_offset")
+ .style.visibility = offsetValues.includes(defaultStart) ? "" : "hidden";
+
+ updateMenuLabelsPlural("default_task_start_offset_text", "default_task_start_offset_units");
+ updateMenuLabelsPlural("default_task_due_offset_text", "default_task_due_offset_units");
+ },
+
+ updateItemtypeDeck: function() {
+ let panelId = document.getElementById("defaults-itemtype-menulist").value;
+ let panel = document.getElementById(panelId);
+ document.getElementById("defaults-itemtype-deck").selectedPanel = panel;
+ },
+
+ initializeTodaypaneMenu: function() {
+ // Assign the labels for the menuitem
+ let soondaysMenu = document.getElementById("soondays-menulist");
+ let items = soondaysMenu.getElementsByTagName("menuitem");
+ for (let menuItem of items) {
+ let menuitemValue = Number(menuItem.value);
+ if (menuitemValue > 7) {
+ menuItem.label = unitPluralForm(menuitemValue / 7, "weeks");
+ } else {
+ menuItem.label = unitPluralForm(menuitemValue, "days");
+ }
+ }
+
+ let prefName = "calendar.agendaListbox.soondays";
+ let soonpref = Preferences.get(prefName, 5);
+
+ // Check if soonDays preference has been edited with a wrong value.
+ if (soonpref > 0 && soonpref <= 28) {
+ if (soonpref % 7 != 0) {
+ let intSoonpref = Math.floor(soonpref / 7) * 7;
+ soonpref = (intSoonpref == 0 ? soonpref : intSoonpref);
+ Preferences.set(prefName, soonpref, "INT");
+ }
+ } else {
+ soonpref = soonpref > 28 ? 28 : 1;
+ Preferences.set(prefName, soonpref, "INT");
+ }
+
+ document.getElementById("soondays-menulist").value = soonpref;
+ },
+
+ updateTodaypaneMenu: function() {
+ let soonpref = Number(document.getElementById("soondays-menulist").value);
+ Preferences.set("calendar.agendaListbox.soondays", soonpref);
+ }
+};
diff --git a/calendar/base/content/preferences/general.xul b/calendar/base/content/preferences/general.xul
new file mode 100644
index 000000000..49fddb994
--- /dev/null
+++ b/calendar/base/content/preferences/general.xul
@@ -0,0 +1,309 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % generalDTD SYSTEM "chrome://calendar/locale/preferences/general.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ <!ENTITY % eventDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ %generalDTD;
+ %globalDTD;
+ %eventDTD;
+]>
+
+<overlay id="CalendarGeneralPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="calPreferencesBoxGeneral">
+ <script type="application/javascript"
+ src="chrome://calendar/content/preferences/general.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+
+ <!-- Get the localized text for use in the .js -->
+ <script type="application/javascript">
+ var labelLong = "&pref.dateformat.long;";
+ var labelShort = "&pref.dateformat.short;";
+ </script>
+
+ <preferences>
+ <preference id="calendar.date.format"
+ name="calendar.date.format"
+ type="int"/>
+ <preference id="calendar.event.defaultlength"
+ name="calendar.event.defaultlength"
+ type="int"/>
+ <preference id="calendar-timezone-local"
+ name="calendar.timezone.local"
+ type="string"/>
+ <preference id="calendar.task.defaultstart"
+ name="calendar.task.defaultstart"
+ type="string"/>
+ <preference id="calendar.task.defaultstartoffset"
+ name="calendar.task.defaultstartoffset"
+ type="int"/>
+ <preference id="calendar.task.defaultstartoffsetunits"
+ name="calendar.task.defaultstartoffsetunits"
+ type="string"/>
+ <preference id="calendar.task.defaultdue"
+ name="calendar.task.defaultdue"
+ type="string"/>
+ <preference id="calendar.task.defaultdueoffset"
+ name="calendar.task.defaultdueoffset"
+ type="int"/>
+ <preference id="calendar.task.defaultdueoffsetunits"
+ name="calendar.task.defaultdueoffsetunits"
+ type="string"/>
+ <preference id="calendar.view.useSystemColors"
+ name="calendar.view.useSystemColors"
+ type="bool"/>
+ <preference id="calendar.agendaListbox.soondays"
+ name="calendar.agendaListbox.soondays"
+ type="int"/>
+ <preference id="calendar.item.editInTab"
+ name="calendar.item.editInTab"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.mainbox.label;"/>
+ <hbox align="center">
+ <label value="&pref.dateformat.label;"
+ accesskey="&pref.dateformat.accesskey;"
+ control="dateformat"/>
+ <menulist id="dateformat" crop="none"
+ preference="calendar.date.format">
+ <menupopup id="dateformatpopup">
+ <menuitem id="dateformat-long-menuitem"
+ label="&pref.dateformat.long;"
+ value="0"/>
+ <menuitem id="dateformat-short-menuitem"
+ label="&pref.dateformat.short;"
+ value="1"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.timezones.caption;"/>
+ <hbox align="center">
+ <label value="&pref.timezones.label;"
+ accesskey="&pref.timezones.accesskey;"
+ control="calendar-timezone-menulist"/>
+ <menulist id="calendar-timezone-menulist"
+ preference="calendar-timezone-local">
+ <menupopup id="calendar-timezone-menupopup"/>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ <groupbox id="defaults-itemtype-groupbox">
+ <caption id="defaults-itemtype-caption" label="&pref.defaults.label;"/>
+ <hbox id="defaults-itemtype-box" align="top">
+ <menulist id="defaults-itemtype-menulist"
+ flex="1"
+ oncommand="gCalendarGeneralPane.updateItemtypeDeck()">
+ <menupopup id="defaults-itemtype-menupopup">
+ <menuitem id="defaults-itemtype-event"
+ label="&pref.events.label;"
+ value="defaults-event-grid"/>
+ <menuitem id="defaults-itemtype-task"
+ label="&pref.tasks.label;"
+ value="defaults-task-grid"/>
+ </menupopup>
+ </menulist>
+ <spacer id="defaults-itemtype-spacer" flex="1"/>
+ <deck id="defaults-itemtype-deck" flex="1">
+ <grid id="defaults-event-grid">
+ <columns id="defaults-event-grid-columns">
+ <column id="defaults-event-grid-column"/>
+ </columns>
+ <rows id="defaults-event-grid-rows">
+ <row id="defaults-event-grid-row" align="center">
+ <label id="default-event-length-label"
+ value="&pref.default_event_length.label;"
+ accesskey="&pref.default_event_length.accesskey;"
+ control="defaultlength"/>
+ <textbox id="defaultlength"
+ preference="calendar.event.defaultlength"
+ type="number"
+ min="0"
+ maxlength="3"
+ size="3"
+ onselect="updateUnitLabelPlural('defaultlength','defaultlengthunit','minutes')"
+ oninput="updateUnitLabelPlural('defaultlength','defaultlengthunit','minutes')"/>
+ <label id="defaultlengthunit"/>
+ </row>
+ </rows>
+ </grid>
+ <grid id="defaults-task-grid">
+ <columns id="defaults-task-grid-columns">
+ <column id="defaults-task-grid-label-column"/>
+ <column id="defaults-task-grid-value-column"/>
+ <column id="defaults-task-grid-offset-column"/>
+ </columns>
+ <rows id="defaults-task-grid-rows">
+ <row id="defaults-task-start-row" align="center">
+ <label id="default-task-start-label"
+ value="&read.only.task.start.label;"
+ control="default_task_start"/>
+ <menulist id="default_task_start"
+ crop="none"
+ oncommand="gCalendarGeneralPane.updateDefaultTodoDates()"
+ preference="calendar.task.defaultstart">
+ <menupopup id="default_task_start_popup">
+ <menuitem id="default_task_start_none"
+ label="&pref.default_task_none.label;"
+ value="none"
+ selected="true"/>
+ <menuitem id="default_task_start_start_of_day"
+ label="&pref.default_task_start_of_day.label;"
+ value="startofday"/>
+ <menuitem id="default_task_start_tomorrow"
+ label="&pref.default_task_tomorrow.label;"
+ value="tomorrow"/>
+ <menuitem id="default_task_start_next_week"
+ label="&pref.default_task_next_week.label;"
+ value="nextweek"/>
+ <menuitem id="default_task_start_offset_current"
+ label="&pref.default_task_offset_current.label;"
+ value="offsetcurrent"/>
+ <menuitem id="default_task_start_offset_next_hour"
+ label="&pref.default_task_offset_next_hour.label;"
+ value="offsetnexthour"/>
+ </menupopup>
+ </menulist>
+ <hbox id="default_task_start_offset" align="center">
+ <textbox id="default_task_start_offset_text"
+ preference="calendar.task.defaultstartoffset"
+ type="number"
+ min="0"
+ maxlength="3"
+ size="3"
+ onselect="updateMenuLabelsPlural('default_task_start_offset_text', 'default_task_start_offset_units')"
+ oninput="updateMenuLabelsPlural('default_task_start_offset_text', 'default_task_start_offset_units')"/>
+ <menulist id="default_task_start_offset_units"
+ crop="none"
+ preference="calendar.task.defaultstartoffsetunits">
+ <menupopup id="default_task_start_offset_units_popup">
+ <menuitem id="default_task_start_offset_units_minutes"
+ value="minutes"
+ selected="true"/>
+ <menuitem id="default_task_start_offset_units_hours"
+ value="hours"/>
+ <menuitem id="default_task_start_offset_units_days"
+ value="days"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </row>
+ <row id="defaults-task-due-row" align="center">
+ <label id="default-task-due-label"
+ value="&read.only.task.due.label;"
+ control="default_task_due"/>
+ <menulist id="default_task_due"
+ crop="none"
+ oncommand="gCalendarGeneralPane.updateDefaultTodoDates()"
+ preference="calendar.task.defaultdue">
+ <menupopup id="default_task_due_popup">
+ <menuitem id="default_task_due_none"
+ label="&pref.default_task_none.label;"
+ value="none"
+ selected="true"/>
+ <menuitem id="default_task_due_end_of_day"
+ label="&pref.default_task_end_of_day.label;"
+ value="endofday"/>
+ <menuitem id="default_task_due_tomorrow"
+ label="&pref.default_task_tomorrow.label;"
+ value="tomorrow"/>
+ <menuitem id="default_task_due_next_week"
+ label="&pref.default_task_next_week.label;"
+ value="nextweek"/>
+ <menuitem id="default_task_due_offset_current"
+ label="&pref.default_task_offset_start.label;"
+ value="offsetcurrent"/>
+ <menuitem id="default_task_due_offset_next_hour"
+ label="&pref.default_task_offset_next_hour.label;"
+ value="offsetnexthour"/>
+ </menupopup>
+ </menulist>
+ <hbox id="default_task_due_offset" align="center">
+ <textbox id="default_task_due_offset_text"
+ preference="calendar.task.defaultdueoffset"
+ type="number"
+ min="0"
+ maxlength="3"
+ size="3"
+ onselect="updateMenuLabelsPlural('default_task_due_offset_text', 'default_task_due_offset_units')"
+ oninput="updateMenuLabelsPlural('default_task_due_offset_text', 'default_task_due_offset_units')"/>
+ <menulist id="default_task_due_offset_units"
+ crop="none"
+ preference="calendar.task.defaultdueoffsetunits">
+ <menupopup id="default_task_due_offset_units_popup">
+ <menuitem id="default_task_due_offset_units_minutes"
+ value="minutes"
+ selected="true"/>
+ <menuitem id="default_task_due_offset_units_hours"
+ value="hours"/>
+ <menuitem id="default_task_due_offset_units_days"
+ value="days"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ </deck>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.calendar.todaypane.agenda.caption;"/>
+ <hbox align="center">
+ <label value="&pref.soondays.label;"
+ accesskey="&pref.soondays.accesskey;"
+ control="soondays-menulist"/>
+ <menulist id="soondays-menulist"
+ preference="calendar.agendaListbox.soondays"
+ oncommand="gCalendarGeneralPane.updateTodaypaneMenu()">
+ <menupopup id="soondaysdurationpopup">
+ <menuitem value="1"/>
+ <menuitem value="2"/>
+ <menuitem value="3"/>
+ <menuitem value="4"/>
+ <menuitem value="5"/>
+ <menuitem value="6"/>
+ <menuitem value="7"/>
+ <menuitem value="14"/>
+ <menuitem value="21"/>
+ <menuitem value="28"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.accessibility.label;"/>
+ <hbox align="center">
+ <checkbox id="systemColors" pack="end"
+ label="&pref.systemcolors.label;"
+ accesskey="&pref.systemcolors.accesskey;"
+ preference="calendar.view.useSystemColors"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.eventsandtasks.label;"/>
+ <hbox align="center">
+ <checkbox id="tabedit" pack="end"
+ label="&pref.editInTab.label;"
+ accesskey="&pref.editInTab.accesskey;"
+ preference="calendar.item.editInTab"/>
+ </hbox>
+ </groupbox>
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/preferences/views.js b/calendar/base/content/preferences/views.js
new file mode 100644
index 000000000..aa4c29e30
--- /dev/null
+++ b/calendar/base/content/preferences/views.js
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gViewsPane */
+
+/**
+ * Global Object to hold methods for the views pref pane
+ */
+var gViewsPane = {
+ /**
+ * Initialize the views pref pane. Sets up dialog controls to match the
+ * values set in prefs.
+ */
+ init: function() {
+ this.updateViewEndMenu(document.getElementById("daystarthour").value);
+ this.updateViewStartMenu(document.getElementById("dayendhour").value);
+ this.updateViewWorkDayCheckboxes(document.getElementById("weekstarts").value);
+ this.initializeViewStartEndMenus();
+ },
+
+ /**
+ * Initialize the strings for the "day starts at" and "day ends at"
+ * menulists. This is needed to respect locales that use AM/PM.
+ */
+ initializeViewStartEndMenus: function() {
+ let labelIdStart;
+ let labelIdEnd;
+ let timeFormatter = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Components.interfaces.nsIScriptableDateFormat);
+ // 1 to 23 instead of 0 to 24 to keep midnight & noon as the localized strings
+ for (let theHour = 1; theHour <= 23; theHour++) {
+ let time = timeFormatter.FormatTime("", Components.interfaces.nsIScriptableDateFormat
+ .timeFormatNoSeconds, theHour, 0, 0);
+
+ labelIdStart = "timeStart" + theHour;
+ labelIdEnd = "timeEnd" + theHour;
+ // This if block to keep Noon as the localized string, instead of as a number.
+ if (theHour != 12) {
+ document.getElementById(labelIdStart).setAttribute("label", time);
+ document.getElementById(labelIdEnd).setAttribute("label", time);
+ }
+ }
+ // Deselect and reselect to update visible item title
+ updateSelectedLabel("daystarthour");
+ updateSelectedLabel("dayendhour");
+ },
+
+
+ /**
+ * Updates the view end menu to only display hours after the selected view
+ * start.
+ *
+ * @param aStartValue The value selected for view start.
+ */
+ updateViewEndMenu: function(aStartValue) {
+ let endMenuKids = document.getElementById("dayendhourpopup")
+ .childNodes;
+ for (let i = 0; i < endMenuKids.length; i++) {
+ if (Number(endMenuKids[i].value) <= Number(aStartValue)) {
+ endMenuKids[i].setAttribute("hidden", true);
+ } else {
+ endMenuKids[i].removeAttribute("hidden");
+ }
+ }
+ },
+
+ /**
+ * Updates the view start menu to only display hours before the selected view
+ * end.
+ *
+ * @param aEndValue The value selected for view end.
+ */
+ updateViewStartMenu: function(aEndValue) {
+ let startMenuKids = document.getElementById("daystarthourpopup")
+ .childNodes;
+ for (let i = 0; i < startMenuKids.length; i++) {
+ if (Number(startMenuKids[i].value) >= Number(aEndValue)) {
+ startMenuKids[i].setAttribute("hidden", true);
+ } else {
+ startMenuKids[i].removeAttribute("hidden");
+ }
+ }
+ },
+
+ /**
+ * Update the workday checkboxes based on the start of the week.
+ *
+ * @Param weekStart The (0-based) index of the weekday the week
+ * should start at.
+ */
+ updateViewWorkDayCheckboxes: function(weekStart) {
+ weekStart = Number(weekStart);
+ for (let i = weekStart; i < weekStart + 7; i++) {
+ let checkbox = document.getElementById("dayoff" + (i % 7));
+ checkbox.parentNode.appendChild(checkbox);
+ }
+ }
+};
diff --git a/calendar/base/content/preferences/views.xul b/calendar/base/content/preferences/views.xul
new file mode 100644
index 000000000..24fc362fb
--- /dev/null
+++ b/calendar/base/content/preferences/views.xul
@@ -0,0 +1,306 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % viewsDTD SYSTEM "chrome://calendar/locale/preferences/views.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ %viewsDTD;
+ %globalDTD;
+]>
+
+<overlay id="ViewsPaneOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="calPreferencesBoxViews">
+ <script type="application/javascript"
+ src="chrome://calendar/content/preferences/views.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <preferences>
+ <preference id="calendar.week.start"
+ name="calendar.week.start"
+ type="int"/>
+ <preference id="calendar.view-minimonth.showWeekNumber"
+ name="calendar.view-minimonth.showWeekNumber"
+ type="bool"/>
+ <preference id="calendar.week.d0sundaysoff"
+ name="calendar.week.d0sundaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d1mondaysoff"
+ name="calendar.week.d1mondaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d2tuesdaysoff"
+ name="calendar.week.d2tuesdaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d3wednesdaysoff"
+ name="calendar.week.d3wednesdaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d4thursdaysoff"
+ name="calendar.week.d4thursdaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d5fridaysoff"
+ name="calendar.week.d5fridaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.week.d6saturdaysoff"
+ name="calendar.week.d6saturdaysoff"
+ type="bool"
+ inverted="true"/>
+ <preference id="calendar.view.daystarthour"
+ name="calendar.view.daystarthour"
+ type="int"/>
+ <preference id="calendar.view.dayendhour"
+ name="calendar.view.dayendhour"
+ type="int"/>
+ <preference id="calendar.view.visiblehours"
+ name="calendar.view.visiblehours"
+ type="int"/>
+ <preference id="calendar.weeks.inview"
+ name="calendar.weeks.inview"
+ type="int"/>
+ <preference id="calendar.previousweeks.inview"
+ name="calendar.previousweeks.inview"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.calendar.view.allview.caption;"/>
+ <hbox>
+ <hbox align="center" flex="1">
+ <label value="&pref.weekstarts.label;"
+ accesskey="&pref.weekstarts.accesskey;"
+ control="weekstarts"/>
+ <menulist id="weekstarts"
+ preference="calendar.week.start"
+ oncommand="gViewsPane.updateViewWorkDayCheckboxes(this.value)">
+ <menupopup id="weekstartspopup">
+ <menuitem label="&day.1.name;" value="0"/>
+ <menuitem label="&day.2.name;" value="1"/>
+ <menuitem label="&day.3.name;" value="2"/>
+ <menuitem label="&day.4.name;" value="3"/>
+ <menuitem label="&day.5.name;" value="4"/>
+ <menuitem label="&day.6.name;" value="5"/>
+ <menuitem label="&day.7.name;" value="6"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox align="center" flex="1">
+ <checkbox id="weekNumber"
+ crop="end"
+ label="&pref.calendar.view-minimonth.showweeknumber.label;"
+ accesskey="&pref.calendar.view-minimonth.showweeknumber.accesskey;"
+ preference="calendar.view-minimonth.showWeekNumber"/>
+ </hbox>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.calendar.view.workweek.caption;"/>
+ <label value="&pref.daysoff.label;"/>
+ <hbox>
+ <checkbox id="dayoff0"
+ class="dayOffCheckbox"
+ label="&day.1.Ddd;"
+ accesskey="&day.1.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d0sundaysoff"/>
+ <checkbox id="dayoff1"
+ class="dayOffCheckbox"
+ label="&day.2.Ddd;"
+ accesskey="&day.2.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d1mondaysoff"/>
+ <checkbox id="dayoff2"
+ class="dayOffCheckbox"
+ label="&day.3.Ddd;"
+ accesskey="&day.3.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d2tuesdaysoff"/>
+ <checkbox id="dayoff3"
+ class="dayOffCheckbox"
+ label="&day.4.Ddd;"
+ accesskey="&day.4.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d3wednesdaysoff"/>
+ <checkbox id="dayoff4"
+ class="dayOffCheckbox"
+ label="&day.5.Ddd;"
+ accesskey="&day.5.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d4thursdaysoff"/>
+ <checkbox id="dayoff5"
+ class="dayOffCheckbox"
+ label="&day.6.Ddd;"
+ accesskey="&day.6.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d5fridaysoff"/>
+ <checkbox id="dayoff6"
+ class="dayOffCheckbox"
+ label="&day.7.Ddd;"
+ accesskey="&day.7.Ddd.accesskey;"
+ orient="vertical"
+ preference="calendar.week.d6saturdaysoff"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.calendar.view.dayandweekviews.caption;"/>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&pref.calendar.view.daystart.label;"
+ accesskey="&pref.calendar.view.daystart.accesskey;"
+ control="daystarthour"/>
+ <menulist id="daystarthour"
+ oncommand="gViewsPane.updateViewEndMenu(this.value);"
+ preference="calendar.view.daystarthour">
+ <menupopup id="daystarthourpopup">
+ <menuitem id="timeStart0" label="&time.midnight;" value="0"/>
+ <menuitem id="timeStart1" value="1"/>
+ <menuitem id="timeStart2" value="2"/>
+ <menuitem id="timeStart3" value="3"/>
+ <menuitem id="timeStart4" value="4"/>
+ <menuitem id="timeStart5" value="5"/>
+ <menuitem id="timeStart6" value="6"/>
+ <menuitem id="timeStart7" value="7"/>
+ <menuitem id="timeStart8" value="8"/>
+ <menuitem id="timeStart9" value="9"/>
+ <menuitem id="timeStart10" value="10"/>
+ <menuitem id="timeStart11" value="11"/>
+ <menuitem id="timeStart12" label="&time.noon;" value="12"/>
+ <menuitem id="timeStart13" value="13"/>
+ <menuitem id="timeStart14" value="14"/>
+ <menuitem id="timeStart15" value="15"/>
+ <menuitem id="timeStart16" value="16"/>
+ <menuitem id="timeStart17" value="17"/>
+ <menuitem id="timeStart18" value="18"/>
+ <menuitem id="timeStart19" value="19"/>
+ <menuitem id="timeStart20" value="20"/>
+ <menuitem id="timeStart21" value="21"/>
+ <menuitem id="timeStart22" value="22"/>
+ <menuitem id="timeStart23" value="23"/>
+ </menupopup>
+ </menulist>
+ <hbox align="center" pack="center">
+ <label value="&pref.calendar.view.visiblehours.label;"
+ accesskey="&pref.calendar.view.visiblehours.accesskey;"
+ control="visiblehours"/>
+ <menulist id="visiblehours"
+ preference="calendar.view.visiblehours">
+ <menupopup id="visiblehourspopup">
+ <menuitem label="1" value="1"/>
+ <menuitem label="2" value="2"/>
+ <menuitem label="3" value="3"/>
+ <menuitem label="4" value="4"/>
+ <menuitem label="5" value="5"/>
+ <menuitem label="6" value="6"/>
+ <menuitem label="7" value="7"/>
+ <menuitem label="8" value="8"/>
+ <menuitem label="9" value="9"/>
+ <menuitem label="10" value="10"/>
+ <menuitem label="11" value="11"/>
+ <menuitem label="12" value="12"/>
+ <menuitem label="13" value="13"/>
+ <menuitem label="14" value="14"/>
+ <menuitem label="15" value="15"/>
+ <menuitem label="16" value="16"/>
+ <menuitem label="17" value="17"/>
+ <menuitem label="18" value="18"/>
+ <menuitem label="19" value="19"/>
+ <menuitem label="20" value="20"/>
+ <menuitem label="21" value="21"/>
+ <menuitem label="22" value="22"/>
+ <menuitem label="23" value="23"/>
+ <menuitem label="24" value="24"/>
+ </menupopup>
+ </menulist>
+ <label value="&pref.calendar.view.visiblehoursend.label;"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <label value="&pref.calendar.view.dayend.label;"
+ accesskey="&pref.calendar.view.dayend.accesskey;"
+ control="dayendhour"/>
+ <menulist id="dayendhour"
+ oncommand="gViewsPane.updateViewStartMenu(this.value);"
+ preference="calendar.view.dayendhour">
+ <menupopup id="dayendhourpopup">
+ <menuitem id="timeEnd1" value="1"/>
+ <menuitem id="timeEnd2" value="2"/>
+ <menuitem id="timeEnd3" value="3"/>
+ <menuitem id="timeEnd4" value="4"/>
+ <menuitem id="timeEnd5" value="5"/>
+ <menuitem id="timeEnd6" value="6"/>
+ <menuitem id="timeEnd7" value="7"/>
+ <menuitem id="timeEnd8" value="8"/>
+ <menuitem id="timeEnd9" value="9"/>
+ <menuitem id="timeEnd10" value="10"/>
+ <menuitem id="timeEnd11" value="11"/>
+ <menuitem id="timeEnd12" label="&time.noon;" value="12"/>
+ <menuitem id="timeEnd13" value="13"/>
+ <menuitem id="timeEnd14" value="14"/>
+ <menuitem id="timeEnd15" value="15"/>
+ <menuitem id="timeEnd16" value="16"/>
+ <menuitem id="timeEnd17" value="17"/>
+ <menuitem id="timeEnd18" value="18"/>
+ <menuitem id="timeEnd19" value="19"/>
+ <menuitem id="timeEnd20" value="20"/>
+ <menuitem id="timeEnd21" value="21"/>
+ <menuitem id="timeEnd22" value="22"/>
+ <menuitem id="timeEnd23" value="23"/>
+ <menuitem id="timeEnd24" label="&time.midnight;" value="24"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox id="viewsMultiweekGroupbox">
+ <caption label="&pref.calendar.view.multiweekview.caption;"/>
+ <hbox align="center">
+ <label value="&pref.numberofweeks.label;"
+ accesskey="&pref.numberofweeks.accesskey;"
+ control="viewsMultiweekTotalWeeks"/>
+ <menulist id="viewsMultiweekTotalWeeks"
+ preference="calendar.weeks.inview">
+ <menupopup>
+ <menuitem label="&pref.numberofweeks.1;" value="1"/>
+ <menuitem label="&pref.numberofweeks.2;" value="2"/>
+ <menuitem label="&pref.numberofweeks.3;" value="3"/>
+ <menuitem label="&pref.numberofweeks.4;" value="4"/>
+ <menuitem label="&pref.numberofweeks.5;" value="5"/>
+ <menuitem label="&pref.numberofweeks.6;" value="6"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox align="center" id="previousWeeksBox">
+ <label value="&pref.numberofpreviousweeks.label;"
+ accesskey="&pref.numberofpreviousweeks.accesskey;"
+ control="viewsMultiweekPreviousWeeks"/>
+ <menulist id="viewsMultiweekPreviousWeeks"
+ preference="calendar.previousweeks.inview">
+ <menupopup>
+ <menuitem label="&pref.numberofweeks.0;" value="0"/>
+ <menuitem label="&pref.numberofweeks.1;" value="1"/>
+ <menuitem label="&pref.numberofweeks.2;" value="2"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ </vbox>
+</overlay>
diff --git a/calendar/base/content/today-pane.js b/calendar/base/content/today-pane.js
new file mode 100644
index 000000000..057e1608c
--- /dev/null
+++ b/calendar/base/content/today-pane.js
@@ -0,0 +1,482 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/**
+ * Namespace object to hold functions related to the today pane.
+ */
+var TodayPane = {
+ paneViews: null,
+ start: null,
+ cwlabel: null,
+ previousMode: null,
+ switchCounter: 0,
+ minidayTimer: null,
+ minidayDrag: {
+ startX: 0,
+ startY: 0,
+ distance: 0,
+ session: false
+ },
+
+ /**
+ * Load Handler, sets up the today pane controls.
+ */
+ onLoad: function() {
+ TodayPane.paneViews = [cal.calGetString("calendar", "eventsandtasks"),
+ cal.calGetString("calendar", "tasksonly"),
+ cal.calGetString("calendar", "eventsonly")];
+ agendaListbox.setupCalendar();
+ TodayPane.initializeMiniday();
+ TodayPane.setShortWeekdays();
+
+ document.getElementById("modeBroadcaster").addEventListener("DOMAttrModified", TodayPane.onModeModified, false);
+ TodayPane.setTodayHeader();
+
+ document.getElementById("today-splitter").addEventListener("command", onCalendarViewResize, false);
+ TodayPane.updateSplitterState();
+ TodayPane.previousMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ TodayPane.showTodayPaneStatusLabel();
+ },
+
+ /**
+ * Unload handler, cleans up the today pane on window unload.
+ */
+ onUnload: function() {
+ document.getElementById("modeBroadcaster").removeEventListener("DOMAttrModified", TodayPane.onModeModified, false);
+ document.getElementById("today-splitter").removeEventListener("command", onCalendarViewResize, false);
+ },
+
+ /**
+ * Sets up the label for the switcher that allows switching between today pane
+ * views. (event+task, task only, event only)
+ */
+ setTodayHeader: function() {
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ let agendaIsVisible = document.getElementById("agenda-panel").isVisible(currentMode);
+ let todoIsVisible = document.getElementById("todo-tab-panel").isVisible(currentMode);
+ let index = 2;
+ if (agendaIsVisible && todoIsVisible) {
+ index = 0;
+ } else if (!agendaIsVisible && todoIsVisible) {
+ index = 1;
+ } else if (agendaIsVisible && !todoIsVisible) {
+ index = 2;
+ } else { // agendaIsVisible == false && todoIsVisible == false:
+ // In this case something must have gone wrong
+ // - probably in the previous session - and no pane is displayed.
+ // We set a default by only displaying agenda-pane.
+ agendaIsVisible = true;
+ document.getElementById("agenda-panel").setVisible(agendaIsVisible);
+ index = 2;
+ }
+ let todayHeader = document.getElementById("today-pane-header");
+ todayHeader.setAttribute("index", index);
+ todayHeader.setAttribute("value", this.paneViews[index]);
+ let todayPaneSplitter = document.getElementById("today-pane-splitter");
+ setBooleanAttribute(todayPaneSplitter, "hidden", index != 0);
+ let todayIsVisible = document.getElementById("today-pane-panel").isVisible();
+
+ // Disable or enable the today pane menuitems that have an attribute
+ // name="minidisplay" depending on the visibility of elements.
+ let menu = document.getElementById("ltnTodayPaneMenuPopup");
+ if (menu) {
+ setAttributeToChildren(menu, "disabled", !todayIsVisible || !agendaIsVisible, "name", "minidisplay");
+ }
+
+ onCalendarViewResize();
+ },
+
+ /**
+ * Sets up the miniday display in the today pane.
+ */
+ initializeMiniday: function() {
+ // initialize the label denoting the current month, year and calendarweek
+ // with numbers that are supposed to consume the largest width
+ // in order to guarantee that the text will not be cropped when modified
+ // during runtime
+ const kYEARINIT = "5555";
+ const kCALWEEKINIT = "55";
+ let monthdisplaydeck = document.getElementById("monthNameContainer");
+ let childNodes = monthdisplaydeck.childNodes;
+
+ for (let i = 0; i < childNodes.length; i++) {
+ let monthlabel = childNodes[i];
+ this.setMonthDescription(monthlabel, i, kYEARINIT, kCALWEEKINIT);
+ }
+
+ let now = cal.now();
+ // Workaround for bug 1070491. Show the correct month and year
+ // after startup even if deck's selectedIndex is reset to 0.
+ this.setMonthDescription(childNodes[0], now.month, now.year, kCALWEEKINIT);
+
+ agendaListbox.addListener(this);
+ this.setDay(now);
+ },
+
+ /**
+ * Go to month/week/day views when double-clicking a label inside miniday
+ */
+ onDoubleClick: function(aEvent) {
+ if (aEvent.button == 0) {
+ if (aEvent.target.id == "datevalue-label") {
+ switchCalendarView("day", true);
+ } else if (aEvent.target.parentNode.id == "weekdayNameContainer") {
+ switchCalendarView("day", true);
+ } else if (aEvent.target.id == "currentWeek-label") {
+ switchCalendarView("week", true);
+ } else if (aEvent.target.parentNode.id == "monthNameContainer") {
+ switchCalendarView("month", true);
+ } else {
+ return;
+ }
+ let title = document.getElementById("calendar-tab-button")
+ .getAttribute("tooltiptext");
+ document.getElementById("tabmail").openTab("calendar", { title: title });
+ currentView().goToDay(agendaListbox.today.start);
+ }
+ },
+
+ /**
+ * Set conditions about start dragging on day-label or start switching
+ * with time on navigation buttons.
+ */
+ onMousedown: function(aEvent, aDir) {
+ if (aEvent.button != 0) {
+ return;
+ }
+ let element = aEvent.target;
+ if (element.id == "previous-day-button" ||
+ element.id == "next-day-button") {
+ // Start switching days by pressing, without release, the navigation buttons
+ element.addEventListener("mouseout", TodayPane.stopSwitching, false);
+ element.addEventListener("mouseup", TodayPane.stopSwitching, false);
+ TodayPane.minidayTimer = setTimeout(TodayPane.updateAdvanceTimer.bind(TodayPane, Event, aDir), 500);
+ } else if (element.id == "datevalue-label") {
+ // Start switching days by dragging the mouse with a starting point on the day label
+ window.addEventListener("mousemove", TodayPane.onMousemove, false);
+ window.addEventListener("mouseup", TodayPane.stopSwitching, false);
+ TodayPane.minidayDrag.startX = aEvent.clientX;
+ TodayPane.minidayDrag.startY = aEvent.clientY;
+ }
+ },
+
+ /**
+ * Figure out the mouse distance from the center of the day's label
+ * to the current position.
+ *
+ * NOTE: This function is usually called without the correct this pointer.
+ */
+ onMousemove: function(aEvent) {
+ const MIN_DRAG_DISTANCE_SQ = 49;
+ let x = aEvent.clientX - TodayPane.minidayDrag.startX;
+ let y = aEvent.clientY - TodayPane.minidayDrag.startY;
+ if (TodayPane.minidayDrag.session) {
+ if (x * x + y * y >= MIN_DRAG_DISTANCE_SQ) {
+ let distance = Math.floor(Math.sqrt(x * x + y * y) - Math.sqrt(MIN_DRAG_DISTANCE_SQ));
+ // Dragging on the left/right side, the day date decrease/increase
+ TodayPane.minidayDrag.distance = (x > 0) ? distance : -distance;
+ } else {
+ TodayPane.minidayDrag.distance = 0;
+ }
+ } else if (x * x + y * y > 9) {
+ // move the mouse a bit before starting the drag session
+ window.addEventListener("mouseout", TodayPane.stopSwitching, false);
+ TodayPane.minidayDrag.session = true;
+ let dragCenterImage = document.getElementById("dragCenter-image");
+ dragCenterImage.removeAttribute("hidden");
+ // Move the starting point in the center so we have a fixed
+ // point where stopping the day switching while still dragging
+ let centerObj = dragCenterImage.boxObject;
+ TodayPane.minidayDrag.startX = Math.floor(centerObj.x + centerObj.width / 2);
+ TodayPane.minidayDrag.startY = Math.floor(centerObj.y + centerObj.height / 2);
+
+ TodayPane.updateAdvanceTimer();
+ }
+ },
+
+ /**
+ * Figure out the days switching speed according to the position (when
+ * dragging) or time elapsed (when pressing buttons).
+ */
+ updateAdvanceTimer: function(aEvent, aDir) {
+ const INITIAL_TIME = 400;
+ const REL_DISTANCE = 8;
+ const MINIMUM_TIME = 100;
+ const ACCELERATE_COUNT_LIMIT = 7;
+ const SECOND_STEP_TIME = 200;
+ if (TodayPane.minidayDrag.session) {
+ // Dragging the day label: days switch with cursor distance and time.
+ let dir = (TodayPane.minidayDrag.distance > 0) - (TodayPane.minidayDrag.distance < 0);
+ TodayPane.advance(dir);
+ let distance = Math.abs(TodayPane.minidayDrag.distance);
+ // Linear relation between distance and switching speed
+ let timeInterval = Math.max(Math.ceil(INITIAL_TIME - distance * REL_DISTANCE), MINIMUM_TIME);
+ TodayPane.minidayTimer = setTimeout(TodayPane.updateAdvanceTimer.bind(TodayPane, null, null), timeInterval);
+ } else {
+ // Keeping pressed next/previous day buttons causes days switching (with
+ // three levels higher speed after some commutations).
+ TodayPane.advance(parseInt(aDir, 10));
+ TodayPane.switchCounter++;
+ let timeInterval = INITIAL_TIME;
+ if (TodayPane.switchCounter > 2 * ACCELERATE_COUNT_LIMIT) {
+ timeInterval = MINIMUM_TIME;
+ } else if (TodayPane.switchCounter > ACCELERATE_COUNT_LIMIT) {
+ timeInterval = SECOND_STEP_TIME;
+ }
+ TodayPane.minidayTimer = setTimeout(TodayPane.updateAdvanceTimer.bind(TodayPane, aEvent, aDir), timeInterval);
+ }
+ },
+
+ /**
+ * Stop automatic days switching when releasing the mouse button or the
+ * position is outside the window.
+ *
+ * NOTE: This function is usually called without the correct this pointer.
+ */
+ stopSwitching: function(aEvent) {
+ let element = aEvent.target;
+ if (TodayPane.minidayDrag.session &&
+ aEvent.type == "mouseout" &&
+ element.id != "messengerWindow") {
+ return;
+ }
+ if (TodayPane.minidayTimer) {
+ clearTimeout(TodayPane.minidayTimer);
+ delete TodayPane.minidayTimer;
+ if (TodayPane.switchCounter == 0 && !TodayPane.minidayDrag.session) {
+ let dir = element.getAttribute("dir");
+ TodayPane.advance(parseInt(dir, 10));
+ }
+ }
+ if (element.id == "previous-day-button" ||
+ element.id == "next-day-button") {
+ TodayPane.switchCounter = 0;
+ let button = document.getElementById(element.id);
+ button.removeEventListener("mouseout", TodayPane.stopSwitching, false);
+ }
+ if (TodayPane.minidayDrag.session) {
+ window.removeEventListener("mouseout", TodayPane.stopSwitching, false);
+ TodayPane.minidayDrag.distance = 0;
+ document.getElementById("dragCenter-image").setAttribute("hidden", "true");
+ TodayPane.minidayDrag.session = false;
+ }
+ window.removeEventListener("mousemove", TodayPane.onMousemove, false);
+ window.removeEventListener("mouseup", TodayPane.stopSwitching, false);
+ },
+
+ /**
+ * Helper function to set the month description on the today pane header.
+ *
+ * @param aMonthLabel The XUL node to set the month label on.
+ * @param aIndex The month number, 0-based.
+ * @param aYear The year this month should be displayed with
+ * @param aCalWeek The calendar week that should be shown.
+ * @return The value set on aMonthLabel.
+ */
+ setMonthDescription: function(aMonthLabel, aIndex, aYear, aCalWeek) {
+ if (this.cwlabel == null) {
+ this.cwlabel = cal.calGetString("calendar", "shortcalendarweek");
+ }
+ document.getElementById("currentWeek-label").value = this.cwlabel + " " + aCalWeek;
+ aMonthLabel.value = cal.getDateFormatter().shortMonthName(aIndex) + " " + aYear;
+ return aMonthLabel.value;
+ },
+
+ /**
+ * Cycle the view shown in the today pane (event+task, event, task).
+ *
+ * @param aCycleForward If true, the views are cycled in the forward
+ * direction, otherwise in the opposite direction
+ */
+ cyclePaneView: function(aCycleForward) {
+ if (this.paneViews == null) {
+ return;
+ }
+ let index = parseInt(document.getElementById("today-pane-header").getAttribute("index"), 10);
+ index = index + aCycleForward;
+ let nViewLen = this.paneViews.length;
+ if (index >= nViewLen) {
+ index = 0;
+ } else if (index == -1) {
+ index = nViewLen - 1;
+ }
+ let agendaPanel = document.getElementById("agenda-panel");
+ let todoPanel = document.getElementById("todo-tab-panel");
+ let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ let isTodoPanelVisible = (index != 2 && todoPanel.isVisibleInMode(currentMode));
+ let isAgendaPanelVisible = (index != 1 && agendaPanel.isVisibleInMode(currentMode));
+ todoPanel.setVisible(isTodoPanelVisible);
+ agendaPanel.setVisible(isAgendaPanelVisible);
+ this.setTodayHeader();
+ },
+
+ /**
+ * Shows short weekday names in the weekdayNameContainer
+ */
+ setShortWeekdays: function() {
+ let weekdisplaydeck = document.getElementById("weekdayNameContainer");
+ let childNodes = weekdisplaydeck.childNodes;
+
+ // Workaround for bug 1070491. Show the correct weekday after
+ // startup even if deck's selectedIndex is reset to 0.
+ let weekday = cal.now().weekday + 1;
+ childNodes[0].setAttribute("value", cal.calGetString("dateFormat", "day." + weekday + ".Mmm"));
+
+ for (let i = 1; i < childNodes.length; i++) {
+ childNodes[i].setAttribute("value", cal.calGetString("dateFormat", "day." + i + ".Mmm"));
+ }
+ },
+
+ /**
+ * Sets the shown date from a JSDate.
+ *
+ * @param aNewDate The date to show.
+ */
+ setDaywithjsDate: function(aNewDate) {
+ let newdatetime = cal.jsDateToDateTime(aNewDate, cal.floating());
+ newdatetime = newdatetime.getInTimezone(cal.calendarDefaultTimezone());
+ this.setDay(newdatetime, true);
+ },
+
+ /**
+ * Sets the first day shown in the today pane.
+ *
+ * @param aNewDate The calIDateTime to set.
+ * @param aDontUpdateMinimonth If true, the minimonth will not be
+ * updated to show the same date.
+ */
+ setDay: function(aNewDate, aDontUpdateMinimonth) {
+ this.start = aNewDate.clone();
+
+ let daylabel = document.getElementById("datevalue-label");
+ daylabel.value = this.start.day;
+
+ let weekdaylabel = document.getElementById("weekdayNameContainer");
+ weekdaylabel.selectedIndex = this.start.weekday + 1;
+
+ let monthnamedeck = document.getElementById("monthNameContainer");
+ monthnamedeck.selectedIndex = this.start.month;
+
+ let selMonthPanel = monthnamedeck.selectedPanel;
+ this.setMonthDescription(selMonthPanel,
+ this.start.month,
+ this.start.year,
+ cal.getWeekInfoService().getWeekTitle(this.start));
+ if (!aDontUpdateMinimonth) {
+ document.getElementById("today-Minimonth").value = cal.dateTimeToJsDate(this.start);
+ }
+ this.updatePeriod();
+ },
+
+ /**
+ * Advance by a given number of days in the today pane.
+ *
+ * @param aDir The number of days to advance. Negative numbers advance
+ * backwards in time.
+ */
+ advance: function(aDir) {
+ if (aDir != 0) {
+ this.start.day += aDir;
+ this.setDay(this.start);
+ }
+ },
+
+ /**
+ * Checks if the today pane is showing today's date.
+ */
+ showsToday: function() {
+ return cal.sameDay(cal.now(), this.start);
+ },
+
+ /**
+ * Update the period headers in the agenda listbox using the today pane's
+ * start date.
+ */
+ updatePeriod: function() {
+ agendaListbox.refreshPeriodDates(this.start.clone());
+ updateCalendarToDoUnifinder();
+ },
+
+ /**
+ * Display a certain section in the minday/minimonth part of the todaypane.
+ *
+ * @param aSection The section to display
+ */
+ displayMiniSection: function(aSection) {
+ document.getElementById("today-minimonth-box").setVisible(aSection == "minimonth");
+ document.getElementById("mini-day-box").setVisible(aSection == "miniday");
+ document.getElementById("today-none-box").setVisible(aSection == "none");
+ setBooleanAttribute(document.getElementById("today-Minimonth"), "freebusy", aSection == "minimonth");
+ },
+
+ /**
+ * Handler function for the DOMAttrModified event used to observe the
+ * todaypane-splitter.
+ *
+ * @param aEvent The DOM event occurring on attribute modification.
+ */
+ onModeModified: function(aEvent) {
+ if (aEvent.attrName == "mode") {
+ let todaypane = document.getElementById("today-pane-panel");
+ // Store the previous mode panel's width.
+ todaypane.setModeAttribute("modewidths", todaypane.width, TodayPane.previousMode);
+
+ TodayPane.setTodayHeader();
+ TodayPane.updateSplitterState();
+ todaypane.width = todaypane.getModeAttribute("modewidths", "width");
+ TodayPane.previousMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+ }
+ },
+
+ /**
+ * Toggle the today-pane and update its visual appearance.
+ *
+ * @param aEvent The DOM event occurring on activated command.
+ */
+ toggleVisibility: function(aEvent) {
+ document.getElementById("today-pane-panel").togglePane(aEvent);
+ TodayPane.setTodayHeader();
+ TodayPane.updateSplitterState();
+ },
+
+ /**
+ * Update the today-splitter state and today-pane width with saved
+ * mode-dependent values.
+ */
+ updateSplitterState: function() {
+ let splitter = document.getElementById("today-splitter");
+ let todaypaneVisible = document.getElementById("today-pane-panel").isVisible();
+ setElementValue(splitter, !todaypaneVisible && "true", "hidden");
+ if (todaypaneVisible) {
+ splitter.setAttribute("state", "open");
+ }
+ },
+
+ /**
+ * Generates the todaypane toggle command when the today-splitter
+ * is being collapsed or uncollapsed.
+ */
+ onCommandTodaySplitter: function() {
+ let todaypane = document.getElementById("today-pane-panel");
+ let splitter = document.getElementById("today-splitter");
+ let splitterCollapsed = splitter.getAttribute("state") == "collapsed";
+
+ if (splitterCollapsed == todaypane.isVisible()) {
+ document.getElementById("calendar_toggle_todaypane_command").doCommand();
+ }
+ },
+
+ /**
+ * Checks if the todayPaneStatusLabel should be hidden.
+ */
+ showTodayPaneStatusLabel: function() {
+ let attributeValue = Preferences.get("calendar.view.showTodayPaneStatusLabel", true) && "false";
+ setElementValue(document.getElementById("calendar-status-todaypane-button"), !attributeValue, "hideLabel");
+ }
+};
+
+window.addEventListener("load", TodayPane.onLoad, false);
+window.addEventListener("unload", TodayPane.onUnload, false);
diff --git a/calendar/base/content/today-pane.xul b/calendar/base/content/today-pane.xul
new file mode 100644
index 000000000..53dd71145
--- /dev/null
+++ b/calendar/base/content/today-pane.xul
@@ -0,0 +1,293 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://lightning/locale/lightning.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://messenger/locale/messenger.dtd" > %dtd3;
+ <!ENTITY % dtd4 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd4;
+ <!ENTITY % dtd5 SYSTEM "chrome://global/locale/global.dtd" > %dtd5;
+ <!ENTITY % dtd6 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd6;
+]>
+
+<?xml-stylesheet href="chrome://calendar/skin/today-pane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/widgets/calendar-widget-bindings.css" type="text/css"?>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/javascript" src="chrome://calendar/content/today-pane.js"/>
+<script type="application/javascript" src="chrome://calendar/content/agenda-listbox.js"/>
+<script type="application/javascript" src="chrome://calendar/content/calendar-management.js"/>
+<script type="application/javascript" src="chrome://calendar/content/calendar-dnd-listener.js"/>
+<script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/>
+
+ <modevbox id="today-pane-panel"
+ mode="mail,calendar,task" modewidths="200,200,200" modesplitterstates="open,open,open"
+ refcontrol="calendar_toggle_todaypane_command"
+ broadcaster="modeBroadcaster" persist="modewidths">
+ <sidebarheader align="center">
+ <label id ="today-pane-header"/>
+ <spacer flex="1"/>
+ <modehbox mode="mail,calendar" broadcaster="modeBroadcaster">
+ <toolbarbutton id="today-pane-cycler-prev"
+ dir="prev"
+ class="today-pane-cycler"
+ oncommand="TodayPane.cyclePaneView(-1);"/>
+ <toolbarbutton id="today-pane-cycler-next"
+ dir="next"
+ class="today-pane-cycler"
+ oncommand="TodayPane.cyclePaneView(1);"/>
+ </modehbox>
+ <spacer id="buttonspacer"/>
+ <toolbarbutton id="today-closer" class="today-closebutton close-icon"
+ oncommand="document.getElementById('today-pane-panel').setVisible(false, true, true);
+ TodayPane.setTodayHeader();
+ TodayPane.updateSplitterState();"/>
+ </sidebarheader>
+ <vbox flex="1">
+ <modevbox id="agenda-panel"
+ flex="1"
+ mode="mail,calendar,task"
+ collapsedinmodes="calendar"
+ persist="collapsed height collapsedinmodes"
+ broadcaster="modeBroadcaster">
+ <modebox id="today-none-box"
+ mode="mail,calendar,task"
+ collapsedinmodes="mail,calendar,task"
+ broadcaster="modeBroadcaster"
+ refcontrol="ltnTodayPaneDisplayNone"
+ persist="collapsedinmodes"/>
+ <modehbox id="today-minimonth-box"
+ pack="center"
+ class="today-subpane"
+ mode="mail,calendar,task"
+ broadcaster="modeBroadcaster"
+ collapsedinmodes="mail,calendar,task"
+ refcontrol="ltnTodayPaneDisplayMinimonth"
+ persist="collapsedinmodes">
+ <minimonth id="today-Minimonth" freebusy="true" onchange="TodayPane.setDaywithjsDate(this.value);"/>
+ </modehbox>
+ <modebox id="mini-day-box"
+ mode="mail,calendar,task"
+ class="today-subpane"
+ refcontrol="ltnTodayPaneDisplayMiniday"
+ broadcaster="modeBroadcaster"
+ collapsedinmodes=""
+ persist="collapsedinmodes"
+ onDOMMouseScroll="TodayPane.advance(event.detail > 0 ? 1 : -1);">
+ <stack flex="1">
+ <image id="mini-day-image" flex="1"/>
+ <hbox flex="1">
+ <stack id="dateContainer">
+ <hbox pack="center"
+ align="center">
+ <label id="datevalue-label" class="dateValue"
+ ondblclick="TodayPane.onDoubleClick(event);"
+ onmousedown="TodayPane.onMousedown(event);"/>
+ </hbox>
+ <hbox flex="1" pack="center" align="center" mousethrough="always">
+ <image id="dragCenter-image" hidden="true"/>
+ </hbox>
+ </stack>
+ <vbox flex="1">
+ <hbox pack="center">
+ <deck id="weekdayNameContainer" pack="center"
+ ondblclick="TodayPane.onDoubleClick(event);">
+ <label/><!-- workaround for bug 1070491-->
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ </deck>
+ <spacer id="weekspacer" flex="1"/>
+ <hbox pack="end">
+ <toolbarbutton id="previous-day-button"
+ class="miniday-nav-buttons"
+ tooltiptext="&onedaybackward.tooltip;"
+ onmousedown="TodayPane.onMousedown(event, parseInt(this.getAttribute('dir')));"
+ dir="-1"/>
+ <toolbarbutton id="today-button"
+ class="miniday-nav-buttons"
+ tooltiptext="&showToday.tooltip;"
+ oncommand="TodayPane.setDay(now());"/>
+ <toolbarbutton id="next-day-button"
+ class="miniday-nav-buttons"
+ tooltiptext="&onedayforward.tooltip;"
+ onmousedown="TodayPane.onMousedown(event, parseInt(this.getAttribute('dir')));"
+ dir="1"/>
+ </hbox>
+ </hbox>
+ <hbox pack="start">
+ <deck id ="monthNameContainer" class="monthlabel"
+ ondblclick="TodayPane.onDoubleClick(event);">
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ <label/>
+ </deck>
+ <label id="currentWeek-label" class="monthlabel"
+ ondblclick="TodayPane.onDoubleClick(event);"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+ <toolbarbutton id="miniday-dropdown-button"
+ tooltiptext="&showselectedday.tooltip;"
+ type="menu">
+ <panel id="miniday-month-panel" position="after_end">
+ <minimonth id="todayMinimonth"
+ flex="1"
+ onchange="TodayPane.setDaywithjsDate(this.value);
+ document.getElementById('miniday-month-panel').hidePopup();"/>
+ </panel>
+ </toolbarbutton>
+ </hbox>
+ </stack>
+ </modebox>
+ <vbox flex="1">
+ <hbox id="agenda-toolbar" iconsize="small">
+ <toolbarbutton id="todaypane-new-event-button"
+ mode="mail"
+ iconsize="small"
+ orient="horizontal"
+ label="&calendar.newevent.button.label;"
+ tooltiptext="&calendar.newevent.button.tooltip;"
+ oncommand="agendaListbox.createNewEvent(event)">
+ <observes element="calendar_new_event_command" attribute="disabled"/>
+ </toolbarbutton>
+ </hbox>
+ <vbox id="richlistitem-container" hidden="true">
+ <agenda-checkbox-richlist-item id="today-header-hidden"
+ title="&calendar.today.button.label;"
+ checked="true"
+ persist="checked"/>
+ <agenda-checkbox-richlist-item id="tomorrow-header-hidden"
+ title="&calendar.tomorrow.button.label;"
+ checked="false"
+ persist="checked"/>
+ <agenda-checkbox-richlist-item id="nextweek-header-hidden"
+ title="&calendar.upcoming.button.label;"
+ checked="false"
+ persist="checked"/>
+ </vbox>
+ <richlistbox id="agenda-listbox" flex="1" context="_child"
+ onblur="agendaListbox.onBlur();"
+ onfocus="agendaListbox.onFocus();"
+ onkeypress="agendaListbox.onKeyPress(event);"
+ ondblclick="agendaListbox.createNewEvent(event);"
+ ondragstart="nsDragAndDrop.startDrag(event, calendarCalendarButtonDNDObserver);"
+ ondragover="nsDragAndDrop.dragOver(event, calendarCalendarButtonDNDObserver);"
+ ondrop="nsDragAndDrop.drop(event, calendarCalendarButtonDNDObserver);">
+ <menupopup id="agenda-menupopup" onpopupshowing="return agendaListbox.setupContextMenu(event.target)">
+ <menuitem label="&calendar.context.modifyorviewitem.label;"
+ accesskey="&calendar.context.modifyorviewitem.accesskey;"
+ observes="agenda_edit_event_command"/>
+ <menu id="agenda-context-menu-convert-menu"
+ label="&calendar.context.convertmenu.label;"
+ accesskey="&calendar.context.convertmenu.accesskey.calendar;">
+ <menupopup id="agenda-context-menu-convert-menupopup">
+ <menuitem id="agenda-context-menu-convert-message-menuitem"
+ label="&calendar.context.convertmenu.message.label;"
+ accesskey="&calendar.context.convertmenu.message.accesskey;"
+ oncommand="calendarMailButtonDNDObserver.onDropItems(agendaListbox.getSelectedItems())"/>
+ <menuitem id="agenda-context-menu-convert-task-menuitem"
+ class="event-only"
+ label="&calendar.context.convertmenu.task.label;"
+ accesskey="&calendar.context.convertmenu.task.accesskey;"
+ oncommand="calendarTaskButtonDNDObserver.onDropItems(agendaListbox.getSelectedItems())"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="calendar-today-pane-menuseparator-before-delete"/>
+ <menuitem label="&calendar.context.deleteevent.label;"
+ accesskey="&calendar.context.deleteevent.accesskey;"
+ key="calendar-delete-item-key"
+ observes="agenda_delete_event_command"/>
+ <menu id="calendar-today-pane-menu-attendance-menu"
+ class="attendance-menu"
+ label="&calendar.context.attendance.menu.label;"
+ accesskey="&calendar.context.attendance.menu.accesskey;"
+ oncommand="setContextPartstat(event.target.value, event.target.getAttribute('scope'), agendaListbox.getSelectedItems({}))"
+ observes="calendar_attendance_command">
+ <menupopup id="calendar-today-pane-menu-attendance-menupopup">
+ <label id="calendar-today-pane-attendance-thisoccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.occurrence.label;"/>
+ <menuitem id="calendar-today-pane-menu-attend-accept-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-today-pane-attendance"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="calendar-today-pane-menu-attend-tentative-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-today-pane-attendance"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="calendar-today-pane-menu-attend-declined-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-today-pane-attendance"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="calendar-today-pane-menu-attend-needsaction-menuitem"
+ type="radio"
+ scope="this-occurrence"
+ name="calendar-today-pane-attendance"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ <label id="calendar-today-pane-attendance-alloccurrence-label"
+ class="calendar-context-heading-label"
+ scope="all-occurrences"
+ value="&calendar.context.attendance.all.label;"/>
+ <menuitem id="calendar-today-pane-menu-attend-accept-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-today-pane-attendance-all"
+ label="&read.only.accept.label;" value="ACCEPTED"/>
+ <menuitem id="calendar-today-pane-menu-attend-tentative-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-today-pane-attendance-all"
+ label="&read.only.tentative.label;" value="TENTATIVE"/>
+ <menuitem id="calendar-today-pane-menu-attend-declined-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-today-pane-attendance-all"
+ label="&read.only.decline.label;" value="DECLINED"/>
+ <menuitem id="calendar-today-pane-menu-attend-needsaction-all-menuitem"
+ type="radio"
+ scope="all-occurrences"
+ name="calendar-today-pane-attendance-all"
+ label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </richlistbox>
+ </vbox>
+ </modevbox>
+ <splitter id="today-pane-splitter" persist="hidden"/>
+ <modevbox id="todo-tab-panel" flex="1" mode="mail,calendar"
+ collapsedinmodes="mail,task"
+ broadcaster="modeBroadcaster"
+ persist="height collapsedinmodes"
+ ondragstart="nsDragAndDrop.startDrag(event, calendarTaskButtonDNDObserver);"
+ ondragover="nsDragAndDrop.dragOver(event, calendarTaskButtonDNDObserver);"
+ ondrop="nsDragAndDrop.drop(event, calendarTaskButtonDNDObserver);"/>
+ </vbox>
+ </modevbox>
+
+ <commandset id="calendar_commands">
+ <command id="calendar_toggle_todaypane_command" oncommand="TodayPane.toggleVisibility(event)"/>
+ </commandset>
+</overlay>
diff --git a/calendar/base/content/widgets/calendar-alarm-widget.xml b/calendar/base/content/widgets/calendar-alarm-widget.xml
new file mode 100644
index 000000000..13c559b73
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-alarm-widget.xml
@@ -0,0 +1,351 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
+]>
+
+<bindings id="calendar-alarms"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="calendar-alarm-widget" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <resources>
+ <stylesheet src="chrome://global/skin/button.css"/>
+ <stylesheet src="chrome://calendar/skin/calendar-alarm-dialog.css"/>
+ </resources>
+
+ <content orient="horizontal">
+ <xul:vbox pack="start">
+ <xul:image class="alarm-calendar-image"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label anonid="alarm-title-label" class="alarm-title-label" crop="end"/>
+ <xul:vbox class="additional-information-box">
+ <xul:label anonid="alarm-date-label" class="alarm-date-label"/>
+ <xul:hbox>
+ <xul:label anonid="alarm-location-label" class="location-label">&calendar.alarm.location.label;</xul:label>
+ <xul:description anonid="alarm-location-description"
+ class="alarm-location-description"
+ crop="end"
+ flex="1"/>
+ </xul:hbox>
+ <xul:hbox pack="start">
+ <xul:label class="text-link alarm-details-label"
+ value="&calendar.alarm.details.label;"
+ onclick="showDetails(event)"
+ onkeypress="showDetails(event)"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:vbox>
+ <xul:spacer flex="1"/>
+ <xul:label anonid="alarm-relative-date-label" class="alarm-relative-date-label"/>
+ <xul:vbox class="alarm-action-buttons" pack="center">
+ <xul:button anonid="alarm-snooze-button"
+ type="menu"
+ label="&calendar.alarm.snoozefor.label;">
+ <xul:menupopup type="snooze-menupopup" ignorekeys="true"/>
+ </xul:button>
+ <xul:button anonid="alarm-dismiss-button"
+ label="&calendar.alarm.dismiss.label;"
+ oncommand="dismissAlarm()"/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ ]]></constructor>
+
+ <field name="mItem">null</field>
+ <field name="mAlarm">null</field>
+
+ <property name="item"
+ onget="return this.mItem;"
+ onset="this.mItem = val; this.updateLabels(); return val;"/>
+ <property name="alarm"
+ onget="return this.mAlarm;"
+ onset="this.mAlarm = val; this.updateLabels(); return val;"/>
+
+ <method name="updateLabels">
+ <body><![CDATA[
+ if (!this.mItem || !this.mAlarm) {
+ // Setup not complete, do nothing for now.
+ return;
+ }
+
+ let formatter = cal.getDateFormatter();
+ let titleLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-title-label");
+ let locationDescription = document.getAnonymousElementByAttribute(this, "anonid", "alarm-location-description");
+ let dateLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-date-label");
+
+ // Dates
+ if (cal.isEvent(this.mItem)) {
+ dateLabel.textContent = formatter.formatItemInterval(this.mItem);
+ } else if (cal.isToDo(this.mItem)) {
+ let startDate = this.mItem.entryDate || this.mItem.dueDate;
+ if (startDate) {
+ // A Task with a start or due date, show with label
+ startDate = startDate.getInTimezone(cal.calendarDefaultTimezone());
+ dateLabel.textContent = calGetString("calendar",
+ "alarmStarts",
+ [formatter.formatDateTime(startDate)]);
+ } else {
+ // If the task has no start date, then format the alarm date.
+ dateLabel.textContent = formatter.formatDateTime(this.mAlarm.alarmDate);
+ }
+ } else {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Relative date
+ this.updateRelativeDateLabel();
+
+ // Title, location
+ titleLabel.textContent = this.mItem.title || "";
+ locationDescription.textContent = this.mItem.getProperty("LOCATION") || "";
+ locationDescription.hidden = (locationDescription.textContent.length < 1);
+
+ document.getAnonymousElementByAttribute(this, "anonid", "alarm-location-label").hidden =
+ (locationDescription.textContent.length < 1);
+ ]]></body>
+ </method>
+
+ <method name="updateRelativeDateLabel">
+ <body><![CDATA[
+ let formatter = cal.getDateFormatter();
+ let item = this.mItem;
+ let relativeDateLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-relative-date-label");
+ let relativeDateString;
+ let startDate = item[calGetStartDateProp(item)] || item[calGetEndDateProp(item)];
+ if (startDate) {
+ startDate = startDate.getInTimezone(calendarDefaultTimezone());
+ let currentDate = now();
+ let sinceDayStart = (currentDate.hour * 3600) + (currentDate.minute * 60);
+
+ currentDate.second = 0;
+ startDate.second = 0;
+
+ let sinceAlarm = currentDate.subtractDate(startDate).inSeconds;
+ this.mAlarmToday = (sinceAlarm < sinceDayStart) && (sinceAlarm > sinceDayStart - 86400);
+
+ if (this.mAlarmToday) {
+ // The alarm is today
+ relativeDateString = calGetString("calendar",
+ "alarmTodayAt",
+ [formatter.formatTime(startDate)]);
+ } else if (sinceAlarm <= sinceDayStart - 86400 && sinceAlarm > sinceDayStart - 172800) {
+ // The alarm is tomorrow
+ relativeDateString = calGetString("calendar",
+ "alarmTomorrowAt",
+ [formatter.formatTime(startDate)]);
+ } else if (sinceAlarm < sinceDayStart + 86400 && sinceAlarm > sinceDayStart) {
+ // The alarm is yesterday
+ relativeDateString = calGetString("calendar",
+ "alarmYesterdayAt",
+ [formatter.formatTime(startDate)]);
+ } else {
+ // The alarm is way back
+ relativeDateString = [formatter.formatDateTime(startDate)];
+ }
+ } else {
+ // No start or end date, therefore the alarm must be absolute and
+ // have an alarm date.
+ relativeDateString = [formatter.formatDateTime(this.mAlarm.alarmDate)];
+ }
+
+ relativeDateLabel.textContent = relativeDateString;
+ ]]></body>
+ </method>
+
+ <method name="showDetails">
+ <parameter name="event"/>
+ <body><![CDATA[
+ if (event.type == "click" ||
+ (event.type == "keypress" &&
+ event.keyCode == event.DOM_VK_RETURN)) {
+ let detailsEvent = document.createEvent("Events");
+ detailsEvent.initEvent("itemdetails", true, false);
+ this.dispatchEvent(detailsEvent);
+ }
+ ]]></body>
+ </method>
+
+ <method name="dismissAlarm">
+ <body><![CDATA[
+ let dismissEvent = document.createEvent("Events");
+ dismissEvent.initEvent("dismiss", true, false);
+ this.dispatchEvent(dismissEvent);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-snooze-popup">
+ <content ignorekeys="true">
+ <xul:menuitem label="&calendar.alarm.snooze.5minutes.label;"
+ value="5"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.10minutes.label;"
+ value="10"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.15minutes.label;"
+ value="15"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.30minutes.label;"
+ value="30"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.45minutes.label;"
+ value="45"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.1hour.label;"
+ value="60"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.2hours.label;"
+ value="120"
+ oncommand="snoozeItem(event)"/>
+ <xul:menuitem label="&calendar.alarm.snooze.1day.label;"
+ value="1440"
+ oncommand="snoozeItem(event)"/>
+ <children/>
+ <xul:menuseparator/>
+ <xul:hbox class="snooze-options-box">
+ <xul:textbox anonid="snooze-value-textbox"
+ oninput="updateAccessibleName()"
+ onselect="updateAccessibleName()"
+ type="number"
+ size="3"/>
+ <xul:menulist anonid="snooze-unit-menulist"
+ class="snooze-unit-menulist menuitem-non-iconic"
+ allowevents="true">
+ <xul:menupopup anonid="snooze-unit-menupopup"
+ position="after_start"
+ ignorekeys="true"
+ class="menulist-menupopup">
+ <xul:menuitem closemenu="single" class="unit-menuitem" value="1"/>
+ <xul:menuitem closemenu="single" class="unit-menuitem" value="60"/>
+ <xul:menuitem closemenu="single" class="unit-menuitem" value="1440"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:toolbarbutton anonid="snooze-popup-ok"
+ class="snooze-popup-button snooze-popup-ok-button"
+ oncommand="snoozeOk()"/>
+ <xul:toolbarbutton anonid="snooze-popup-cancel"
+ class="snooze-popup-button snooze-popup-cancel-button"
+ aria-label="&calendar.alarm.snooze.cancel;"
+ oncommand="snoozeCancel()"/>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ let snoozePref = Preferences.get("calendar.alarms.defaultsnoozelength", 0);
+ if (snoozePref <= 0) {
+ snoozePref = 5;
+ }
+
+ let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
+ let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
+
+ let selectedIndex = 0;
+ if ((snoozePref % 60) == 0) {
+ snoozePref = snoozePref / 60;
+ if ((snoozePref % 24) == 0) {
+ snoozePref = snoozePref / 24;
+ selectedIndex = 2; // Days
+ } else {
+ selectedIndex = 1; // Hours
+ }
+ } else {
+ selectedIndex = 0; // Minutes
+ }
+
+ unitList.selectedIndex = selectedIndex;
+ unitValue.value = snoozePref;
+
+ updateAccessibleName();
+ ]]></constructor>
+
+ <method name="snoozeAlarm">
+ <parameter name="minutes"/>
+ <body><![CDATA[
+ let snoozeEvent = document.createEvent("Events");
+ snoozeEvent.initEvent("snooze", true, false);
+ snoozeEvent.detail = minutes;
+
+ // The onsnooze attribute is set on the menupopup, this binding is
+ // instanciated on the menupopup's arrowscrollbox. Therefore we need
+ // to go up one node.
+ let handler = this.parentNode.getAttribute("onsnooze");
+ let cancel = false;
+ if (handler) {
+ let func = new Function("event", handler);
+ cancel = (func.call(this, snoozeEvent) === false);
+ }
+
+ if (!cancel) {
+ this.dispatchEvent(snoozeEvent);
+ }
+ ]]></body>
+ </method>
+
+ <method name="snoozeItem">
+ <parameter name="event"/>
+ <body><![CDATA[
+ this.snoozeAlarm(event.target.value);
+ ]]></body>
+ </method>
+
+ <method name="snoozeOk">
+ <body><![CDATA[
+ let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
+ let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
+
+ let minutes = (unitList.value || 1) * unitValue.value;
+ this.snoozeAlarm(minutes);
+ ]]></body>
+ </method>
+
+ <method name="snoozeCancel">
+ <body><![CDATA[
+ this.parentNode.hidePopup();
+ ]]></body>
+ </method>
+
+ <method name="updateAccessibleName">
+ <body><![CDATA[
+ let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
+ let unitPopup = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menupopup");
+ let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
+ let okButton = document.getAnonymousElementByAttribute(this, "anonid", "snooze-popup-ok");
+
+ function unitName(list) {
+ return { 1: "unitMinutes", 60: "unitHours", 1440: "unitDays" }[list.value] || "unitMinutes";
+ }
+
+ let pluralString = cal.calGetString("calendar", unitName(unitList));
+ let unitPlural = PluralForm.get(unitValue.value, pluralString)
+ .replace("#1", unitValue.value);
+
+ let accessibleString = cal.calGetString("calendar-alarms",
+ "reminderSnoozeOkA11y",
+ [unitPlural]);
+ okButton.setAttribute("aria-label", accessibleString);
+
+ let items = unitPopup.getElementsByTagName("xul:menuitem");
+ for (let menuItem of items) {
+ pluralString = cal.calGetString("calendar", unitName(menuItem));
+ menuItem.label = PluralForm.get(unitValue.value, pluralString)
+ .replace("#1", "").trim();
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/widgets/calendar-list-tree.xml b/calendar/base/content/widgets/calendar-list-tree.xml
new file mode 100644
index 000000000..a91fc873f
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-list-tree.xml
@@ -0,0 +1,1110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://calendar/locale/calendar.dtd">
+
+<bindings id="calendar-list-tree-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <binding id="full-calendar-list-tree" extends="#calendar-list-tree">
+ <!--
+ - This binding implements a full calendar list, that automatically adds
+ - and removes calendars when a calendar is registered or unregistered.
+ -->
+ <implementation>
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ let calMgr = cal.getCalendarManager();
+ calMgr.addObserver(this.calMgrObserver);
+
+ ]]></constructor>
+ <destructor><![CDATA[
+ let calMgr = cal.getCalendarManager();
+ calMgr.removeObserver(this.calMgrObserver);
+ this.calMgrObserver.listTree = null;
+ ]]></destructor>
+
+ <field name="mAddingFromComposite">false</field>
+
+ <property name="compositeCalendar">
+ <getter><![CDATA[
+ if (!this.mCompositeCalendar) {
+ throw Components.Exception("Calendar list has no composite calendar yet",
+ Components.results.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this.mCompositeCalendar;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mCompositeCalendar = val;
+ this.mCompositeCalendar.addObserver(this.compositeObserver);
+
+ // Now that we have a composite calendar, we can get all calendars
+ // from the calendar manager.
+ this.mAddingFromComposite = true;
+ let calendars = sortCalendarArray(getCalendarManager().getCalendars({}));
+ calendars.forEach(this.addCalendar, this);
+ this.mAddingFromComposite = false;
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="calendars">
+ <getter><![CDATA[
+ return this.mCalendarList;
+ ]]></getter>
+ <setter><![CDATA[
+ // Setting calendars externally is not wanted. This is done internally
+ // in the compositeCalendar setter.
+ throw Components.Exception("Seting calendars on type='full' is not supported",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ ]]></setter>
+ </property>
+
+ <field name="calMgrObserver"><![CDATA[
+ ({
+ listTree: this,
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICalendarManagerObserver]),
+
+ // calICalendarManagerObserver
+ onCalendarRegistered: function(aCalendar) {
+ this.listTree.addCalendar(aCalendar);
+ let composite = this.listTree.compositeCalendar;
+ let inComposite = aCalendar.getProperty(composite.prefPrefix +
+ "-in-composite");
+ if ((inComposite === null) || inComposite) {
+ composite.addCalendar(aCalendar);
+ }
+ },
+
+ onCalendarUnregistering: function(aCalendar) {
+ this.listTree.removeCalendar(aCalendar);
+ },
+
+ onCalendarDeleting: function(aCalendar) {
+ // Now that the calendar is unregistered, update the commands to
+ // make sure that New Event/Task commands are correctly
+ // enabled/disabled.
+ document.commandDispatcher.updateCommands("calendar_commands");
+ }
+ })
+ ]]></field>
+ <field name="compositeObserver"><![CDATA[
+ ({
+ listTree: this,
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICompositeObserver,
+ Components.interfaces.calIObserver]),
+
+ // calICompositeObserver
+ onCalendarAdded: function(aCalendar) {
+ // Make sure the checkbox state is updated
+ this.listTree.updateCalendar(aCalendar);
+ },
+
+ onCalendarRemoved: function(aCalendar) {
+ // Make sure the checkbox state is updated
+ this.listTree.updateCalendar(aCalendar);
+ },
+
+ onDefaultCalendarChanged: function(aCalendar) {
+ },
+
+ // calIObserver
+ onStartBatch: function() { },
+ onEndBatch: function() { },
+ onLoad: function() { },
+
+ onAddItem: function(aItem) {
+ if (aItem.calendar.type != "caldav") {
+ this.listTree.ensureCalendarVisible(aItem.calendar);
+ }
+ },
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (aNewItem.calendar.type != "caldav") {
+ this.listTree.ensureCalendarVisible(aNewItem.calendar);
+ }
+ },
+ onDeleteItem: function(aDeletedItem) { },
+ onError: function(aCalendar, aErrNo, aMessage) { },
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "disabled":
+ case "readOnly":
+ calendarUpdateNewItemsCommand();
+ document.commandDispatcher.updateCommands("calendar_commands");
+ break;
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ }
+ })
+ ]]></field>
+ </implementation>
+ <handlers>
+ <handler event="dblclick"><![CDATA[
+ let col = {};
+ let calendar = this.getCalendarFromEvent(event, col);
+ if (event.button != 0 ||
+ (col.value && col.value.element &&
+ col.value.element.getAttribute("anonid") == "checkbox-treecol")) {
+ // Only left clicks that are not on the checkbox column
+ return;
+ }
+ if (calendar) {
+ openCalendarProperties(calendar);
+ } else {
+ openCalendarWizard();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="calendar-list-tree">
+ <content>
+ <xul:tree anonid="tree"
+ xbl:inherits="hidecolumnpicker"
+ hidecolumnpicker="true"
+ seltype="single"
+ flex="1">
+ <xul:treecols anonid="treecols"
+ xbl:inherits="hideheader"
+ hideheader="true">
+ <xul:treecol anonid="checkbox-treecol"
+ xbl:inherits="cycler,hideheader"
+ cycler="true"
+ hideheader="true"
+ width="17"/>
+ <xul:treecol anonid="color-treecol"
+ xbl:inherits="cycler,hideheader"
+ hideheader="true"
+ width="16"/>
+ <xul:treecol anonid="calendarname-treecol"
+ xbl:inherits="cycler,hideheader"
+ hideheader="true"
+ label="&calendar.unifinder.tree.calendarname.label;"
+ flex="1"/>
+ <xul:treecol anonid="status-treecol"
+ xbl:inherits="cycler,hideheader"
+ hideheader="true"
+ width="18"/>
+ <children includes="treecol"/>
+ <xul:treecol anonid="scrollbar-spacer"
+ xbl:inherits="cycler,hideheader"
+ fixed="true"
+ hideheader="true">
+ <!-- This is a very elegant workaround to make sure the last column
+ is not covered by the scrollbar in case of an overflow. This
+ treecol needs to be here last -->
+ <xul:slider anonid="scrollbar-slider" orient="vertical"/>
+ </xul:treecol>
+ </xul:treecols>
+ <xul:treechildren anonid="treechildren"
+ xbl:inherits="tooltip=childtooltip,context=childcontext"
+ tooltip="_child"
+ context="_child"
+ ondragstart="onDragStart(event);"
+ onoverflow="displayScrollbarSpacer(true)"
+ onunderflow="displayScrollbarSpacer(false)">
+ <children includes="tooltip|menupopup"/>
+ </xul:treechildren>
+ </xul:tree>
+ </content>
+ <implementation implements="nsITreeView">
+
+ <field name="mCalendarList">[]</field>
+ <field name="mCompositeCalendar">null</field>
+ <field name="tree">null</field>
+ <field name="treebox">null</field>
+ <field name="ruleCache">null</field>
+ <field name="mCachedSheet">null</field>
+
+ <field name="mCycleCalendarFlag">null</field>
+ <field name="mCycleTimer">null</field>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ this.tree.view = this;
+ this.ruleCache = {};
+ this.mCycleCalendarFlag = {};
+ ]]></constructor>
+ <destructor><![CDATA[
+ // Clean up the calendar manager observers. Do not use removeCalendar
+ // here since that will remove the calendar from the composite calendar.
+ for (let calendar of this.mCalendarList) {
+ calendar.removeObserver(this.calObserver);
+ }
+
+ this.tree.view = null;
+ this.calObserver.listTree = null;
+
+ if (this.mCompositeCalendar) {
+ this.mCompositeCalendar.removeObserver(this.compositeObserver);
+ }
+ ]]></destructor>
+
+ <field name="calObserver"><![CDATA[
+ ({
+ listTree: this,
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIObserver]),
+
+ // calIObserver. Note that each registered calendar uses this observer
+ onStartBatch: function() { },
+ onEndBatch: function() { },
+ onLoad: function() { },
+
+ onAddItem: function(aItem) { },
+ onModifyItem: function(aNewItem, aOldItem) { },
+ onDeleteItem: function(aDeletedItem) { },
+ onError: function(aCalendar, aErrNo, aMessage) { },
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "color":
+ // TODO See other TODO in this file about updateStyleSheetForViews
+ if ("updateStyleSheetForViews" in window) {
+ updateStyleSheetForViews(aCalendar);
+ }
+ this.listTree.updateCalendarColor(aCalendar);
+ // Fall through, update item in any case
+ case "name":
+ case "currentStatus":
+ case "readOnly":
+ case "disabled":
+ this.listTree.updateCalendar(aCalendar);
+ // Fall through, update commands in any cases.
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ // Since the old value is not used directly in onPropertyChanged,
+ // but should not be the same as the value, set it to a different
+ // value.
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ }
+ })
+ ]]></field>
+
+ <field name="compositeObserver"><![CDATA[
+ ({
+ listTree: this,
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICompositeObserver]),
+
+ // calICompositeObserver
+ onCalendarAdded: function(aCalendar) {
+ // Make sure the checkbox state is updated
+ this.listTree.updateCalendar(aCalendar);
+ },
+
+ onCalendarRemoved: function(aCalendar) {
+ // Make sure the checkbox state is updated
+ this.listTree.updateCalendar(aCalendar);
+ },
+
+ onDefaultCalendarChanged: function(aCalendar) {
+ }
+ })
+ ]]></field>
+
+ <property name="treechildren"
+ readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'treechildren')"/>
+ <property name="tree"
+ readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'tree')"/>
+
+
+ <property name="sheet" readonly="true">
+ <getter><![CDATA[
+ if (!this.mCachedSheet) {
+ for (let sheet of document.styleSheets) {
+ if (sheet.href == "chrome://calendar/skin/calendar-management.css") {
+ this.mCachedSheet = sheet;
+ break;
+ }
+ }
+ if (!this.mCachedSheet) {
+ cal.ERROR("Could not find calendar-management.css, needs to be added to " +
+ window.document.title + "'s stylesheets");
+ }
+ }
+
+ return this.mCachedSheet;
+ ]]></getter>
+ </property>
+
+ <property name="calendars">
+ <getter><![CDATA[
+ return this.mCalendarList;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mCalendarList = val;
+ this.mCalendarList.forEach(this.addCalendar, this);
+ return this.mCalendarList;
+ ]]></setter>
+ </property>
+
+ <property name="compositeCalendar">
+ <getter><![CDATA[
+ if (!this.mCompositeCalendar) {
+ this.mCompositeCalendar =
+ Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
+ .createInstance(Components.interfaces.calICompositeCalendar);
+ }
+
+ return this.mCompositeCalendar;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.mCompositeCalendar) {
+ throw Components.Exception("A composite calendar has already been set",
+ Components.results.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ this.mCompositeCalendar = val;
+ this.mCompositeCalendar.addObserver(this.compositeObserver);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="sortOrder"
+ readonly="true"
+ onget="return this.mCalendarList.map(x => x.id);"/>
+ <property name="selectedCalendars"
+ readonly="true"
+ onget="return this.compositeCalendar.getCalendars({});"/>
+ <property name="allowDrag"
+ onget="return (this.getAttribute('allowdrag') == 'true');"
+ onset="return setBooleanAttribute(this, 'allowdrag', val);"/>
+ <property name="writable"
+ onget="return (this.getAttribute('writable') == 'true');"
+ onset="return setBooleanAttribute(this, 'writable', val);"/>
+ <property name="ignoreDisabledState"
+ onget="return (this.getAttribute('ignoredisabledstate') == 'true');"
+ onset="return setBooleanAttribute(this, 'ignoredisabledstate', val);"/>
+
+ <method name="sortOrderChanged">
+ <parameter name=""/>
+ <body><![CDATA[
+ if (this.mAddingFromComposite) {
+ return;
+ }
+ let event = document.createEvent("Events");
+ event.initEvent("SortOrderChanged", true, false);
+ event.sortOrder = this.sortOrder;
+ this.dispatchEvent(event);
+
+ let handler = this.getAttribute("onSortOrderChanged");
+ if (handler) {
+ // Call the given code in a function
+ let func = new Function("event", handler);
+ func(event);
+ }
+ ]]></body>
+ </method>
+ <method name="displayScrollbarSpacer">
+ <parameter name="aShouldDisplay"/>
+ <body><![CDATA[
+ let spacer = document.getAnonymousElementByAttribute(this, "anonid", "scrollbar-spacer");
+ spacer.collapsed = !aShouldDisplay;
+ ]]></body>
+ </method>
+
+ <method name="ensureCalendarVisible">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ this.compositeCalendar.addCalendar(aCalendar);
+ ]]></body>
+ </method>
+
+ <method name="getColumn">
+ <parameter name="aAnonId"/>
+ <body><![CDATA[
+ let colElem = document.getAnonymousElementByAttribute(this, "anonid", aAnonId);
+ return this.treebox.columns.getColumnFor(colElem);
+ ]]></body>
+ </method>
+
+ <method name="findIndexById">
+ <!--
+ - Find the array index of the calendar with the passed id.
+ -
+ - @param aId The calendar id to find an index for.
+ - @return The array index, or -1 if not found.
+ -->
+ <parameter name="aId"/>
+ <body><![CDATA[
+ for (let i = 0; i < this.mCalendarList.length; i++) {
+ if (this.mCalendarList[i].id == aId) {
+ return i;
+ }
+ }
+ return -1;
+ ]]></body>
+ </method>
+
+ <method name="addCalendar">
+ <!--
+ - Add a calendar to the calendar list
+ -
+ - @param aCalendar The calendar to add.
+ -->
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let composite = this.compositeCalendar;
+
+ let initialSortOrderPos = aCalendar.getProperty("initialSortOrderPos");
+ if (initialSortOrderPos != null && initialSortOrderPos < this.mCalendarList.length) {
+ // Insert the calendar at the requested sort order position
+ // and then discard the property
+ this.mCalendarList.splice(initialSortOrderPos, 0, aCalendar);
+ aCalendar.deleteProperty("initialSortOrderPos");
+ } else {
+ this.mCalendarList.push(aCalendar);
+ }
+ this.treebox.rowCountChanged(this.mCalendarList.length - 1, 1);
+
+ if (!composite.defaultCalendar ||
+ aCalendar.id == composite.defaultCalendar.id) {
+ this.tree.view.selection.select(this.mCalendarList.length - 1);
+ }
+
+ this.updateCalendarColor(aCalendar);
+
+ // TODO This should be done only once outside of this binding, but to
+ // do that right, we need to have an easy way to register an observer
+ // all calendar properties. This could be the calendar manager that
+ // holds an observer on every calendar anyway, which would then use the
+ // global observer service which clients can register with.
+ if ("updateStyleSheetForViews" in window) {
+ updateStyleSheetForViews(aCalendar);
+ }
+
+ // Watch the calendar for changes, i.e color.
+ aCalendar.addObserver(this.calObserver);
+
+ // Adding a calendar causes the sortorder to be changed.
+ this.sortOrderChanged();
+
+ // Re-assign defaultCalendar, sometimes it is not the right one after
+ // remove & add calendar.
+ if (composite.defaultCalendar && this.tree.currentIndex > -1) {
+ let currentCal = this.getCalendar(this.tree.currentIndex);
+ if (composite.defaultCalendar.id != currentCal.id) {
+ composite.defaultCalendar = currentCal;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="removeCalendar">
+ <!--
+ - Remove a calendar from the calendar list
+ -
+ - @param aCalendar The calendar to remove.
+ -->
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let index = this.findIndexById(aCalendar.id);
+ if (index < 0) {
+ return;
+ }
+
+ this.mCalendarList.splice(index, 1);
+ if (index == this.rowCount) {
+ index--;
+ }
+
+ this.tree.view.selection.select(index + 1);
+ this.treebox.rowCountChanged(index, -1);
+
+ aCalendar.removeObserver(this.calObserver);
+
+ // Make sure the calendar is removed from the composite calendar
+ this.compositeCalendar.removeCalendar(aCalendar);
+
+ // Remove the css style rule from the sheet.
+ let sheet = this.sheet;
+ for (let i = 0; i < sheet.cssRules.length; i++) {
+ if (sheet.cssRules[i] == this.ruleCache[aCalendar.id]) {
+ sheet.deleteRule(i);
+ delete this.ruleCache[aCalendar.id];
+ break;
+ }
+ }
+
+ this.sortOrderChanged();
+ ]]></body>
+ </method>
+
+ <method name="updateCalendar">
+ <!--
+ - Update a calendar's tree row (to refresh the color and such)
+ -
+ - @param aCalendar The calendar to update.
+ -->
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ this.treebox.invalidateRow(this.findIndexById(aCalendar.id));
+ ]]></body>
+ </method>
+
+ <method name="updateCalendarColor">
+ <!--
+ - Update a calendar's color rules.
+ -
+ - @param aCalendar The calendar to update.
+ -->
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let color = aCalendar.getProperty("color") || "#a8c2e1";
+ let sheet = this.sheet;
+ if (!(aCalendar.id in this.ruleCache)) {
+ let ruleString = "calendar-list-tree > tree > treechildren" +
+ "::-moz-tree-cell(color-treecol, id-" +
+ aCalendar.id + ") {}";
+
+ let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
+ this.ruleCache[aCalendar.id] = sheet.cssRules[ruleIndex];
+ }
+ this.ruleCache[aCalendar.id].style.backgroundColor = color;
+ ]]></body>
+ </method>
+
+ <method name="getCalendarFromEvent">
+ <!--
+ - Get the calendar from the given DOM event. This can be a Mouse event or a
+ - keyboard event.
+ -
+ - @param event The DOM event to check
+ - @param aCol An out-object for the column id.
+ - @param aRow An out-object for the row index.
+ -->
+ <parameter name="event"/>
+ <parameter name="aCol"/>
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ if (event.clientX && event.clientY) {
+ // If we have a client point, get the row directly from the client
+ // point.
+ aRow = aRow || {};
+ this.treebox.getCellAt(event.clientX,
+ event.clientY,
+ aRow,
+ aCol || {},
+ {});
+ } else if (document.popupNode && document.popupNode.contextCalendar) {
+ // Otherwise, we can try to get the context calendar from the popupNode.
+ return document.popupNode.contextCalendar;
+ }
+ return aRow && aRow.value > -1 && this.mCalendarList[aRow.value];
+ ]]></body>
+ </method>
+
+ <method name="getCalendar">
+ <!--
+ - Get the calendar from a certain index.
+ -
+ - @param aIndex The index to get the calendar for.
+ -->
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ let index = Math.max(0, Math.min(this.mCalendarList.length - 1, aIndex));
+ return this.mCalendarList[index];
+ ]]></body>
+ </method>
+
+ <!-- Implement nsITreeView -->
+ <property name="rowCount"
+ readonly="true"
+ onget="return this.mCalendarList.length"/>
+
+ <method name="getCellProperties">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ try {
+ let rowProps = this.getRowProperties(aRow);
+ let colProps = this.getColumnProperties(aCol);
+ return rowProps + (rowProps && colProps ? " " : "") + colProps;
+ } catch (e) {
+ // It seems errors in these functions are not shown, do this
+ // explicitly.
+ cal.ERROR("Error getting cell props: " + e);
+ return "";
+ }
+ ]]></body>
+ </method>
+
+ <method name="getRowProperties">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ let properties = [];
+ let calendar = this.getCalendar(aRow);
+ let composite = this.compositeCalendar;
+
+ // Set up the composite calendar status
+ properties.push(composite.getCalendarById(calendar.id) ? "checked" : "unchecked");
+
+ // Set up the calendar id
+ properties.push("id-" + calendar.id);
+
+ // Get the calendar color
+ let color = (calendar.getProperty("color") || "").substr(1);
+
+ // Set up the calendar color (background)
+ properties.push("color-" + (color || "default"));
+
+ // Set a property to get the contrasting text color (foreground)
+ properties.push(cal.getContrastingTextColor(color || "a8c2e1"));
+
+ let currentStatus = calendar.getProperty("currentStatus");
+ if (!Components.isSuccessCode(currentStatus)) {
+ // 'readfailed' is supposed to "win" over 'readonly', meaning that
+ // if reading from a calendar fails there is no further need to also display
+ // information about 'readonly' status
+ properties.push("readfailed");
+ } else if (calendar.readOnly) {
+ properties.push("readonly");
+ }
+
+ // Set up the disabled state
+ properties.push(!this.ignoreDisabledState && calendar.getProperty("disabled") ?
+ "disabled" : "enabled");
+
+ return properties.join(" ");
+ ]]></body>
+ </method>
+
+ <method name="getColumnProperties">
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ // Workaround for anonymous treecols
+ return aCol.element.getAttribute("anonid");
+ ]]></body>
+ </method>
+
+ <method name="isContainer">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="isContainerOpen">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="isContainerEmpty">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="isSeparator">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="isSorted">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="onDragStart">
+ <!--
+ - Initiate a drag operation for the calendar list. Can be used in the
+ - dragstart handler.
+ -
+ - @param event The DOM event containing drag information.
+ -->
+ <parameter name="event"/>
+ <body><![CDATA[
+ let calendar = this.getCalendarFromEvent(event);
+ if (this.allowDrag && event.dataTransfer) {
+ // Setting data starts a drag session, do this only if dragging
+ // is enabled for this binding.
+ event.dataTransfer.setData("application/x-moz-calendarID", calendar.id);
+ event.dataTransfer.effectAllowed = "move";
+ }
+ ]]></body>
+ </method>
+
+ <method name="canDrop">
+ <parameter name="aRow"/>
+ <parameter name="aOrientation"/>
+ <body><![CDATA[
+ let dragSession = cal.getDragService().getCurrentSession();
+ let dataTransfer = dragSession && dragSession.dataTransfer;
+ if (!this.allowDrag || !dataTransfer) {
+ // If dragging is not allowed or there is no data transfer then
+ // we can't drop (i.e dropping a file on the calendar list).
+ return false;
+ }
+
+ let dragCalId = dataTransfer.getData("application/x-moz-calendarID");
+
+ return (aOrientation != Components.interfaces.nsITreeView.DROP_ON &&
+ dragCalId != null);
+ ]]></body>
+ </method>
+
+ <method name="drop">
+ <parameter name="aRow"/>
+ <parameter name="aOrientation"/>
+ <body><![CDATA[
+ let dragSession = cal.getDragService().getCurrentSession();
+ let dataTransfer = dragSession.dataTransfer;
+ let dragCalId = dataTransfer &&
+ dataTransfer.getData("application/x-moz-calendarID");
+ if (!this.allowDrag || !dataTransfer || !dragCalId) {
+ return false;
+ }
+
+ let oldIndex = -1;
+ for (let i = 0; i < this.mCalendarList.length; i++) {
+ if (this.mCalendarList[i].id == dragCalId) {
+ oldIndex = i;
+ break;
+ }
+ }
+ if (oldIndex < 0) {
+ return false;
+ }
+
+ // If no row is specified (-1), then assume append.
+ let row = (aRow < 0 ? this.mCalendarList.length - 1 : aRow);
+ let targetIndex = row + Math.max(0, aOrientation);
+
+ // We don't need to move if the target row has the same index as the old
+ // row. The same goes for dropping after the row before the old row or
+ // before the row after the old row. Think about it :-)
+ if (aRow != oldIndex && row + aOrientation != oldIndex) {
+ // Add the new one, remove the old one.
+ this.mCalendarList.splice(targetIndex, 0, this.mCalendarList[oldIndex]);
+ this.mCalendarList.splice(oldIndex + (oldIndex > targetIndex ? 1 : 0), 1);
+
+ // Invalidate the tree rows between the old item and the new one.
+ if (oldIndex < targetIndex) {
+ this.treebox.invalidateRange(oldIndex, targetIndex);
+ } else {
+ this.treebox.invalidateRange(targetIndex, oldIndex);
+ }
+
+ // Fire event
+ this.sortOrderChanged();
+ }
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="foreignDrop">
+ <!--
+ - This function can be used by other nodes to simulate dropping on the
+ - tree. This can be used for example on the tree header so that the row
+ - will be inserted before the first visible row. The event client
+ - coordinate are used to determine if the row should be dropped before the
+ - first row (above treechildren) or below the last visible row (below top
+ - of treechildren).
+ -
+ - @param event The DOM drop event.
+ - @return Boolean indicating if the drop succeeded.
+ -
+ -->
+ <parameter name="event"/>
+ <body><![CDATA[
+ let hasDropped;
+ if (event.clientY < this.tree.boxObject.y) {
+ hasDropped = this.drop(this.treebox.getFirstVisibleRow(), -1);
+ } else {
+ hasDropped = this.drop(this.treebox.getLastVisibleRow(), 1);
+ }
+ if (hasDropped) {
+ event.preventDefault();
+ }
+ return hasDropped;
+ ]]></body>
+ </method>
+
+ <method name="foreignCanDrop">
+ <!--
+ - Similar function to foreignCanDrop but for the dragenter event
+ - @see ::foreignDrop
+ -->
+ <parameter name="event"/>
+ <body><![CDATA[
+ // The dragenter/dragover events expect false to be returned when
+ // dropping is allowed, therefore we return !canDrop.
+ if (event.clientY < this.tree.boxObject.y) {
+ return !this.canDrop(this.treebox.getFirstVisibleRow(), -1);
+ } else {
+ return !this.canDrop(this.treebox.getLastVisibleRow(), 1);
+ }
+ ]]></body>
+ </method>
+
+ <method name="getParentIndex">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return -1;
+ ]]></body>
+ </method>
+
+ <method name="hasNextSibling">
+ <parameter name="aRow"/>
+ <parameter name="aAfterIndex"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="getLevel">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ return 0;
+ ]]></body>
+ </method>
+
+ <method name="getImageSrc">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="getProgressMode">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="getCellValue">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ let calendar = this.getCalendar(aRow);
+ let composite = this.compositeCalendar;
+
+ switch (aCol.element.getAttribute("anonid")) {
+ case "checkbox-treecol":
+ return composite.getCalendarById(calendar.id) ? "true" : "false";
+ case "status-treecol":
+ // The value of this cell shows the calendar readonly state
+ return (calendar.readOnly ? "true" : "false");
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="getCellText">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ switch (aCol.element.getAttribute("anonid")) {
+ case "calendarname-treecol":
+ return this.getCalendar(aRow).name;
+ }
+ return "";
+ ]]></body>
+ </method>
+
+ <method name="setTree">
+ <parameter name="aTreeBox"/>
+ <body><![CDATA[
+ this.treebox = aTreeBox;
+ ]]></body>
+ </method>
+
+ <method name="toggleOpenState">
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="cycleHeader">
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="cycleCell">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ let calendar = this.getCalendar(aRow);
+ if (this.mCycleCalendarFlag[calendar.id]) {
+ delete this.mCycleCalendarFlag[calendar.id];
+ } else {
+ this.mCycleCalendarFlag[calendar.id] = calendar;
+ }
+
+ if (this.mCycleTimer) {
+ clearTimeout(this.mCycleTimer);
+ }
+ this.treebox.invalidateRow(aRow);
+ this.mCycleTimer = setTimeout(this.cycleCellCommit.bind(this), 200);
+ ]]></body>
+ </method>
+
+ <method name="cycleCellCommit">
+ <body><![CDATA[
+ let composite = this.compositeCalendar;
+ try {
+ composite.startBatch();
+ for (let id in this.mCycleCalendarFlag) {
+ if (composite.getCalendarById(id)) {
+ composite.removeCalendar(this.mCycleCalendarFlag[id]);
+ } else {
+ composite.addCalendar(this.mCycleCalendarFlag[id]);
+ }
+ delete this.mCycleCalendarFlag[id];
+ }
+ } finally {
+ composite.endBatch();
+ }
+ ]]></body>
+ </method>
+
+ <method name="isEditable">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="setCellValue">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ let calendar = this.getCalendar(aRow);
+ let composite = this.compositeCalendar;
+
+ switch (aCol.element.getAttribute("anonid")) {
+ case "checkbox-treecol":
+ if (aValue == "true") {
+ composite.addCalendar(calendar);
+ } else {
+ composite.removeCalendar(calendar);
+ }
+ break;
+ default:
+ return null;
+ }
+ return aValue;
+ ]]></body>
+ </method>
+
+ <method name="setCellText">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="performAction">
+ <parameter name="aAction"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="performActionOnRow">
+ <parameter name="aAction"/>
+ <parameter name="aRow"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="performActionOnCell">
+ <parameter name="aAction"/>
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="select"><![CDATA[
+ this.compositeCalendar.defaultCalendar = this.getCalendar(this.tree.currentIndex);
+ ]]></handler>
+
+ <handler event="keypress" keycode="VK_DELETE"><![CDATA[
+ if (this.writable) {
+ promptDeleteCalendar(this.compositeCalendar.defaultCalendar);
+ event.preventDefault();
+ }
+ ]]></handler>
+
+ <!-- use key=" " since keycode="VK_SPACE" doesn't work -->
+ <handler event="keypress" key=" "><![CDATA[
+ if (this.tree.currentIndex > -1) {
+ this.cycleCell(this.tree.currentIndex, this.getColumn("checkbox-treecol"));
+ event.preventDefault();
+ }
+ ]]></handler>
+
+ <handler event="keypress" keycode="VK_DOWN" modifiers="control"><![CDATA[
+ if (!this.allowDrag) {
+ return;
+ }
+
+ let idx = this.tree.currentIndex;
+
+ if (idx < this.mCalendarList.length - 1) {
+ this.mCalendarList.splice(idx + 1, 0, this.mCalendarList.splice(idx, 1)[0]);
+ this.treebox.invalidateRange(idx, idx + 1);
+
+ if (this.tree.view.selection.isSelected(idx)) {
+ this.tree.view.selection.toggleSelect(idx);
+ this.tree.view.selection.toggleSelect(idx + 1);
+ }
+ if (this.tree.view.selection.currentIndex == idx) {
+ this.tree.view.selection.currentIndex = idx + 1;
+ }
+
+ // Fire event
+ this.sortOrderChanged();
+ }
+ // Don't call the default <key> handler.
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="keypress" keycode="VK_UP" modifiers="control"><![CDATA[
+ if (!this.allowDrag) {
+ return;
+ }
+
+ let idx = this.tree.currentIndex;
+ if (idx > 0) {
+ this.mCalendarList.splice(idx - 1, 0, this.mCalendarList.splice(idx, 1)[0]);
+ this.treebox.invalidateRange(idx - 1, idx);
+
+ if (this.tree.view.selection.isSelected(idx)) {
+ this.tree.view.selection.toggleSelect(idx);
+ this.tree.view.selection.toggleSelect(idx - 1);
+ }
+ if (this.tree.view.selection.currentIndex == idx) {
+ this.tree.view.selection.currentIndex = idx - 1;
+ }
+
+ // Fire event
+ this.sortOrderChanged();
+ }
+ // Don't call the default <key> handler.
+ event.preventDefault();
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/widgets/calendar-subscriptions-list.xml b/calendar/base/content/widgets/calendar-subscriptions-list.xml
new file mode 100644
index 000000000..852f2c89f
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-subscriptions-list.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<bindings id="calendar-subscriptions-list-bindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="calendar-subscriptions-richlistbox"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"
+ xbl:inherits="flex">
+
+ <implementation>
+ <method name="addCalendar">
+ <parameter name="aCalendar"/>
+ <parameter name="bSubscribed"/>
+ <body><![CDATA[
+ let newNode = createXULElement("calendar-subscriptions-richlistitem");
+ this.appendChild(newNode);
+ newNode.setAttribute("anonid", "subscriptions-listitem");
+ newNode.calendar = aCalendar;
+ newNode.subscribed = bSubscribed;
+ ]]></body>
+ </method>
+
+ <method name="clear">
+ <body><![CDATA[
+ while (this.hasChildNodes()) {
+ this.lastChild.remove();
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="calendar-subscriptions-richlistitem"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox flex="1">
+ <xul:checkbox anonid="subscription-checkbox" class="calendar-subscriptions-richlistitem-checkbox"/>
+ <xul:label anonid="subscription-name" flex="1" crop="end"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <field name="mCalendar">null</field>
+ <field name="mSubscribed">false</field>
+
+ <property name="calendar">
+ <getter><![CDATA[
+ return this.mCalendar;
+ ]]></getter>
+ <setter><![CDATA[
+ this.setCalendar(val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="subscribed">
+ <getter><![CDATA[
+ return this.mSubscribed;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mSubscribed = val;
+ this.checked = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="checked">
+ <getter><![CDATA[
+ let checkbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "subscription-checkbox");
+ if (checkbox.getAttribute("checked") == "true") {
+ return true;
+ } else {
+ return false;
+ }
+ ]]></getter>
+ <setter><![CDATA[
+ let checkbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "subscription-checkbox");
+ if (val) {
+ checkbox.setAttribute("checked", "true");
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="disabled">
+ <getter><![CDATA[
+ let checkbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "subscription-checkbox");
+ if (checkbox.getAttribute("disabled") == "true") {
+ return true;
+ } else {
+ return false;
+ }
+ ]]></getter>
+ <setter><![CDATA[
+ let checkbox = document.getAnonymousElementByAttribute(
+ this, "anonid", "subscription-checkbox");
+ if (val) {
+ checkbox.setAttribute("disabled", "true");
+ } else {
+ checkbox.removeAttribute("disabled");
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="setCalendar">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ this.mCalendar = aCalendar;
+ let label = document.getAnonymousElementByAttribute(
+ this, "anonid", "subscription-name");
+ label.setAttribute("value", aCalendar.name);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/widgets/calendar-widget-bindings.css b/calendar/base/content/widgets/calendar-widget-bindings.css
new file mode 100644
index 000000000..f3dd4029a
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-widget-bindings.css
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+treenode-checkbox {
+ -moz-binding: url("chrome://calendar/content/widgets/calendar-widgets.xml#treenode-checkbox");
+}
+
+modebox {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#modebox);
+ -moz-user-focus: normal;
+}
+
+modevbox {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#modevbox);
+ -moz-user-focus: normal;
+}
+
+modehbox {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#modehbox);
+ -moz-user-focus: normal;
+}
+
+toolbarbutton[doubleimage="true"] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#doubleimage-toolbarbutton);
+}
+
+toolbarbutton[todaypane="true"] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#todaypane-toolbarbutton);
+}
+
+minimonth {
+ -moz-binding: url("chrome://calendar/content/widgets/minimonth.xml#minimonth");
+}
+
+.minimonth-day {
+ -moz-binding: url("chrome://calendar/content/widgets/minimonth.xml#minimonth-day");
+}
+
+minimonth-header {
+ -moz-binding: url("chrome://calendar/content/widgets/minimonth.xml#active-minimonth-header");
+}
+
+minimonth-header[readonly="true"] {
+ -moz-binding: url("chrome://calendar/content/widgets/minimonth.xml#minimonth-header");
+}
+
+dragndropContainer {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#dragndropContainer);
+}
+
+tab[calview] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#view-tab);
+}
+
+calendar-list-tree {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-list-tree.xml#calendar-list-tree);
+}
+
+calendar-list-tree[type="full"] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-list-tree.xml#full-calendar-list-tree);
+}
+
+menulist[type="panel-menulist"] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#panel-menulist);
+}
+
+panel[type="category-panel"] {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-widgets.xml#category-panel);
+}
diff --git a/calendar/base/content/widgets/calendar-widgets.xml b/calendar/base/content/widgets/calendar-widgets.xml
new file mode 100644
index 000000000..4e2e8e5b1
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-widgets.xml
@@ -0,0 +1,731 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE dialog [
+ <!ENTITY % dtd1 SYSTEM "chrome://global/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd2;
+]>
+
+<bindings id="calendar-widgets"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- Unfortunately, the normal menulist binding doesn't allow a panel child
+ This binding replaces the usual menulist to allow a panel -->
+ <binding id="panel-menulist" extends="chrome://global/content/bindings/menulist.xml#menulist">
+ <content sizetopopup="pref">
+ <xul:hbox class="menulist-label-box" flex="1">
+ <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
+ <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
+ </xul:hbox>
+ <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
+ <children includes="menupopup|panel"/>
+ </content>
+ </binding>
+
+ <binding id="category-panel" extends="chrome://global/content/bindings/popup.xml#panel">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+ <content>
+ <xul:textbox anonid="category-textbox"
+ class="categories-textbox"
+ type="search"
+ searchbutton="true"
+ placeholder="&event.categories.textbox.label;"
+ oncommand="document.getBindingParent(this).addNewCategory();"
+ flex="1"/>
+ <xul:listbox anonid="categories-listbox"
+ class="categories-listbox"
+ onselect="document.getBindingParent(this).selectCategory()"
+ selType="multiple"
+ >
+ <children/>
+ </xul:listbox>
+ </content>
+ <implementation>
+ <field name="_maxCount">0</field>
+
+ <property name="categories" readonly="true">
+ <getter><![CDATA[
+ let categoryListbox = document.getAnonymousElementByAttribute(this, "anonid", "categories-listbox");
+ if (this.maxCount == 1) {
+ let selectedItem = categoryListbox.selectedItem;
+ return selectedItem ? [selectedItem.getAttribute("value")] : [];
+ } else {
+ let checkedNodes = categoryListbox.getElementsByAttribute("checked", "true");
+ let sliceEnd = this.maxCount > 0 ? this.maxCount : checkedNodes.length;
+ return Array.slice(checkedNodes, 0, sliceEnd)
+ .map(x => x.getAttribute("value"));
+ }
+ ]]></getter>
+ </property>
+
+ <property name="maxCount">
+ <getter><![CDATA[
+ return this._maxCount;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._maxCount != val) {
+ this._maxCount = val;
+ this.setupSelection();
+ }
+ ]]></setter>
+ </property>
+
+ <method name="selectCategory">
+ <body><![CDATA[
+ this.setupSelection();
+ if (this.maxCount == 1) {
+ this.hidePopup();
+ }
+ ]]></body>
+ </method>
+
+ <method name="setupSelection">
+ <body><![CDATA[
+ let categoryListbox = document.getAnonymousElementByAttribute(this, "anonid", "categories-listbox");
+ categoryListbox.setAttribute("seltype", this.maxCount == 1 ? "single" : "multiple");
+
+ if (this.maxCount == 1) {
+ for (let node of categoryListbox.childNodes) {
+ // Single selection doesn't have checkboxes
+ node.removeAttribute("type");
+
+ // Even though we have single select, these may be checked
+ // in case the user switches between calendars that support
+ // one vs multiple categories. Uncheck the other nodes to
+ // make sure the UX is not weird.
+ setBooleanAttribute(node, "checked", node == categoryListbox.selectedItem);
+ }
+ } else {
+ let categoryTextbox = document.getAnonymousElementByAttribute(this, "anonid", "category-textbox");
+ let maxCountReached = this.maxCount > 0 && this.categories.length == this.maxCount;
+ setBooleanAttribute(categoryTextbox, "disabled", maxCountReached);
+
+ for (let node of categoryListbox.childNodes) {
+ // Multiselect has checkboxes
+ node.setAttribute("type", "checkbox");
+
+ if (maxCountReached && node.getAttribute("checked") != "true") {
+ // If the maxcount is reached, disable all unchecked items
+ node.setAttribute("disabled", "true");
+ } else if (!maxCountReached) {
+ // If its not reached, remove the disabled attribute
+ node.removeAttribute("disabled");
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="insertCategory">
+ <parameter name="category" />
+ <parameter name="categories" />
+ <parameter name="categoryListbox" />
+ <parameter name="compare" />
+ <body><![CDATA[
+ let newIndex = cal.binaryInsert(categories, category, compare, true);
+ let item = categoryListbox.childNodes[Math.min(newIndex, categoryListbox.childNodes.length - 1)];
+
+ if (!item || item.getAttribute("value") != category) {
+ // The item doesn't exist, insert it at the correct spot.
+ item = categoryListbox.insertItemAt(newIndex, category, category);
+
+ if (this.maxCount != 1) {
+ item.setAttribute("type", "checkbox");
+ }
+ }
+
+ item.setAttribute("checked", "true");
+ return item;
+ ]]></body>
+ </method>
+
+ <method name="addNewCategory">
+ <body><![CDATA[
+ let categoryListbox = document.getAnonymousElementByAttribute(this, "anonid", "categories-listbox");
+ let categoryTextbox = document.getAnonymousElementByAttribute(this, "anonid", "category-textbox");
+ let category = categoryTextbox.value;
+
+ if (!category) {
+ return;
+ }
+
+ let localeCollator = cal.createLocaleCollator();
+ let compare = localeCollator.compareString.bind(localeCollator, 0);
+
+ let children = categoryListbox.childNodes;
+ let categories = [];
+ for (let i = 0; i < children.length; i++) {
+ categories.push(children[i].label);
+ }
+
+ let item = this.insertCategory(category, categories, categoryListbox, compare);
+ categoryTextbox.value = "";
+
+ if (this.maxCount == 1) {
+ categoryListbox.selectedItem = item;
+ } else {
+ this.selectCategory();
+ }
+
+ categoryListbox.ensureElementIsVisible(item);
+ ]]></body>
+ </method>
+
+ <method name="loadItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ let categoryListbox = document.getAnonymousElementByAttribute(this, "anonid", "categories-listbox");
+ let categoryList = getPrefCategoriesArray();
+
+ cal.sortArrayByLocaleCollator(categoryList);
+
+ removeChildren(categoryListbox);
+
+ for (let cat of categoryList) {
+ // First insert all categories from the prefs
+ let item = categoryListbox.appendItem(cat, cat);
+ item.setAttribute("type", "checkbox");
+ }
+
+ if (aItem) {
+ let localeCollator = cal.createLocaleCollator();
+ let compare = localeCollator.compareString.bind(localeCollator, 0);
+
+ // Ensure the item's categories are in the list and they are checked.
+ for (let cat of aItem.getCategories({})) {
+ this.insertCategory(cat, categoryList, categoryListbox, compare);
+ }
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="doubleimage-toolbarbutton" extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+
+ <content>
+ <children includes="observes|template|menupopup|tooltip"/>
+ <xul:image class="toolbarbutton-icon-begin" xbl:inherits="validate,src-begin=image,toolbarmode,buttonstyle"/>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,toolbarmode,buttonstyle"/>
+ <xul:image class="toolbarbutton-icon-end" xbl:inherits="validate,src-end=image,toolbarmode,buttonstyle"/>
+ </content>
+ </binding>
+
+ <binding id="todaypane-toolbarbutton" extends="chrome://calendar/content/widgets/calendar-widgets.xml#doubleimage-toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|tooltip"/>
+ <xul:stack pack="center" align="end">
+ <xul:image class="toolbarbutton-icon-begin" xbl:inherits="validate,src-begin=image,toolbarmode,buttonstyle"/>
+ <xul:label anonid="day-label" class="toolbarbutton-day-text"/>
+ </xul:stack>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,toolbarmode,buttonstyle"/>
+ <xul:image class="toolbarbutton-icon-end" xbl:inherits="validate,src-end=image,toolbarmode,buttonstyle"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.setUpTodayDate();
+ ]]></constructor>
+
+ <method name="setUpTodayDate">
+ <body><![CDATA[
+ let dayNumber = calGetString("dateFormat", "day." + cal.now().day + ".number");
+ document.getAnonymousElementByAttribute(this, "anonid", "day-label").value = dayNumber;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- this binding directly extends to a checkbox but is visualized as
+ a treenode in a treecontrol-->
+ <binding id="treenode-checkbox" extends="chrome://global/content/bindings/checkbox.xml#checkbox-baseline">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+ </binding>
+
+ <!-- this binding directly extends to a xul:box element and automatically
+ sets the "orient" attribute to "vertical" thus behaving like a vbox-->
+ <binding id="modevbox" extends="chrome://calendar/content/widgets/calendar-widgets.xml#modebox">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.setAttribute("orient", "vertical");
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <!-- this binding directly extends to a xul:box element and automatically
+ sets the "orient" attribute to "horizontal" thus behaving like a vbox-->
+ <binding id="modehbox" extends="chrome://calendar/content/widgets/calendar-widgets.xml#modebox">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+ <implementation>
+ <constructor><![CDATA[
+ this.setAttribute("orient", "horizontal");
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <!-- this binding directly extends to a xul:box element and enriches this with some functionality: It is designed
+ to be displayed only 1) in given application modes (e.g "task" mode, "calendar" mode) and 2) only in relation
+ to the "checked" attribute of command or a checkbox control.
+ - The attribute "mode" denotes a coma-separated list of all modes that the binding should not be collapsed in,
+ e.g. mode="calendar,task"
+ - The attribute "broadcaster" points to the id of a broadcaster that is supposed to be notified (by the application)
+ as soon as the mode changes. When this happens the modebox" will be notified and will check if it should
+ collapse itself or not.
+ - The attribute "refcontrol" points to a control either a "command", "checkbox" or a "treenode-checkbox" or other
+ elements that support a "checked" attribute that is often used to denote whether a modebox is supposed to be
+ displayed or not. If "refcontrol" is set to the id of a command you can there set the oncommend attribute like:
+ "oncommand='document.getElementById('my-mode-pane').togglePane(event)'. In case it is a checkbox element or derived
+ checkbox element this is done automatically by listening to the event "CheckboxChange";
+ So if the current application mode is one of the modes listed in the "mode" attribute it is
+ additionally verified if the xul-element denoted by "refcontrol" is checked or not. During runtime an attribute named
+ "collapsedinmodes" with the collpsed modes comma-separated e.g. "mail,calendar,task. This attribute is also made
+ persistent-->
+ <binding id="modebox" extends="xul:box">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+ <implementation>
+ <field name="mBroadcaster">null</field>;
+ <field name="mModHandler">null</field>;
+ <field name="mRefControl">null</field>;
+ <field name="mControlHandler">null</field>;
+
+ <constructor><![CDATA[
+ if (this.hasAttribute("broadcaster")) {
+ this.setAttribute("broadcaster", this.getAttribute("broadcaster"));
+ }
+ if (this.hasAttribute("refcontrol")) {
+ this.mRefControl = document.getElementById(this.getAttribute("refcontrol"));
+ if (this.mRefControl && ((this.mRefControl.localName == "treenode-checkbox") ||
+ (this.mRefControl.localName == "checkbox"))) {
+ this.mControlHandler = {
+ binding: this,
+ handleEvent: function(aEvent, aHandled) {
+ return this.binding.onCheckboxStateChange(aEvent, this.binding);
+ }
+ };
+ this.mRefControl.addEventListener("CheckboxStateChange", this.mControlHandler, true);
+ }
+ }
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ if (this.mBroadcaster) {
+ this.mBroadcaster.removeEventListener("DOMAttrModified", this.mModHandler, true);
+ }
+ if (this.mRefControl) {
+ this.mRefControl.removeEventListener("CheckboxStateChange", this.mControlHandler, true);
+ }
+ ]]></destructor>
+
+ <property name="currentMode">
+ <getter><![CDATA[
+ if (this.mBroadcaster && this.mBroadcaster.hasAttribute("mode")) {
+ return this.mBroadcaster.getAttribute("mode");
+ } else {
+ return "";
+ }
+ ]]></getter>
+ </property>
+
+ <method name="isVisible">
+ <parameter name="aMode"/>
+ <body><![CDATA[
+ let lMode = aMode || this.currentMode;
+ if (!this.isVisibleInMode(lMode)) {
+ return false;
+ }
+ let collapsedModes = this.getAttribute("collapsedinmodes").split(",");
+ return !collapsedModes.includes(lMode);
+ ]]></body>
+ </method>
+
+ <method name="setModeAttribute">
+ <parameter name="aModeAttribute"/>
+ <parameter name="aModeValue"/>
+ <parameter name="amode"/>
+ <body><![CDATA[
+ if (this.hasAttribute(aModeAttribute)) {
+ let lMode = amode || this.currentMode;
+ let modeAttributeValues = this.getAttribute(aModeAttribute).split(",");
+ let modes = this.getAttribute("mode").split(",");
+ modeAttributeValues[modes.indexOf(lMode)] = aModeValue;
+ this.setAttribute(aModeAttribute, modeAttributeValues.join(","));
+ }
+ ]]></body>
+ </method>
+
+ <method name="getModeAttribute">
+ <parameter name="aModeAttribute"/>
+ <parameter name="aAttribute"/>
+ <parameter name="amode"/>
+ <body><![CDATA[
+ if (this.hasAttribute(aModeAttribute)) {
+ let lMode = amode || this.currentMode;
+ let modeAttributeValues = this.getAttribute(aModeAttribute).split(",");
+ let modes = this.getAttribute("mode").split(",");
+ return modeAttributeValues[modes.indexOf(lMode)];
+ } else {
+ return "";
+ }
+ ]]></body>
+ </method>
+
+ <method name="setVisible">
+ <parameter name="aVisible"/>
+ <parameter name="aPushModeCollapsedAttribute"/>
+ <parameter name="aNotifyRefControl"/>
+ <body><![CDATA[
+ let notifyRefControl = aNotifyRefControl == null || aNotifyRefControl === true;
+ let pushModeCollapsedAttribute = aPushModeCollapsedAttribute == null ||
+ aPushModeCollapsedAttribute === true;
+ let collapsedModes = [];
+ let modeIndex = -1;
+ let display = aVisible;
+ let collapsedInMode = false;
+ if (this.hasAttribute("collapsedinmodes")) {
+ collapsedModes = this.getAttribute("collapsedinmodes").split(",");
+ modeIndex = collapsedModes.indexOf(this.currentMode);
+ collapsedInMode = modeIndex > -1;
+ }
+ if (aVisible === true && pushModeCollapsedAttribute == false) {
+ display = (aVisible === true) && (!collapsedInMode);
+ }
+
+ setBooleanAttribute(this, "collapsed", !display || !this.isVisibleInMode());
+ if (pushModeCollapsedAttribute) {
+ if (!display) {
+ if (modeIndex == -1) {
+ collapsedModes.push(this.currentMode);
+ if (this.getAttribute("collapsedinmodes") == ",") {
+ collapsedModes.splice(0, 2);
+ }
+ }
+ } else if (modeIndex > -1) {
+ collapsedModes.splice(modeIndex, 1);
+ if (collapsedModes.join(",") == "") {
+ collapsedModes[0] = ",";
+ }
+ }
+ this.setAttribute("collapsedinmodes", collapsedModes.join(","));
+ let id = this.getAttribute("id");
+ if (id) {
+ document.persist(id, "collapsedinmodes");
+ }
+ }
+ if (notifyRefControl === true) {
+ if (this.hasAttribute("refcontrol")) {
+ let command = document.getElementById(this.getAttribute("refcontrol"));
+ if (command) {
+ command.setAttribute("checked", display);
+ setBooleanAttribute(command, "disabled", !this.isVisibleInMode());
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="isVisibleInMode">
+ <parameter name="aMode"/>
+ <body><![CDATA[
+ let lMode = aMode || this.currentMode;
+ let display = true;
+ let lModes = [];
+ if (this.hasAttribute("mode")) {
+ let modeString = this.getAttribute("mode");
+ lModes = modeString.split(",");
+ }
+ if (lModes && lModes.length > 0) {
+ display = lModes.includes(lMode);
+ }
+ return display;
+ ]]></body>
+ </method>
+
+ <method name="onModeModified">
+ <parameter name="aEvent"/>
+ <parameter name="aBinding"/>
+ <body><![CDATA[
+ if (aEvent.attrName == "mode") {
+ let display = aBinding.isVisibleInMode(aEvent.newValue);
+ aBinding.setVisible(display, false, true);
+ }
+ ]]></body>
+ </method>
+
+ <method name="togglePane">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let command = aEvent.target;
+ let newValue = (command.getAttribute("checked") == "true" ? "false" : "true");
+ command.setAttribute("checked", newValue);
+ this.setVisible(newValue == "true", true, true);
+ ]]></body>
+ </method>
+
+ <method name="onCheckboxStateChange">
+ <parameter name="aEvent"/>
+ <parameter name="aBinding"/>
+ <body><![CDATA[
+ let newValue = aEvent.target.checked;
+ this.setVisible(newValue, true, true);
+ ]]></body>
+ </method>
+
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ if (aAttr == "broadcaster") {
+ this.mBroadcaster = document.getElementById(aVal);
+ if (this.mBroadcaster) {
+ this.mModHandler = {
+ binding: this,
+ handleEvent: function(aEvent, aHandled) {
+ return this.binding.onModeModified(aEvent, this.binding);
+ }
+ };
+ this.mBroadcaster.addEventListener("DOMAttrModified", this.mModHandler, true);
+ }
+ }
+ return XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- This binding may server as a droptarget container for arbitrary items
+ it contains methods to add DropShadows. This binding is meant to be used
+ as a parent binding. The methods may be overwritten. -->
+ <binding id="dragndropContainer">
+ <implementation>
+ <field name="mDropShadows">[]</field>
+ <field name="mCalendarView">null</field>
+
+ <!-- The ViewController that supports the interface 'calICalendarView'-->
+ <property name="calendarView"
+ onget="return this.mCalendarView;"
+ onset="return (this.mCalendarView = val);"/>
+
+ <!-- method to add individual code e.g to set up the new item during
+ 'ondrop' -->
+ <method name="onDropItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ // method that may be overridden by derived bindings...
+ ]]></body>
+ </method>
+
+ <method name="getDropShadows">
+ <body><![CDATA[
+ return this.mDropShadows;
+ ]]></body>
+ </method>
+
+ <!-- Adds the dropshadows to the children of the binding. The dropshadows
+ are added at the first position of the children -->
+ <method name="addDropShadows">
+ <body><![CDATA[
+ if (this.mDropShadows) {
+ if (this.getElementsByAttribute("class", "dropshadow").length == 0) {
+ let offset = this.calendarView.mShadowOffset;
+ let shadowStartDate = this.date.clone();
+ shadowStartDate.addDuration(offset);
+ this.calendarView.mDropShadows = [];
+ for (let i = 0; i < this.calendarView.mDropShadowsLength; i++) {
+ let box = this.calendarView.findDayBoxForDate(shadowStartDate);
+ if (!box) {
+ // Dragging to the end or beginning of a view
+ shadowStartDate.day += 1;
+ continue;
+ }
+ let dropshadow = createXULElement("box");
+ dropshadow.setAttribute("class", "dropshadow");
+ if (box.hasChildNodes()) {
+ box.insertBefore(dropshadow, box.firstChild);
+ } else {
+ box.appendChild(dropshadow);
+ }
+ shadowStartDate.day += 1;
+ this.calendarView.mDropShadows.push(box);
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- removes all dropShadows from the binding. Dropshadows are recognized
+ as such by carrying an attribute "dropshadow" -->
+ <method name="removeDropShadows">
+ <body><![CDATA[
+ // method that may be overwritten by derived bindings...
+ if (this.calendarView.mDropShadows) {
+ for (let shadow of this.calendarView.mDropShadows) {
+ cal.removeChildElementsByAttribute(shadow, "class", "dropshadow");
+ }
+ }
+ this.calendarView.mDropShadows = null;
+ ]]></body>
+ </method>
+
+ <!-- By setting the attribute "dropbox" to "true" or "false" the
+ dropshadows are added or removed -->
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ if (aAttr == "dropbox") {
+ let session = cal.getDragService().getCurrentSession();
+ let startingDayBox = session.sourceNode.mParentBox;
+ if (session) {
+ session.canDrop = true;
+ // no shadows when dragging in the initial position
+ if (aVal == "true" && this != startingDayBox) {
+ this.mDropShadows = [session.sourceNode.sourceObject];
+ this.addDropShadows();
+ } else {
+ this.removeDropShadows();
+ }
+ }
+ }
+ return XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="dragstart" phase="capturing"><![CDATA[
+ let draggedDOMNode = event.target;
+ if (!draggedDOMNode || draggedDOMNode.parentNode != this) {
+ return;
+ }
+ let item = draggedDOMNode.occurrence.clone();
+ let beginMoveDate = draggedDOMNode.mParentBox.date;
+ let itemStartDate = (item.startDate || item.entryDate || item.dueDate).getInTimezone(calendarView.mTimezone);
+ let itemEndDate = (item.endDate || item.dueDate || item.entryDate).getInTimezone(calendarView.mTimezone);
+ let oneMoreDay = (itemEndDate.hour > 0 || itemEndDate.minute > 0);
+ itemStartDate.isDate = true;
+ itemEndDate.isDate = true;
+ let offsetDuration = itemStartDate.subtractDate(beginMoveDate);
+ let lenDuration = itemEndDate.subtractDate(itemStartDate);
+ let len = lenDuration.weeks * 7 + lenDuration.days;
+ this.calendarView.mShadowOffset = offsetDuration;
+ this.calendarView.mDropShadowsLength = oneMoreDay ? len + 1 : len;
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ let session = cal.getDragService().getCurrentSession();
+ if (!session || !session.sourceNode || !session.sourceNode.sourceObject) {
+ // No source item? Then this is not for us.
+ return;
+ }
+
+ // We handled the event
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="dragenter"><![CDATA[
+ if (event.target.localName == this.localName) {
+ let session = cal.getDragService().getCurrentSession();
+ if (session) {
+ if (!session.sourceNode || !session.sourceNode.sourceObject) {
+ // No source item? Then this is not for us.
+ return;
+ }
+
+ // We can drop now, tell the drag service.
+ event.preventDefault();
+
+ if (!this.hasAttribute("dropbox") || this.getAttribute("dropbox") == "false") {
+ // As it turned out it was not possible to remove the remaining dropshadows
+ // at the "dragleave" or "dragexit" event, majorly because it was not reliably
+ // fired. As the dragndropcontainer may be anonymous it is further on not
+ // possible to remove the dropshadows by something like
+ // "document.getElementsByAttribute('dropbox').removeDropShadows();";
+ // So we have to remove them at the currentView(). The restriction of course is
+ // that these containers so far may not be used for drag and drop from/to e.g.
+ // the today-pane.
+ currentView().removeDropShadows();
+ }
+ this.setAttribute("dropbox", "true");
+ }
+ }
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ let session = cal.getDragService().getCurrentSession();
+ if (!session || !session.sourceNode || !session.sourceNode.sourceObject) {
+ // No source node? Not our drag.
+ return;
+ }
+ let item = session.sourceNode.sourceObject.clone();
+ this.setAttribute("dropbox", "false");
+ let transfer = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+ transfer.init(null);
+
+ if (isEvent(item)) {
+ transfer.addDataFlavor("application/x-moz-cal-event");
+ } else {
+ transfer.addDataFlavor("application/x-moz-cal-task");
+ }
+
+ session.getData(transfer, 0);
+ item = session.sourceNode.sourceObject;
+
+ let newItem = this.onDropItem(item).clone();
+ let newStart = newItem.startDate || newItem.entryDate || newItem.dueDate;
+ let newEnd = newItem.endDate || newItem.dueDate || newItem.entryDate;
+ let offset = this.calendarView.mShadowOffset;
+ newStart.addDuration(offset);
+ newEnd.addDuration(offset);
+ this.calendarView.controller.modifyOccurrence(item, newStart, newEnd);
+
+ // We handled the event
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ currentView().removeDropShadows();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="view-tab" extends="chrome://global/content/bindings/tabbox.xml#tab">
+ <resources>
+ <stylesheet src="chrome://calendar/skin/widgets/calendar-widgets.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox class="tab-middle box-inherit" xbl:inherits="align,dir,pack,orient,selected" flex="1">
+ <xul:image class="tab-icon" xbl:inherits="validate,src=image"/>
+ <xul:stack>
+ <xul:label class="tab-text unselected-text"
+ xbl:inherits="value=label,accesskey,crop,disabled,selected"
+ flex="1"/>
+ <xul:label class="tab-text selected-text"
+ xbl:inherits="value=label,accesskey,crop,disabled,selected"
+ flex="1"/>
+ </xul:stack>
+ </xul:hbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/calendar/base/content/widgets/minimonth.xml b/calendar/base/content/widgets/minimonth.xml
new file mode 100644
index 000000000..931aa0028
--- /dev/null
+++ b/calendar/base/content/widgets/minimonth.xml
@@ -0,0 +1,1221 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+ MiniMonth Calendar: day-of-month grid XBL component.
+ Displays month name and year above grid of days of month by week rows.
+ Arrows move forward or back a month.
+ Selecting a month name from month menu moves to that month in same year.
+ Selecting a year from year menu moves to same month in selected year.
+ Clicking on a day cell calls onchange attribute.
+ Changing month via arrows or menus calls onmonthchange attribute.
+
+ At site, can provide id, and code to run when value changed by picker.
+ <calendar id="my-date-picker" onchange="myDatePick( this );"/>
+
+ May get/set value in javascript with
+ document.getElementById("my-date-picker").value = new Date();
+
+ Use attributes onpopuplisthidden and onmonthchange for working around
+ bugs that occur when minimonth is displayed in a popup (as in datepicker):
+ Currently (2005.3)
+ whenever a child popup is hidden, the parent popup needs to be reshown.
+ Use onpopuplisthidden to reshow parent popop (hidePopup, openPopup).
+ When title month or year changes, parent popup may need to be reshown.
+ Use onmonthchange to reshow parent popop (hidePopup, openPopup).
+-->
+
+<!DOCTYPE bindings
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://global/locale/global.dtd" > %dtd2;
+]>
+
+<bindings id="xulMiniMonth"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="minimonth-header" extends="xul:box">
+ <content class="minimonth-month-box" align="center">
+ <xul:deck anonid="monthheader" xbl:inherits="selectedIndex=month" class="minimonth-month-name-readonly">
+ <xul:text value="&month.1.name;"/>
+ <xul:text value="&month.2.name;"/>
+ <xul:text value="&month.3.name;"/>
+ <xul:text value="&month.4.name;"/>
+ <xul:text value="&month.5.name;"/>
+ <xul:text value="&month.6.name;"/>
+ <xul:text value="&month.7.name;"/>
+ <xul:text value="&month.8.name;"/>
+ <xul:text value="&month.9.name;"/>
+ <xul:text value="&month.10.name;"/>
+ <xul:text value="&month.11.name;"/>
+ <xul:text value="&month.12.name;"/>
+ </xul:deck>
+ <xul:text anonid="yearcell" class="minimonth-year-name-readonly" xbl:inherits="value=year"/>
+ <xul:spacer flex="1"/>
+ </content>
+ </binding>
+
+ <binding id="active-minimonth-header" extends="chrome://calendar/content/widgets/minimonth.xml#minimonth-header">
+ <content class="minimonth-month-box" align="center">
+ <xul:deck anonid="monthheader" xbl:inherits="selectedIndex=month" class="minimonth-month-name">
+ <xul:toolbarbutton label="&month.1.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.2.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.3.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.4.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.5.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.6.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.7.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.8.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.9.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.10.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.11.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ <xul:toolbarbutton label="&month.12.name;" oncommand="showPopupList(event, 'months-popup')"/>
+ </xul:deck>
+ <xul:toolbarbutton anonid="yearcell"
+ class="minimonth-year-name"
+ oncommand="showPopupList(event, 'years-popup')"
+ xbl:inherits="label=year"/>
+ <xul:spacer flex="1"/>
+ <xul:toolbarbutton anonid="back-button" class="minimonth-nav-btns" dir="-1"
+ oncommand="this.kMinimonth.advanceMonth(parseInt(this.getAttribute('dir'), 10))"
+ tooltiptext="&onemonthbackward.tooltip;"/>
+ <xul:toolbarbutton anonid="today-button" class="minimonth-nav-btns" dir="0"
+ oncommand="this.kMinimonth.value = new Date();"
+ tooltiptext="&showToday.tooltip;"/>
+ <xul:toolbarbutton anonid="forward-button" class="minimonth-nav-btns" dir="1"
+ oncommand="this.kMinimonth.advanceMonth(parseInt(this.getAttribute('dir'), 10))"
+ tooltiptext="&onemonthforward.tooltip;"/>
+ <xul:popupset anonid="minmonth-popupset">
+ <xul:menupopup anonid="months-popup" position="after_start"
+ onpopupshowing="event.stopPropagation();"
+ onpopuphidden="firePopupListHidden();">
+ <xul:vbox>
+ <xul:text class="minimonth-list" value="&month.1.name;" index="0"/>
+ <xul:text class="minimonth-list" value="&month.2.name;" index="1"/>
+ <xul:text class="minimonth-list" value="&month.3.name;" index="2"/>
+ <xul:text class="minimonth-list" value="&month.4.name;" index="3"/>
+ <xul:text class="minimonth-list" value="&month.5.name;" index="4"/>
+ <xul:text class="minimonth-list" value="&month.6.name;" index="5"/>
+ <xul:text class="minimonth-list" value="&month.7.name;" index="6"/>
+ <xul:text class="minimonth-list" value="&month.8.name;" index="7"/>
+ <xul:text class="minimonth-list" value="&month.9.name;" index="8"/>
+ <xul:text class="minimonth-list" value="&month.10.name;" index="9"/>
+ <xul:text class="minimonth-list" value="&month.11.name;" index="10"/>
+ <xul:text class="minimonth-list" value="&month.12.name;" index="11"/>
+ </xul:vbox>
+ </xul:menupopup>
+ <xul:menupopup anonid="years-popup" position="after_start"
+ onpopupshowing="moveYears('reset', 0); event.stopPropagation();"
+ onpopuphidden="firePopupListHidden();">
+ <xul:vbox>
+ <xul:autorepeatbutton class="autorepeatbutton-up"
+ orient="vertical"
+ oncommand="moveYears('up', 1);"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:text class="minimonth-list"/>
+ <xul:autorepeatbutton class="autorepeatbutton-down"
+ orient="vertical"
+ oncommand="moveYears('down', 1);"/>
+ </xul:vbox>
+ </xul:menupopup>
+ </xul:popupset>
+ </content>
+ <implementation>
+ <field name="kMinimonth">null</field>
+ <field name="mPopup">null</field>
+ <field name="mScrollYearsHandler">null</field>
+ <field name="mPixelScrollDelta">0</field>
+ <constructor><![CDATA[
+ this.kMinimonth = getParentNodeOrThis(this, "minimonth");
+ document.getAnonymousElementByAttribute(this, "anonid", "back-button").kMinimonth = this.kMinimonth;
+ document.getAnonymousElementByAttribute(this, "anonid", "today-button").kMinimonth = this.kMinimonth;
+ document.getAnonymousElementByAttribute(this, "anonid", "forward-button").kMinimonth = this.kMinimonth;
+
+ this.mScrollYearsHandler = this.scrollYears.bind(this);
+ document.getAnonymousElementByAttribute(this, "anonid", "years-popup")
+ .addEventListener("wheel", this.mScrollYearsHandler, true);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "years-popup")
+ .removeEventListener("wheel", this.mScrollYearsHandler, true);
+ this.mScrollYearsHandler = null;
+ ]]></destructor>
+
+ <method name="showPopupList">
+ <parameter name="aEvent"/>
+ <parameter name="aPopupAnonId"/>
+ <body><![CDATA[
+ // Close open popups (if any), to prevent linux crashes
+ if (this.mPopup) {
+ this.mPopup.hidePopup();
+ }
+ this.mPopup = document.getAnonymousElementByAttribute(this, "anonid", aPopupAnonId);
+ this.mPopup.openPopup(aEvent.target, "after_start");
+ ]]></body>
+ </method>
+
+ <method name="hidePopupList">
+ <body><![CDATA[
+ // Close open popups (if any)
+ let popup = this.mPopup;
+ this.mPopup = null;
+ if (popup) {
+ popup.hidePopup();
+ }
+ ]]></body>
+ </method>
+
+ <method name="firePopupListHidden">
+ <body><![CDATA[
+ if (this.mPopup) {
+ this.mPopup = null;
+ this.kMinimonth.fireEvent("popuplisthidden");
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateMonthPopup">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ let months = document.getAnonymousElementByAttribute(this, "anonid", "months-popup").firstChild.childNodes;
+ let month = aDate.getMonth();
+ for (let i = 0; i < months.length; i++) {
+ months[i].setAttribute("current", i == month ? "true" : "false");
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateYearPopup">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ let years = document.getAnonymousElementByAttribute(this, "anonid", "years-popup").firstChild.childNodes;
+ let year = new Date(aDate);
+ let compFullYear = aDate.getFullYear();
+ year.setFullYear(Math.max(1, compFullYear - Math.trunc(years.length / 2) + 1));
+ for (let i = 1; i < years.length - 1; i++) {
+ let curfullYear = year.getFullYear();
+ years[i].setAttribute("value", curfullYear);
+ years[i].setAttribute("current", curfullYear == compFullYear ? "true" : "false");
+ year.setFullYear(curfullYear + 1);
+ }
+ ]]></body>
+ </method>
+
+ <method name="scrollYears">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let yearPopup = getParentNodeOrThis(event.target, "menupopup");
+ const pixelThreshold = 75;
+ if (yearPopup) {
+ let monthList = yearPopup.getElementsByAttribute("class", "minimonth-list");
+ if (monthList && monthList.length > 0) {
+ if (event.deltaMode == event.DOM_DELTA_PAGE) {
+ let dir = event.deltaY > 0 ? "up" : "down";
+ this.moveYears(dir, Math.abs(event.deltaY) * monthList.length);
+ } else if (event.deltaMode == event.DOM_DELTA_LINE) {
+ let dir = event.deltaY > 0 ? "up" : "down";
+ this.moveYears(dir, 1);
+ } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
+ this.mPixelScrollDelta += event.deltaY;
+ if (this.mPixelScrollDelta > pixelThreshold) {
+ this.moveYears("down", 1);
+ this.mPixelScrollDelta = 0;
+ } else if (this.mPixelScrollDelta < -pixelThreshold) {
+ this.moveYears("up", 1);
+ this.mPixelScrollDelta = 0;
+ }
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="moveYears">
+ <parameter name="direction"/>
+ <parameter name="scrollOffset"/>
+ <body><![CDATA[
+ // Update the year popup
+ let years = document.getAnonymousElementByAttribute(this, "anonid", "years-popup").firstChild.childNodes;
+ let current = this.getAttribute("year");
+ let offset;
+ switch (direction) {
+ case "reset": {
+ let middleyear = years[Math.floor(years.length / 2)].getAttribute("value");
+ if (current <= (years.length / 2)) {
+ offset = 1 - years[1].getAttribute("value");
+ } else {
+ offset = current - middleyear;
+ }
+ break;
+ }
+ case "up": {
+ offset = -Math.abs(scrollOffset) || -1;
+ break;
+ }
+ case "down": {
+ offset = Math.abs(scrollOffset) || 1;
+ break;
+ }
+ }
+
+ // Disable the up arrow when we get to the year 1.
+ years[0].disabled = parseInt(years[1].getAttribute("value"), 10) + offset < 2;
+
+ if (!offset) {
+ // No need to loop through when the offset is zero.
+ return;
+ }
+
+ // Go through all visible years and set the new value. Be sure to
+ // skip the autorepeatbuttons.
+ for (let i = 1; i < years.length - 1; i++) {
+ let value = parseInt(years[i].getAttribute("value"), 10) + offset;
+ years[i].setAttribute("value", value);
+ years[i].setAttribute("current", value == current ? "true" : "false");
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="bindingattached" action="this.initialize();"/>
+
+ <!-- handle click from nested months popup and years popup -->
+ <handler event="click"><![CDATA[
+ let element = event.originalTarget;
+ let popup = getParentNodeOrThis(element, "menupopup");
+ if (popup) {
+ let anonid = popup.getAttribute("anonid");
+ switch (anonid) {
+ case "months-popup": {
+ this.hidePopupList();
+ this.kMinimonth.switchMonth(element.getAttribute("index"));
+ break;
+ }
+ case "years-popup": {
+ this.hidePopupList();
+ let value = element.getAttribute("value");
+ if (value) {
+ this.kMinimonth.switchYear(value);
+ }
+ break;
+ }
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="minimonth" extends="xul:box">
+ <resources>
+ <stylesheet src="chrome://calendar-common/skin/widgets/minimonth.css"/>
+ </resources>
+
+ <content orient="vertical" xbl:inherits="onchange,onmonthchange,onpopuplisthidden,readonly">
+ <xul:minimonth-header anonid="minimonth-header" xbl:inherits="readonly,month,year"/>
+ <xul:vbox anonid="minimonth-calendar" class="minimonth-cal-box">
+ <xul:hbox class="minimonth-row-head" anonid="minimonth-row-header" equalsize="always">
+ <xul:text class="minimonth-row-header-week" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ <xul:text class="minimonth-row-header" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
+ <xul:text class="minimonth-week" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ <xul:text class="minimonth-day" flex="1"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+
+ <implementation implements="calICompositeObserver calIOperationListener nsIObserver" >
+ <property name="value"
+ onget="return this.mValue"
+ onset="this.update(val)"/>
+
+ <property name="extra"
+ onget="return this.mExtraDate"
+ onset="this.mExtraDate = val"/>
+
+ <!--returns the first (inclusive) date of the minimonth as a calIDateTime object-->
+ <property name="firstDate" readonly="true">
+ <getter><![CDATA[
+ let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
+ let date = calbox.childNodes[1].firstChild.nextSibling.date;
+ return cal.jsDateToDateTime(date);
+ ]]></getter>
+ </property>
+
+ <!--returns the last (exclusive) date of the minimonth as a calIDateTime object-->
+ <property name="lastDate" readonly="true">
+ <getter><![CDATA[
+ let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
+ let date = calbox.lastChild.lastChild.date;
+ let lastDateTime = cal.jsDateToDateTime(date);
+ lastDateTime.day = lastDateTime.day + 1;
+ return lastDateTime;
+ ]]></getter>
+ </property>
+
+ <field name="mDaymap">null</field>
+ <field name="mValue">null</field>
+ <field name="mEditorDate">null</field>
+ <field name="mExtraDate">null</field>
+ <field name="mPixelScrollDelta">0</field>
+ <field name="mIsReadOnly">false</field>
+ <field name="mObservesComposite">false</field>
+ <field name="mShowWeekNumber">true</field>
+
+ <constructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ this.mToday = false;
+ this.mSelected = false;
+ this.mExtra = false;
+ this.mValue = new Date(); // Default to "today"
+ // save references for convenience
+ if (this.hasAttribute("readonly")) {
+ this.mIsReadOnly = this.getAttribute("readonly") == "true";
+ }
+ this.refreshDisplay();
+ if (this.hasAttribute("freebusy")) {
+ this._setFreeBusy(this.getAttribute("freebusy") == "true");
+ }
+ this.mShowWeekNumber = Preferences.get("calendar.view-minimonth.showWeekNumber", true);
+
+ // Add pref observer
+ let branch = Services.prefs.getBranch("");
+ branch.addObserver("calendar.", this, false);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ if (this.mObservesComposite == true) {
+ getCompositeCalendar().removeObserver(this);
+ }
+
+ // Remove pref observer
+ let branch = Services.prefs.getBranch("");
+ branch.removeObserver("calendar.", this, false);
+ ]]></destructor>
+
+ <!-- calIOperationListener methods -->
+ <method name="onOperationComplete">
+ <parameter name="aCalendar"/>
+ <parameter name="aStatus"/>
+ <parameter name="aOperationType"/>
+ <parameter name="aId"/>
+ <parameter name="aDetail"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="onGetResult">
+ <parameter name="aCalendar"/>
+ <parameter name="aStatus"/>
+ <parameter name="aItemType"/>
+ <parameter name="aDetail"/>
+ <parameter name="aCount"/>
+ <parameter name="aItems"/>
+ <body><![CDATA[
+ if (!Components.isSuccessCode(aStatus)) {
+ return;
+ }
+ for (let item of aItems) {
+ this.setBusyDaysForOccurrence(item, true);
+ }
+ ]]></body>
+ </method>
+
+ <method name="setBusyDaysForItem">
+ <parameter name="aItem"/>
+ <parameter name="aState"/>
+ <body><![CDATA[
+ let items = [aItem];
+ if (aItem.recurrenceInfo) {
+ let startDate = this.firstDate;
+ let endDate = this.lastDate;
+ items = aItem.getOccurrencesBetween(startDate, endDate, {});
+ }
+ for (let item of items) {
+ this.setBusyDaysForOccurrence(item, aState);
+ }
+ ]]></body>
+ </method>
+
+ <method name="parseBoxBusy">
+ <parameter name="aBox"/>
+ <body><![CDATA[
+ let boxBusy = {};
+
+ let busyStr = aBox.getAttribute("busy");
+ if (busyStr && busyStr.length > 0) {
+ let calChunks = busyStr.split("\u001A");
+ for (let chunk of calChunks) {
+ let expr = chunk.split("=");
+ boxBusy[expr[0]] = parseInt(expr[1], 10);
+ }
+ }
+
+ return boxBusy;
+ ]]></body>
+ </method>
+
+ <method name="updateBoxBusy">
+ <parameter name="aBox"/>
+ <parameter name="aBoxBusy"/>
+ <body><![CDATA[
+ let calChunks = [];
+
+ for (let calId in aBoxBusy) {
+ if (aBoxBusy[calId]) {
+ calChunks.push(calId + "=" + aBoxBusy[calId]);
+ }
+ }
+
+ if (calChunks.length > 0) {
+ let busyStr = calChunks.join("\u001A");
+ aBox.setAttribute("busy", busyStr);
+ } else {
+ aBox.removeAttribute("busy");
+ }
+ ]]></body>
+ </method>
+
+ <method name="removeCalendarFromBoxBusy">
+ <parameter name="aBox"/>
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ let boxBusy = this.parseBoxBusy(aBox);
+ if (boxBusy[aCalendar.id]) {
+ delete boxBusy[aCalendar.id];
+ }
+ this.updateBoxBusy(aBox, boxBusy);
+ ]]></body>
+ </method>
+
+ <method name="setBusyDaysForOccurrence">
+ <parameter name="aOccurrence"/>
+ <parameter name="aState"/>
+ <body><![CDATA[
+ if (aOccurrence.getProperty("TRANSP") == "TRANSPARENT") {
+ // Skip transparent events
+ return;
+ }
+ let start = aOccurrence[calGetStartDateProp(aOccurrence)] || aOccurrence.dueDate;
+ let end = aOccurrence[calGetEndDateProp(aOccurrence)] || start;
+ if (!start) {
+ return;
+ }
+
+ // We need to compare with midnight of the current day, so reset the
+ // time here.
+ let current = start.clone().getInTimezone(cal.calendarDefaultTimezone());
+ current.hour = 0;
+ current.minute = 0;
+ current.second = 0;
+
+ // Cache the result so the compare isn't called in each iteration.
+ let compareResult = (start.compare(end) == 0 ? 1 : 0);
+
+ // Setup the busy days.
+ while (current.compare(end) < compareResult) {
+ let box = this.getBoxForDate(current);
+ if (box) {
+ let busyCalendars = this.parseBoxBusy(box);
+ if (!busyCalendars[aOccurrence.calendar.id]) {
+ busyCalendars[aOccurrence.calendar.id] = 0;
+ }
+ busyCalendars[aOccurrence.calendar.id] += (aState ? 1 : -1);
+ this.updateBoxBusy(box, busyCalendars);
+ }
+ current.day++;
+ }
+ ]]></body>
+ </method>
+
+ <!--calIObserver methods -->
+ <method name="onStartBatch">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="onEndBatch">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="onLoad">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="onAddItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ this.setBusyDaysForItem(aItem, true);
+ ]]></body>
+ </method>
+
+ <method name="onDeleteItem">
+ <parameter name="aItem"/>
+ <body><![CDATA[
+ this.setBusyDaysForItem(aItem, false);
+ ]]></body>
+ </method>
+
+ <method name="onModifyItem">
+ <parameter name="aNewItem"/>
+ <parameter name="aOldItem"/>
+ <body><![CDATA[
+ this.setBusyDaysForItem(aOldItem, false);
+ this.setBusyDaysForItem(aNewItem, true);
+ ]]></body>
+ </method>
+
+ <method name="onError">
+ <parameter name="aCalendar"/>
+ <parameter name="aErrNo"/>
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <method name="onPropertyChanged">
+ <parameter name="aCalendar"/>
+ <parameter name="aName"/>
+ <parameter name="aValue"/>
+ <parameter name="aOldValue"/>
+ <body><![CDATA[
+ switch (aName) {
+ case "disabled":
+ this.resetAttributesForDate();
+ this.getItems();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="onPropertyDeleting">
+ <parameter name="aCalendar"/>
+ <parameter name="aName"/>
+ <body><![CDATA[
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ ]]></body>
+ </method>
+
+ <!-- calICompositeObserver methods -->
+ <method name="onCalendarAdded">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ this.getItems(aCalendar);
+ ]]></body>
+ </method>
+
+ <method name="onCalendarRemoved">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ for (let day in this.mDayMap) {
+ this.removeCalendarFromBoxBusy(this.mDayMap[day], aCalendar);
+ }
+ ]]></body>
+ </method>
+
+ <method name="onDefaultCalendarChanged">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ ]]></body>
+ </method>
+
+ <!-- nsIObserver methods -->
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ switch (aData) {
+ case "calendar.week.start":
+ case "calendar.view-minimonth.showWeekNumber":
+ this.refreshDisplay();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="refreshDisplay">
+ <body><![CDATA[
+ // Find out which should be the first day of the week
+ this.weekStart = Preferences.get("calendar.week.start", 0);
+ this.mShowWeekNumber = Preferences.get("calendar.view-minimonth.showWeekNumber", true);
+ if (!this.mValue) {
+ this.mValue = new Date();
+ }
+ this.setHeader();
+ this.showMonth(this.mValue);
+ ]]></body>
+ </method>
+
+ <method name="setHeader">
+ <body><![CDATA[
+ // Reset the headers
+ let header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-row-header");
+ let dayList = new Array(7);
+ let tempDate = new Date();
+ let i, j;
+ let useOSFormat;
+ tempDate.setDate(tempDate.getDate() - (tempDate.getDay() - this.weekStart));
+ for (i = 0; i < header.childNodes.length - 1; i++) {
+ // if available, use UILocale days, else operating system format
+ try {
+ dayList[i] = calGetString("dateFormat",
+ "day." + (tempDate.getDay() + 1) + ".short");
+ } catch (e) {
+ dayList[i] = tempDate.toLocaleFormat("%a");
+ useOSFormat = true;
+ }
+ tempDate.setDate(tempDate.getDate() + 1);
+ }
+
+ if (useOSFormat) {
+ // To keep datepicker popup compact, shrink localized weekday
+ // abbreviations down to 1 or 2 chars so each column of week can
+ // be as narrow as 2 digits.
+ //
+ // 1. Compute the minLength of the day name abbreviations.
+ let minLength = dayList[0].length;
+ for (i = 1; i < dayList.length; i++) {
+ minLength = Math.min(minLength, dayList[i].length);
+ }
+ // 2. If some day name abbrev. is longer than 2 chars (not Catalan),
+ // and ALL localized day names share same prefix (as in Chinese),
+ // then trim shared "day-" prefix.
+ if (dayList.some(dayAbbr => dayAbbr.length > 2)) {
+ for (let endPrefix = 0; endPrefix < minLength; endPrefix++) {
+ let suffix = dayList[0][endPrefix];
+ if (dayList.some(dayAbbr => dayAbbr[endPrefix] != suffix)) {
+ if (endPrefix > 0) {
+ for (i = 0; i < dayList.length; i++) { // trim prefix chars.
+ dayList[i] = dayList[i].substring(endPrefix);
+ }
+ }
+ break;
+ }
+ }
+ }
+ // 3. trim each day abbreviation to 1 char if unique, else 2 chars.
+ for (i = 0; i < dayList.length; i++) {
+ let foundMatch = 1;
+ for (j = 0; j < dayList.length; j++) {
+ if (i != j) {
+ if (dayList[i].substring(0, 1) == dayList[j].substring(0, 1)) {
+ foundMatch = 2;
+ break;
+ }
+ }
+ }
+ dayList[i] = dayList[i].substring(0, foundMatch);
+ }
+ }
+
+ setBooleanAttribute(header.childNodes[0], "hidden", !this.mShowWeekNumber);
+ for (let column = 1; column < header.childNodes.length; column++) {
+ header.childNodes[column].setAttribute("value", dayList[column - 1]);
+ }
+ ]]></body>
+ </method>
+
+ <method name="showMonth">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ // Use mExtraDate if aDate is null.
+ aDate = new Date(aDate || this.mExtraDate);
+
+ aDate.setDate(1);
+ // We set the hour and minute to something highly unlikely to be the
+ // exact change point of DST, so timezones like America/Sao Paulo
+ // don't display some days twice.
+ aDate.setHours(12);
+ aDate.setMinutes(34);
+ aDate.setSeconds(0);
+ aDate.setMilliseconds(0);
+ // Don't fire onmonthchange event upon initialization
+ let monthChanged = this.mEditorDate && (this.mEditorDate.valueOf() != aDate.valueOf());
+ this.mEditorDate = aDate; // only place mEditorDate is set.
+
+ if (this.mToday) {
+ this.mToday.removeAttribute("today");
+ this.mToday = null;
+ }
+
+ if (this.mSelected) {
+ this.mSelected.removeAttribute("selected");
+ this.mSelected = null;
+ }
+
+ if (this.mExtra) {
+ this.mExtra.removeAttribute("extra");
+ this.mExtra = null;
+ }
+
+ // Update the month and year title
+ this.setAttribute("month", aDate.getMonth());
+ this.setAttribute("year", aDate.getFullYear());
+ if (!this.mIsReadOnly) {
+ // Update the month popup
+ let header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-header");
+ header.updateYearPopup(aDate);
+ header.updateMonthPopup(aDate);
+ }
+ // Update the calendar
+ let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
+ let date = this._getStartDate(aDate);
+
+ // get today's date
+ let today = new Date();
+
+ this.mDayMap = {};
+ let defaultTz = cal.calendarDefaultTimezone();
+ for (let k = 1; k < calbox.childNodes.length; k++) {
+ let row = calbox.childNodes[k];
+
+ // Set the week number.
+ let firstElement = row.childNodes[0];
+ setBooleanAttribute(firstElement, "hidden", !this.mShowWeekNumber);
+ if (this.mShowWeekNumber) {
+ let weekNumber = cal.getWeekInfoService()
+ .getWeekTitle(cal.jsDateToDateTime(date, defaultTz));
+ firstElement.setAttribute("value", weekNumber);
+ }
+
+ for (let i = 1; i < 8; i++) {
+ let day = row.childNodes[i];
+ let ymd = date.getFullYear() + "-" +
+ date.getMonth() + "-" +
+ date.getDate();
+ this.mDayMap[ymd] = day;
+
+ if (!this.mIsReadOnly) {
+ day.setAttribute("interactive", "true");
+ }
+
+ if (aDate.getMonth() == date.getMonth()) {
+ day.removeAttribute("othermonth");
+ } else {
+ day.setAttribute("othermonth", "true");
+ }
+
+ // highlight today
+ if (this._sameDay(today, date)) {
+ this.mToday = day;
+ day.setAttribute("today", "true");
+ }
+
+ // highlight the current date
+ let val = this.value;
+ if (this._sameDay(val, date)) {
+ this.mSelected = day;
+ day.setAttribute("selected", "true");
+ }
+
+ // highlight the extra date
+ if (this._sameDay(this.mExtraDate, date)) {
+ this.mExtra = day;
+ day.setAttribute("extra", "true");
+ }
+
+ day.date = new Date(date);
+ day.minimonthParent = this;
+ day.setAttribute("value", date.getDate());
+ date.setDate(date.getDate() + 1);
+
+ if (monthChanged) {
+ this.resetAttributesForDate(day.date);
+ }
+ }
+ }
+
+ if (monthChanged) {
+ this.fireEvent("monthchange");
+ }
+
+ if (this.getAttribute("freebusy") == "true") {
+ this.getItems();
+ }
+ ]]></body>
+ </method>
+
+ <!--Attention - duplicate!!!!-->
+ <method name="fireEvent">
+ <parameter name="aEventName"/>
+ <body><![CDATA[
+ let event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="getBoxForDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ // aDate is a calIDateTime
+ let ymd = [aDate.year, aDate.month, aDate.day].join("-");
+ return (ymd in this.mDayMap ? this.mDayMap[ymd] : null);
+ ]]></body>
+ </method>
+
+ <method name="resetAttributesForDate">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ function removeForBox(aBox) {
+ let allowedAttributes = 0;
+ while (aBox.attributes.length > allowedAttributes) {
+ switch (aBox.attributes[allowedAttributes].nodeName) {
+ case "selected":
+ case "othermonth":
+ case "today":
+ case "extra":
+ case "interactive":
+ case "value":
+ case "class":
+ case "flex":
+ allowedAttributes++;
+ break;
+ default:
+ aBox.removeAttribute(aBox.attributes[allowedAttributes].nodeName);
+ break;
+ }
+ }
+ }
+
+ if (aDate) {
+ let box = this.getBoxForDate(cal.jsDateToDateTime(aDate, cal.calendarDefaultTimezone()));
+ if (box) {
+ removeForBox(box);
+ }
+ } else {
+ let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
+ for (let k = 1; k < calbox.childNodes.length; k++) {
+ for (let i = 1; i < 8; i++) {
+ removeForBox(calbox.childNodes[k].childNodes[i]);
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_setFreeBusy">
+ <parameter name="aFreeBusy"/>
+ <body><![CDATA[
+ if (aFreeBusy == true) {
+ if (this.mObservesComposite == false) {
+ getCompositeCalendar().addObserver(this);
+ this.mObservesComposite = true;
+ this.getItems();
+ }
+ } else if (this.mObservesComposite == true) {
+ getCompositeCalendar().removeObserver(this);
+ this.mObservesComposite = false;
+ }
+ ]]></body>
+ </method>
+
+ <method name="removeAttribute">
+ <parameter name="aAttr"/>
+ <body><![CDATA[
+ if (aAttr == "freebusy") {
+ this._setFreeBusy(false);
+ }
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.removeAttribute.call(this, aAttr);
+ return ret;
+ ]]></body>
+ </method>
+
+ <method name="setAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ if (aAttr == "freebusy") {
+ this._setFreeBusy(aVal == "true");
+ }
+ // this should be done using lookupMethod(), see bug 286629
+ let ret = XULElement.prototype.setAttribute.call(this, aAttr, aVal);
+ return ret;
+ ]]></body>
+ </method>
+
+ <method name="getItems">
+ <parameter name="aCalendar"/>
+ <body><![CDATA[
+ // The minimonth automatically clears extra styles on a month change.
+ // Therefore we only need to fill the minimonth with new info.
+
+ let calendar = aCalendar || getCompositeCalendar();
+ let filter = calendar.ITEM_FILTER_COMPLETED_ALL |
+ calendar.ITEM_FILTER_CLASS_OCCURRENCES |
+ calendar.ITEM_FILTER_ALL_ITEMS;
+
+ // Get new info
+ calendar.getItems(filter,
+ 0,
+ this.firstDate,
+ this.lastDate,
+ this);
+ ]]></body>
+ </method>
+
+ <method name="onSelectDay">
+ <parameter name="aDayBox"/>
+ <body><![CDATA[
+ if (this.mIsReadOnly) {
+ return;
+ }
+ if (this.mSelected) {
+ this.mSelected.removeAttribute("selected");
+ }
+ this.mSelected = aDayBox;
+ this.value = aDayBox.date;
+ this.fireEvent("select");
+ ]]></body>
+ </method>
+
+ <method name="update">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.mValue = aValue;
+ if (this.mValue) {
+ this.fireEvent("change");
+ }
+ this.showMonth(aValue);
+ ]]></body>
+ </method>
+
+ <method name="hidePopupList">
+ <body><![CDATA[
+ if (!this.mIsReadOnly) {
+ let header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-header");
+ header.hidePopupList();
+ }
+ ]]></body>
+ </method>
+
+ <method name="switchMonth">
+ <parameter name="aMonth"/>
+ <body><![CDATA[
+ let newMonth = new Date(this.mEditorDate);
+ newMonth.setMonth(aMonth);
+ this.showMonth(newMonth);
+ ]]></body>
+ </method>
+
+ <method name="switchYear">
+ <parameter name="aYear"/>
+ <body><![CDATA[
+ let newMonth = new Date(this.mEditorDate);
+ newMonth.setFullYear(aYear);
+ this.showMonth(newMonth);
+ ]]></body>
+ </method>
+
+ <method name="selectDate">
+ <parameter name="aDate"/>
+ <parameter name="aMainDate"/>
+ <body><![CDATA[
+ if (!aMainDate || aDate < this._getStartDate(aMainDate) || aDate > this._getEndDate(aMainDate)) {
+ aMainDate = new Date(aDate);
+ aMainDate.setDate(1);
+ }
+ // note, that aMainDate and this.mEditorDate refer to the first day
+ // of the corresponding month
+ let sameMonth = this._sameDay(aMainDate, this.mEditorDate);
+ let sameDate = this._sameDay(aDate, this.mValue);
+ if (!sameMonth && !sameDate) {
+ // change month and select day
+ this.mValue = aDate;
+ this.showMonth(aMainDate);
+ } else if (!sameMonth) {
+ // change month only
+ this.showMonth(aMainDate);
+ } else if (!sameDate) {
+ // select day only
+ let day = this.getBoxForDate(cal.jsDateToDateTime(aDate, cal.calendarDefaultTimezone()));
+ if (this.mSelected) {
+ this.mSelected.removeAttribute("selected");
+ }
+ this.mSelected = day;
+ day.setAttribute("selected", "true");
+ this.mValue = aDate;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_getStartDate">
+ <parameter name="aMainDate"/>
+ <body><![CDATA[
+ let date = new Date(aMainDate);
+ let firstWeekday = (7 + aMainDate.getDay() - this.weekStart) % 7;
+ date.setDate(date.getDate() - firstWeekday);
+ return date;
+ ]]></body>
+ </method>
+
+ <method name="_getEndDate">
+ <parameter name="aMainDate"/>
+ <body><![CDATA[
+ let date = this._getStartDate(aMainDate);
+ let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
+ let days = (calbox.childNodes.length - 1) * 7;
+ date.setDate(date.getDate() + days - 1);
+ return date;
+ ]]></body>
+ </method>
+
+ <method name="_sameDay">
+ <parameter name="aDate1"/>
+ <parameter name="aDate2"/>
+ <body><![CDATA[
+ if (aDate1 && aDate2 &&
+ (aDate1.getDate() == aDate2.getDate()) &&
+ (aDate1.getMonth() == aDate2.getMonth()) &&
+ (aDate1.getFullYear() == aDate2.getFullYear())) {
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="advanceMonth">
+ <parameter name="aDir"/>
+ <body><![CDATA[
+ let advEditorDate = new Date(this.mEditorDate); // at 1st of month
+ let advMonth = this.mEditorDate.getMonth() + aDir;
+ advEditorDate.setMonth(advMonth);
+ this.showMonth(advEditorDate);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="wheel"><![CDATA[
+ const pixelThreshold = 150;
+ let deltaView = 0;
+ if (this.mIsReadOnly) {
+ // No scrolling on readonly months
+ return;
+ }
+ if (event.deltaMode == event.DOM_DELTA_LINE ||
+ event.deltaMode == event.DOM_DELTA_PAGE) {
+ if (event.deltaY != 0) {
+ deltaView = event.deltaY > 0 ? 1 : -1;
+ }
+ } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
+ this.mPixelScrollDelta += event.deltaY;
+ if (this.mPixelScrollDelta > pixelThreshold) {
+ deltaView = 1;
+ this.mPixelScrollDelta = 0;
+ } else if (this.mPixelScrollDelta < -pixelThreshold) {
+ deltaView = -1;
+ this.mPixelScrollDelta = 0;
+ }
+ }
+
+ if (deltaView != 0) {
+ this.advanceMonth(deltaView);
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="minimonth-day" extends="xul:text">
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ if (this.minimonthParent.getAttribute("readonly") != "true") {
+ this.setAttribute("selected", "true");
+ this.minimonthParent.onSelectDay(this);
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/calendar/base/jar.mn b/calendar/base/jar.mn
new file mode 100644
index 000000000..d9d53ea07
--- /dev/null
+++ b/calendar/base/jar.mn
@@ -0,0 +1,203 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+calendar.jar:
+% resource calendar .
+% content calendar %content/calendar/
+ content/calendar/agenda-listbox.js (content/agenda-listbox.js)
+ content/calendar/agenda-listbox.xml (content/agenda-listbox.xml)
+ content/calendar/calendar-bindings.css (content/calendar-bindings.css)
+ content/calendar/calendar-calendars-list.xul (content/calendar-calendars-list.xul)
+ content/calendar/calendar-chrome-startup.js (content/calendar-chrome-startup.js)
+ content/calendar/calendar-clipboard.js (content/calendar-clipboard.js)
+* content/calendar/calendar-common-sets.xul (content/calendar-common-sets.xul)
+ content/calendar/calendar-common-sets.js (content/calendar-common-sets.js)
+ content/calendar/calendar-daypicker.xml (content/calendar-daypicker.xml)
+ content/calendar/calendar-views.xml (content/calendar-views.xml)
+* content/calendar/calendar-dnd-listener.js (content/calendar-dnd-listener.js)
+ content/calendar/calendar-extract.js (content/calendar-extract.js)
+ content/calendar/calendar-invitations-manager.js (content/calendar-invitations-manager.js)
+ content/calendar/calendar-item-editing.js (content/calendar-item-editing.js)
+ content/calendar/calendar-item-bindings.xml (content/calendar-item-bindings.xml)
+ content/calendar/calendar-management.js (content/calendar-management.js)
+ content/calendar/calendar-menus.xml (content/calendar-menus.xml)
+ content/calendar/calendar-views.xul (content/calendar-views.xul)
+ content/calendar/calendar-month-view.xml (content/calendar-month-view.xml)
+ content/calendar/calendar-multiday-view.xml (content/calendar-multiday-view.xml)
+ content/calendar/calendar-base-view.xml (content/calendar-base-view.xml)
+ content/calendar/calendar-statusbar.js (content/calendar-statusbar.js)
+ content/calendar/calendar-task-editing.js (content/calendar-task-editing.js)
+ content/calendar/calendar-task-tree.xml (content/calendar-task-tree.xml)
+ content/calendar/calendar-task-tree.js (content/calendar-task-tree.js)
+ content/calendar/calendar-task-view.xul (content/calendar-task-view.xul)
+ content/calendar/calendar-task-view.js (content/calendar-task-view.js)
+ content/calendar/calendar-ui-utils.js (content/calendar-ui-utils.js)
+ content/calendar/calendar-unifinder.xul (content/calendar-unifinder.xul)
+ content/calendar/calendar-unifinder.js (content/calendar-unifinder.js)
+ content/calendar/calendar-unifinder-todo.xul (content/calendar-unifinder-todo.xul)
+ content/calendar/calendar-unifinder-todo.js (content/calendar-unifinder-todo.js)
+ content/calendar/calendar-view-bindings.css (content/calendar-view-bindings.css)
+ content/calendar/calendar-view-core.xml (content/calendar-view-core.xml)
+ content/calendar/calendar-views.js (content/calendar-views.js)
+ content/calendar/import-export.js (content/import-export.js)
+ content/calendar/today-pane.xul (content/today-pane.xul)
+ content/calendar/today-pane.js (content/today-pane.js)
+ content/calendar/calendar-alarm-dialog.js (content/dialogs/calendar-alarm-dialog.js)
+ content/calendar/calendar-alarm-dialog.xul (content/dialogs/calendar-alarm-dialog.xul)
+ content/calendar/calendar-conflicts-dialog.xul (content/dialogs/calendar-conflicts-dialog.xul)
+ content/calendar/calendar-creation.js (content/dialogs/calendar-creation.js)
+ content/calendar/calendar-dialog-utils.js (content/dialogs/calendar-dialog-utils.js)
+ content/calendar/calendar-error-prompt.xul (content/dialogs/calendar-error-prompt.xul)
+ content/calendar/calendar-event-dialog.css (content/dialogs/calendar-event-dialog.css)
+* content/calendar/calendar-event-dialog.xul (content/dialogs/calendar-event-dialog.xul)
+ content/calendar/calendar-event-dialog-attendees.xml (content/dialogs/calendar-event-dialog-attendees.xml)
+ content/calendar/calendar-event-dialog-freebusy.xml (content/dialogs/calendar-event-dialog-freebusy.xml)
+ content/calendar/calendar-event-dialog-recurrence.xul (content/dialogs/calendar-event-dialog-recurrence.xul)
+ content/calendar/calendar-event-dialog-recurrence.js (content/dialogs/calendar-event-dialog-recurrence.js)
+ content/calendar/calendar-event-dialog-recurrence-preview.xml (content/dialogs/calendar-event-dialog-recurrence-preview.xml)
+ content/calendar/calendar-event-dialog-reminder.js (content/dialogs/calendar-event-dialog-reminder.js)
+ content/calendar/calendar-event-dialog-reminder.xul (content/dialogs/calendar-event-dialog-reminder.xul)
+ content/calendar/calendar-event-dialog-timezone.js (content/dialogs/calendar-event-dialog-timezone.js)
+ content/calendar/calendar-event-dialog-timezone.xul (content/dialogs/calendar-event-dialog-timezone.xul)
+ content/calendar/calendar-event-dialog-attendees.xul (content/dialogs/calendar-event-dialog-attendees.xul)
+ content/calendar/calendar-event-dialog-attendees.js (content/dialogs/calendar-event-dialog-attendees.js)
+ content/calendar/calendar-invitations-dialog.css (content/dialogs/calendar-invitations-dialog.css)
+ content/calendar/calendar-invitations-dialog.js (content/dialogs/calendar-invitations-dialog.js)
+ content/calendar/calendar-invitations-dialog.xul (content/dialogs/calendar-invitations-dialog.xul)
+ content/calendar/calendar-invitations-list.xml (content/dialogs/calendar-invitations-list.xml)
+* content/calendar/calendar-migration-dialog.js (content/dialogs/calendar-migration-dialog.js)
+ content/calendar/calendar-migration-dialog.xul (content/dialogs/calendar-migration-dialog.xul)
+ content/calendar/calendar-occurrence-prompt.xul (content/dialogs/calendar-occurrence-prompt.xul)
+ content/calendar/calendar-print-dialog.js (content/dialogs/calendar-print-dialog.js)
+ content/calendar/calendar-print-dialog.xul (content/dialogs/calendar-print-dialog.xul)
+ content/calendar/calendar-properties-dialog.xul (content/dialogs/calendar-properties-dialog.xul)
+ content/calendar/calendar-properties-dialog.js (content/dialogs/calendar-properties-dialog.js)
+ content/calendar/calendar-providerUninstall-dialog.xul (content/dialogs/calendar-providerUninstall-dialog.xul)
+ content/calendar/calendar-providerUninstall-dialog.js (content/dialogs/calendar-providerUninstall-dialog.js)
+ content/calendar/calendar-subscriptions-dialog.css (content/dialogs/calendar-subscriptions-dialog.css)
+ content/calendar/calendar-subscriptions-dialog.js (content/dialogs/calendar-subscriptions-dialog.js)
+ content/calendar/calendar-subscriptions-dialog.xul (content/dialogs/calendar-subscriptions-dialog.xul)
+ content/calendar/calendar-summary-dialog.js (content/dialogs/calendar-summary-dialog.js)
+ content/calendar/calendar-summary-dialog.xul (content/dialogs/calendar-summary-dialog.xul)
+ content/calendar/chooseCalendarDialog.xul (content/dialogs/chooseCalendarDialog.xul)
+ content/calendar/preferences/alarms.xul (content/preferences/alarms.xul)
+ content/calendar/preferences/alarms.js (content/preferences/alarms.js)
+ content/calendar/preferences/categories.xul (content/preferences/categories.xul)
+ content/calendar/preferences/categories.js (content/preferences/categories.js)
+ content/calendar/preferences/editCategory.xul (content/preferences/editCategory.xul)
+ content/calendar/preferences/editCategory.js (content/preferences/editCategory.js)
+ content/calendar/preferences/general.js (content/preferences/general.js)
+* content/calendar/preferences/general.xul (content/preferences/general.xul)
+ content/calendar/preferences/views.js (content/preferences/views.js)
+ content/calendar/preferences/views.xul (content/preferences/views.xul)
+ content/calendar/widgets/minimonth.xml (content/widgets/minimonth.xml)
+ content/calendar/widgets/calendar-alarm-widget.xml (content/widgets/calendar-alarm-widget.xml)
+ content/calendar/widgets/calendar-widgets.xml (content/widgets/calendar-widgets.xml)
+ content/calendar/widgets/calendar-list-tree.xml (content/widgets/calendar-list-tree.xml)
+ content/calendar/calendar-subscriptions-list.xml (content/widgets/calendar-subscriptions-list.xml)
+ content/calendar/widgets/calendar-widget-bindings.css (content/widgets/calendar-widget-bindings.css)
+ content/calendar/calApplicationUtils.js (src/calApplicationUtils.js)
+ content/calendar/calUtils.js (src/calUtils.js)
+ content/calendar/calFilter.js (src/calFilter.js)
+ content/calendar/WindowsNTToZoneInfoTZId.properties (src/WindowsNTToZoneInfoTZId.properties)
+% skin calendar classic/1.0 chrome/skin/linux/calendar/
+% skin calendar classic/1.0 chrome/skin/windows/calendar/ os=WINNT
+% skin calendar-common classic/1.0 chrome/skin/common/
+% style chrome://global/content/customizeToolbar.xul chrome://calendar/skin/calendar-task-view.css
+% style chrome://global/content/customizeToolbar.xul chrome://calendar/skin/calendar-event-dialog.css
+% style chrome://calendar/content/calendar-event-dialog.xul chrome://calendar-common/skin/dialogs/calendar-event-dialog.css
+% style chrome://lightning/content/lightning-item-iframe.xul chrome://calendar-common/skin/dialogs/calendar-event-dialog.css
+% style chrome://calendar/content/calendar-event-dialog-attendees.xul chrome://calendar-common/skin/dialogs/calendar-event-dialog.css
+ ../skin/common/alarm-flashing.png (themes/common/images/alarm-flashing.png)
+ ../skin/common/alarm-icons.png (themes/common/images/alarm-icons.png)
+ ../skin/common/attendee-icons.png (themes/common/images/attendee-icons.png)
+ ../skin/common/calendar-overlay.png (themes/common/images/calendar-overlay.png)
+ ../skin/common/calendar-status.png (themes/common/images/calendar-status.png)
+ ../skin/common/checkbox-images.png (themes/common/images/checkbox-images.png)
+ ../skin/common/classification.png (themes/common/images/classification.png)
+ ../skin/common/day-box-item-image.png (themes/common/images/day-box-item-image.png)
+ ../skin/common/event-grippy-bottom.png (themes/common/images/event-grippy-bottom.png)
+ ../skin/common/event-grippy-left.png (themes/common/images/event-grippy-left.png)
+ ../skin/common/event-grippy-right.png (themes/common/images/event-grippy-right.png)
+ ../skin/common/event-grippy-top.png (themes/common/images/event-grippy-top.png)
+ ../skin/common/ok-cancel.png (themes/common/images/ok-cancel.png)
+ ../skin/common/task-images.png (themes/common/images/task-images.png)
+ ../skin/common/timezone_map.png (themes/common/images/timezone_map.png)
+ ../skin/common/timezones.png (themes/common/images/timezones.png)
+ ../skin/common/calendar-event-dialog.png (themes/common/dialogs/images/calendar-event-dialog.png)
+ ../skin/common/calendar-event-tab.png (themes/common/dialogs/images/calendar-event-tab.png)
+ ../skin/common/calendar-task-tab.png (themes/common/dialogs/images/calendar-task-tab.png)
+ ../skin/common/widgets/nav-arrow.svg (themes/common/widgets/images/nav-arrow.svg)
+ ../skin/common/widgets/nav-today.svg (themes/common/widgets/images/nav-today.svg)
+ ../skin/common/widgets/nav-today-hov.svg (themes/common/widgets/images/nav-today-hov.svg)
+ ../skin/common/widgets/view-navigation.svg (themes/common/widgets/images/view-navigation.svg)
+ ../skin/common/widgets/view-navigation-hov.svg (themes/common/widgets/images/view-navigation-hov.svg)
+ ../skin/common/widgets/drag-center.svg (themes/common/widgets/images/drag-center.svg)
+ ../skin/common/calendar-alarms.css (themes/common/calendar-alarms.css)
+ ../skin/common/calendar-attendees.css (themes/common/calendar-attendees.css)
+ ../skin/common/calendar-creation-wizard.css (themes/common/calendar-creation-wizard.css)
+ ../skin/common/calendar-daypicker.css (themes/common/calendar-daypicker.css)
+ ../skin/common/calendar-management.css (themes/common/calendar-management.css)
+ ../skin/common/calendar-occurrence-prompt.css (themes/common/calendar-occurrence-prompt.css)
+ ../skin/common/calendar-printing.css (themes/common/calendar-printing.css)
+ ../skin/common/calendar-providerUninstall-dialog.css (themes/common/calendar-providerUninstall-dialog.css)
+ ../skin/common/calendar-task-tree.css (themes/common/calendar-task-tree.css)
+ ../skin/common/calendar-task-view.css (themes/common/calendar-task-view.css)
+ ../skin/common/calendar-toolbar.svg (themes/common/calendar-toolbar.svg)
+ ../skin/common/calendar-toolbar-osxlion.svg (themes/common/calendar-toolbar-osxlion.svg)
+ ../skin/common/calendar-itip-icons.svg (themes/common/calendar-itip-icons.svg)
+ ../skin/common/calendar-unifinder.css (themes/common/calendar-unifinder.css)
+ ../skin/common/calendar-views.css (themes/common/calendar-views.css)
+ ../skin/common/today-pane.css (themes/common/today-pane.css)
+ ../skin/common/today-pane-cycler.svg (themes/common/today-pane-cycler.svg)
+ ../skin/common/dialogs/calendar-alarm-dialog.css (themes/common/dialogs/calendar-alarm-dialog.css)
+ ../skin/common/dialogs/calendar-event-dialog.css (themes/common/dialogs/calendar-event-dialog.css)
+ ../skin/common/dialogs/calendar-invitations-dialog.css (themes/common/dialogs/calendar-invitations-dialog.css)
+ ../skin/common/calendar-event-dialog-attendees.png (themes/common/dialogs/images/calendar-event-dialog-attendees.png)
+ ../skin/common/calendar-invitations-dialog-button-images.png (themes/common/dialogs/images/calendar-invitations-dialog-button-images.png)
+ ../skin/common/calendar-invitations-dialog-list-images.png (themes/common/dialogs/images/calendar-invitations-dialog-list-images.png)
+ ../skin/common/calendar-properties-dialog.css (themes/common/dialogs/calendar-properties-dialog.css)
+ ../skin/common/calendar-subscriptions-dialog.css (themes/common/dialogs/calendar-subscriptions-dialog.css)
+ ../skin/common/calendar-timezone-highlighter.css (themes/common/dialogs/calendar-timezone-highlighter.css)
+ ../skin/common/widgets/calendar-widgets.css (themes/common/widgets/calendar-widgets.css)
+ ../skin/common/widgets/minimonth.css (themes/common/widgets/minimonth.css)
+
+# Linux theme files
+ ../skin/linux/calendar/cal-icon32.png (themes/linux/images/cal-icon32.png)
+ ../skin/linux/calendar/cal-icon24.png (themes/linux/images/cal-icon24.png)
+ ../skin/linux/calendar/calendar-alarm-dialog.css (themes/linux/dialogs/calendar-alarm-dialog.css)
+ ../skin/linux/calendar/calendar-daypicker.css (themes/linux/calendar-daypicker.css)
+ ../skin/linux/calendar/calendar-event-dialog.css (themes/linux/dialogs/calendar-event-dialog.css)
+ ../skin/linux/calendar/calendar-invitations-dialog.css (themes/linux/dialogs/calendar-invitations-dialog.css)
+ ../skin/linux/calendar/calendar-management.css (themes/linux/calendar-management.css)
+ ../skin/linux/calendar/calendar-task-tree.css (themes/linux/calendar-task-tree.css)
+ ../skin/linux/calendar/calendar-task-view.css (themes/linux/calendar-task-view.css)
+ ../skin/linux/calendar/calendar-unifinder.css (themes/linux/calendar-unifinder.css)
+ ../skin/linux/calendar/calendar-views.css (themes/linux/calendar-views.css)
+ ../skin/linux/calendar/today-pane.css (themes/linux/today-pane.css)
+ ../skin/linux/calendar/widgets/calendar-widgets.css (themes/linux/widgets/calendar-widgets.css)
+ ../skin/linux/calendar/calendar-occurrence-prompt.png (themes/linux/images/calendar-occurrence-prompt.png)
+
+# Windows theme files
+ ../skin/windows/calendar/cal-icon32.png (themes/windows/images/cal-icon32.png)
+ ../skin/windows/calendar/cal-icon24.png (themes/windows/images/cal-icon24.png)
+ ../skin/windows/calendar/calendar-alarm-dialog.css (themes/windows/dialogs/calendar-alarm-dialog.css)
+ ../skin/windows/calendar/calendar-daypicker.css (themes/windows/calendar-daypicker.css)
+ ../skin/windows/calendar/calendar-event-dialog.css (themes/windows/dialogs/calendar-event-dialog.css)
+ ../skin/windows/calendar/calendar-invitations-dialog.css (themes/windows/dialogs/calendar-invitations-dialog.css)
+ ../skin/windows/calendar/calendar-management.css (themes/windows/calendar-management.css)
+ ../skin/windows/calendar/calendar-task-tree.css (themes/windows/calendar-task-tree.css)
+ ../skin/windows/calendar/calendar-task-view.css (themes/windows/calendar-task-view.css)
+ ../skin/windows/calendar/calendar-unifinder.css (themes/windows/calendar-unifinder.css)
+ ../skin/windows/calendar/calendar-views.css (themes/windows/calendar-views.css)
+ ../skin/windows/calendar/today-pane.css (themes/windows/today-pane.css)
+ ../skin/windows/calendar/widgets/calendar-widgets.css (themes/windows/widgets/calendar-widgets.css)
+ ../skin/windows/calendar/calendar-event-dialog-toolbar.png (themes/windows/dialogs/images/calendar-event-dialog-toolbar.png)
+ ../skin/windows/calendar/calendar-event-dialog-toolbar-small.png (themes/windows/dialogs/images/calendar-event-dialog-toolbar-small.png)
+ ../skin/windows/calendar/calendar-occurrence-prompt.png (themes/windows/images/calendar-occurrence-prompt.png)
+ ../skin/windows/calendar/tasks-actions.png (themes/windows/images/tasks-actions.png)
+ ../skin/windows/calendar/toolbar-large.png (themes/windows/images/toolbar-large.png)
+ ../skin/windows/calendar/toolbar-small.png (themes/windows/images/toolbar-small.png)
+ ../skin/windows/calendar/calendar-occurrence-prompt-aero.png (themes/windows/images/calendar-occurrence-prompt-aero.png)
diff --git a/calendar/base/modules/calAlarmUtils.jsm b/calendar/base/modules/calAlarmUtils.jsm
new file mode 100644
index 000000000..25532a24b
--- /dev/null
+++ b/calendar/base/modules/calAlarmUtils.jsm
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+cal.alarms = {
+ /**
+ * Read default alarm settings from user preferences and apply them to the
+ * event/todo passed in. The item's calendar should be set to ensure the
+ * correct alarm type is set.
+ *
+ * @param aItem The item to apply the default alarm values to.
+ */
+ setDefaultValues: function(aItem) {
+ let type = cal.isEvent(aItem) ? "event" : "todo";
+ if (Preferences.get("calendar.alarms.onfor" + type + "s", 0) == 1) {
+ let alarmOffset = cal.createDuration();
+ let alarm = cal.createAlarm();
+ let units = Preferences.get("calendar.alarms." + type + "alarmunit", "minutes");
+
+ // Make sure the alarm pref is valid, default to minutes otherwise
+ if (!["weeks", "days", "hours", "minutes", "seconds"].includes(units)) {
+ units = "minutes";
+ }
+
+ alarmOffset[units] = Preferences.get("calendar.alarms." + type + "alarmlen", 0);
+ alarmOffset.normalize();
+ alarmOffset.isNegative = true;
+ if (type == "todo" && !aItem.entryDate) {
+ // You can't have an alarm if the entryDate doesn't exist.
+ aItem.entryDate = cal.now();
+ }
+ alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+ alarm.offset = alarmOffset;
+
+ // Default to a display alarm, unless the calendar doesn't support
+ // it or we have no calendar yet. (Man this is hard to wrap)
+ let actionValues = (aItem.calendar &&
+ aItem.calendar.getProperty("capabilities.alarms.actionValues")) ||
+ ["DISPLAY"];
+
+ alarm.action = (actionValues.includes("DISPLAY") ? "DISPLAY" : actionValues[0]);
+ aItem.addAlarm(alarm);
+ }
+ },
+
+ /**
+ * Calculate the alarm date for a calIAlarm.
+ *
+ * @param aItem The item used to calculate the alarm date.
+ * @param aAlarm The alarm to calculate the date for.
+ * @return The alarm date.
+ */
+ calculateAlarmDate: function(aItem, aAlarm) {
+ if (aAlarm.related == aAlarm.ALARM_RELATED_ABSOLUTE) {
+ return aAlarm.alarmDate;
+ } else {
+ let returnDate;
+ if (aAlarm.related == aAlarm.ALARM_RELATED_START) {
+ returnDate = aItem[cal.calGetStartDateProp(aItem)];
+ } else if (aAlarm.related == aAlarm.ALARM_RELATED_END) {
+ returnDate = aItem[cal.calGetEndDateProp(aItem)];
+ }
+
+ if (returnDate && aAlarm.offset) {
+ // Handle all day events. This is kinda weird, because they don't
+ // have a well defined startTime. We just consider the start/end
+ // to be midnight in the user's timezone.
+ if (returnDate.isDate) {
+ let timezone = cal.calendarDefaultTimezone();
+ // This returns a copy, so no extra cloning needed.
+ returnDate = returnDate.getInTimezone(timezone);
+ returnDate.isDate = false;
+ } else {
+ if (returnDate.timezone.tzid == "floating") {
+ let timezone = cal.calendarDefaultTimezone();
+ returnDate = returnDate.getInTimezone(timezone);
+ } else {
+ // Clone the date to correctly add the duration.
+ returnDate = returnDate.clone();
+ }
+ }
+
+ returnDate.addDuration(aAlarm.offset);
+ return returnDate;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Calculate the alarm offset for a calIAlarm. The resulting offset is
+ * related to either start or end of the event, depending on the aRelated
+ * parameter.
+ *
+ * @param aItem The item to calculate the offset for.
+ * @param aAlarm The alarm to calculate the offset for.
+ * @param aRelated (optional) A relation constant from calIAlarm. If not
+ * passed, ALARM_RELATED_START will be assumed.
+ * @return The alarm offset.
+ */
+ calculateAlarmOffset: function(aItem, aAlarm, aRelated) {
+ let offset = aAlarm.offset;
+ if (aAlarm.related == aAlarm.ALARM_RELATED_ABSOLUTE) {
+ let returnDate;
+ if (aRelated === undefined || aRelated == aAlarm.ALARM_RELATED_START) {
+ returnDate = aItem[cal.calGetStartDateProp(aItem)];
+ } else if (aRelated == aAlarm.ALARM_RELATED_END) {
+ returnDate = aItem[cal.calGetEndDateProp(aItem)];
+ }
+
+ if (returnDate && aAlarm.alarmDate) {
+ offset = aAlarm.alarmDate.subtractDate(returnDate);
+ }
+ }
+ return offset;
+ },
+
+ /**
+ * Adds reminder images to a given node, making sure only one icon per alarm
+ * action is added.
+ *
+ * @param aElement The element to add the images to.
+ * @param aReminders The set of reminders to add images for.
+ */
+ addReminderImages: function(aElement, aReminders) {
+ function createOwnedXULNode(elem) {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return aElement.ownerDocument.createElementNS(XUL_NS, elem);
+ }
+
+ function setupActionImage(node, reminder) {
+ let image = node || createOwnedXULNode("image");
+ image.setAttribute("class", "reminder-icon");
+ image.setAttribute("value", reminder.action);
+ return image;
+ }
+
+ // Fill up the icon box with the alarm icons, show max one icon per
+ // alarm type.
+ let countIconChildren = aElement.childNodes.length;
+ let actionMap = {};
+ let i, offset;
+ for (i = 0, offset = 0; i < aReminders.length; i++) {
+ let reminder = aReminders[i];
+ if (reminder.action in actionMap) {
+ // Only show one icon of each type;
+ offset++;
+ continue;
+ }
+ actionMap[reminder.action] = true;
+
+ if (i - offset >= countIconChildren) {
+ // Not enough nodes, append it.
+ aElement.appendChild(setupActionImage(null, reminder));
+ } else {
+ // There is already a node there, change its properties
+ setupActionImage(aElement.childNodes[i - offset], reminder);
+ }
+ }
+
+ // Remove unused image nodes
+ for (i -= offset; i < countIconChildren; i++) {
+ aElement.childNodes[i].remove();
+ }
+ }
+};
diff --git a/calendar/base/modules/calAsyncUtils.jsm b/calendar/base/modules/calAsyncUtils.jsm
new file mode 100644
index 000000000..cbb2adb5a
--- /dev/null
+++ b/calendar/base/modules/calAsyncUtils.jsm
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/*
+ * Asynchronous tools for handling calendar operations.
+ */
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+var cIOL = Components.interfaces.calIOperationListener;
+var cIC = Components.interfaces.calICalendar;
+
+var promisifyProxyHandler = {
+ promiseOperation: function(target, name, args) {
+ let deferred = PromiseUtils.defer();
+ let listener = cal.async.promiseOperationListener(deferred);
+ args.push(listener);
+ target[name](...args);
+ return deferred.promise;
+ },
+ get: function(target, name) {
+ switch (name) {
+ case "adoptItem":
+ case "addItem":
+ case "modifyItem":
+ case "deleteItem":
+ case "getItem":
+ case "getItems":
+ return (...args) => this.promiseOperation(target, name, args);
+ case "getAllItems":
+ return () => this.promiseOperation(target, "getItems", [cIC.ITEM_FILTER_ALL_ITEMS, 0, null, null]);
+ default:
+ return target[name];
+ }
+ }
+};
+
+cal.async = {
+ /**
+ * Creates a proxy to the given calendar where the CRUD operations are replaced
+ * with versions that return a promise and don't take a listener.
+ *
+ * Before:
+ * calendar.addItem(item, {
+ * onGetResult: function() {},
+ * onOperationComplete: function (c,status,t,c,detail) {
+ * if (Components.isSuccessCode(status)) {
+ * handleSuccess(detail);
+ * } else {
+ * handleFailure(status);
+ * }
+ * }
+ * });
+ *
+ * After:
+ * let pcal = promisifyCalendar(calendar);
+ * pcal.addItem(item).then(handleSuccess, handleFailure);
+ *
+ * Bonus methods in addition:
+ * pcal.getAllItems() // alias for getItems without any filters
+ *
+ * IMPORTANT: Don't pass this around thinking its like an xpcom calICalendar,
+ * otherwise code might indefinitely wait for the listener to return or there
+ * will be complaints that an argument is missing.
+ */
+ promisifyCalendar: function(aCalendar) {
+ return new Proxy(aCalendar, promisifyProxyHandler);
+ },
+ /**
+ * Create an operation listener (calIOperationListener) that resolves when
+ * the operation succeeds. Note this listener will collect the items, so it
+ * might not be a good idea in a situation where a lot of items will be
+ * retrieved.
+ *
+ * Standalone Usage:
+ * function promiseAddItem(aItem) {
+ * let deferred = PromiseUtils.defer();
+ * let listener = cal.async.promiseOperationListener(deferred);
+ * aItem.calendar.addItem(aItem, listener);
+ * return deferred.promise;
+ * }
+ *
+ * See also promisifyCalendar, where the above can be replaced with:
+ * function promiseAddItem(aItem) {
+ * let calendar = cal.async.promisifyCalendar(aItem.calendar);
+ * return calendar.addItem(aItem);
+ * }
+ */
+ promiseOperationListener: function(deferred) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ items: [],
+ itemStatus: Components.results.NS_OK,
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail,
+ aCount, aItems) {
+ this.itemStatus = aStatus;
+ if (Components.isSuccessCode(aStatus)) {
+ this.items = this.items.concat(aItems);
+ } else {
+ this.itemSuccess = aStatus;
+ }
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ if (!Components.isSuccessCode(aStatus)) {
+ // This function has failed, reject with the status
+ deferred.reject(aStatus);
+ } else if (!Components.isSuccessCode(this.itemStatus)) {
+ // onGetResult has failed, reject with its status
+ deferred.reject(this.itemStatus);
+ } else if (aOpType == cIOL.GET) {
+ // Success of a GET operation: resolve with array of
+ // resulting items.
+ deferred.resolve(this.items);
+ } else { /* ADD,MODIFY,DELETE: resolve with 1 item */
+ // Success of an ADD MODIFY or DELETE operation, resolve
+ // with the one item that was processed.
+ deferred.resolve(aDetail);
+ }
+ }
+ };
+ }
+};
diff --git a/calendar/base/modules/calAuthUtils.jsm b/calendar/base/modules/calAuthUtils.jsm
new file mode 100644
index 000000000..2862f3edf
--- /dev/null
+++ b/calendar/base/modules/calAuthUtils.jsm
@@ -0,0 +1,385 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/*
+ * Authentication helper code
+ */
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+cal.auth = {
+ /**
+ * Auth prompt implementation - Uses password manager if at all possible.
+ */
+ Prompt: function() {
+ this.mWindow = cal.getCalendarWindow();
+ this.mReturnedLogins = {};
+ },
+
+ /**
+ * Tries to get the username/password combination of a specific calendar name
+ * from the password manager or asks the user.
+ *
+ * @param in aTitle The dialog title.
+ * @param in aCalendarName The calendar name or url to look up. Can be null.
+ * @param inout aUsername The username that belongs to the calendar.
+ * @param inout aPassword The password that belongs to the calendar.
+ * @param inout aSavePassword Should the password be saved?
+ * @param in aFixedUsername Whether the user name is fixed or editable
+ * @return Could a password be retrieved?
+ */
+ getCredentials: function(aTitle, aCalendarName, aUsername, aPassword,
+ aSavePassword, aFixedUsername) {
+ if (typeof aUsername != "object" ||
+ typeof aPassword != "object" ||
+ typeof aSavePassword != "object") {
+ throw new Components.Exception("", Components.results.NS_ERROR_XPC_NEED_OUT_OBJECT);
+ }
+
+ let prompter = Services.ww.getNewPrompter(null);
+
+ // Only show the save password box if we are supposed to.
+ let savepassword = null;
+ if (Preferences.get("signon.rememberSignons", true)) {
+ savepassword = cal.calGetString("passwordmgr", "rememberPassword", null, "passwordmgr");
+ }
+
+ let aText;
+ if (aFixedUsername) {
+ aText = cal.calGetString("commonDialogs", "EnterPasswordFor", [aUsername.value, aCalendarName], "global");
+ return prompter.promptPassword(aTitle,
+ aText,
+ aPassword,
+ savepassword,
+ aSavePassword);
+ } else {
+ aText = cal.calGetString("commonDialogs", "EnterUserPasswordFor2", [aCalendarName], "global");
+ return prompter.promptUsernameAndPassword(aTitle,
+ aText,
+ aUsername,
+ aPassword,
+ savepassword,
+ aSavePassword);
+ }
+ },
+
+ /**
+ * Make sure the passed origin is actually an uri string, because password
+ * manager functions require it. This is a fallback for compatibility only
+ * and should be removed a few versions after Lightning 5.5
+ *
+ * @param aOrigin The hostname or origin to check
+ * @return The origin uri
+ */
+ _ensureOrigin: function(aOrigin) {
+ try {
+ return Services.io.newURI(aOrigin, null, null).spec;
+ } catch (e) {
+ return "https://" + aOrigin;
+ }
+ },
+
+ /**
+ * Helper to insert/update an entry to the password manager.
+ *
+ * @param aUserName The username
+ * @param aPassword The corresponding password
+ * @param aOrigin The corresponding origin
+ * @param aRealm The password realm (unused on branch)
+ */
+ passwordManagerSave: function(aUsername, aPassword, aOrigin, aRealm) {
+ cal.ASSERT(aUsername);
+ cal.ASSERT(aPassword);
+
+ let origin = this._ensureOrigin(aOrigin);
+
+ try {
+ let logins = Services.logins.findLogins({}, origin, null, aRealm);
+
+ let newLoginInfo = Components.classes["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Components.interfaces.nsILoginInfo);
+ newLoginInfo.init(origin, null, aRealm, aUsername, aPassword, "", "");
+ if (logins.length > 0) {
+ Services.logins.modifyLogin(logins[0], newLoginInfo);
+ } else {
+ Services.logins.addLogin(newLoginInfo);
+ }
+ } catch (exc) {
+ // Only show the message if its not an abort, which can happen if
+ // the user canceled the master password dialog
+ cal.ASSERT(exc.result == Components.results.NS_ERROR_ABORT, exc);
+ }
+ },
+
+ /**
+ * Helper to retrieve an entry from the password manager.
+ *
+ * @param in aUsername The username to search
+ * @param out aPassword The corresponding password
+ * @param aOrigin The corresponding origin
+ * @param aRealm The password realm (unused on branch)
+ * @return Does an entry exist in the password manager
+ */
+ passwordManagerGet: function(aUsername, aPassword, aOrigin, aRealm) {
+ cal.ASSERT(aUsername);
+
+ if (typeof aPassword != "object") {
+ throw new Components.Exception("", Components.results.NS_ERROR_XPC_NEED_OUT_OBJECT);
+ }
+
+ let origin = this._ensureOrigin(aOrigin);
+
+ try {
+ if (!Services.logins.getLoginSavingEnabled(origin)) {
+ return false;
+ }
+
+ let logins = Services.logins.findLogins({}, origin, null, aRealm);
+ for (let loginInfo of logins) {
+ if (loginInfo.username == aUsername) {
+ aPassword.value = loginInfo.password;
+ return true;
+ }
+ }
+ } catch (exc) {
+ cal.ASSERT(false, exc);
+ }
+ return false;
+ },
+
+ /**
+ * Helper to remove an entry from the password manager
+ *
+ * @param aUsername The username to remove.
+ * @param aOrigin The corresponding origin
+ * @param aRealm The password realm (unused on branch)
+ * @return Could the user be removed?
+ */
+ passwordManagerRemove: function(aUsername, aOrigin, aRealm) {
+ cal.ASSERT(aUsername);
+
+ let origin = this._ensureOrigin(aOrigin);
+
+ try {
+ let logins = Services.logins.findLogins({}, origin, null, aRealm);
+ for (let loginInfo of logins) {
+ if (loginInfo.username == aUsername) {
+ Services.logins.removeLogin(loginInfo);
+ return true;
+ }
+ }
+ } catch (exc) {
+ // If no logins are found, fall through to the return statement below.
+ }
+ return false;
+ }
+};
+
+/**
+ * Calendar Auth prompt implementation. This instance of the auth prompt should
+ * be used by providers and other components that handle authentication using
+ * nsIAuthPrompt2 and friends.
+ *
+ * This implementation guarantees there are no request loops when an invalid
+ * password is stored in the login-manager.
+ *
+ * There is one instance of that object per calendar provider.
+ */
+cal.auth.Prompt.prototype = {
+ mProvider: null,
+
+ getPasswordInfo: function(aPasswordRealm) {
+ let username;
+ let password;
+ let found = false;
+
+ let logins = Services.logins.findLogins({}, aPasswordRealm.prePath, null, aPasswordRealm.realm);
+ if (logins.length) {
+ username = logins[0].username;
+ password = logins[0].password;
+ found = true;
+ }
+ if (found) {
+ let keyStr = aPasswordRealm.prePath + ":" + aPasswordRealm.realm;
+ let now = new Date();
+ // Remove the saved password if it was already returned less
+ // than 60 seconds ago. The reason for the timestamp check is that
+ // nsIHttpChannel can call the nsIAuthPrompt2 interface
+ // again in some situation. ie: When using Digest auth token
+ // expires.
+ if (this.mReturnedLogins[keyStr] &&
+ now.getTime() - this.mReturnedLogins[keyStr].getTime() < 60000) {
+ cal.LOG("Credentials removed for: user=" + username + ", host=" + aPasswordRealm.prePath + ", realm=" + aPasswordRealm.realm)
+;
+ delete this.mReturnedLogins[keyStr];
+ cal.auth.passwordManagerRemove(username,
+ aPasswordRealm.prePath,
+ aPasswordRealm.realm);
+ return { found: false, username: username };
+ } else {
+ this.mReturnedLogins[keyStr] = now;
+ }
+ }
+ return { found: found, username: username, password: password };
+ },
+
+ /**
+ * Requests a username and a password. Implementations will commonly show a
+ * dialog with a username and password field, depending on flags also a
+ * domain field.
+ *
+ * @param aChannel
+ * The channel that requires authentication.
+ * @param level
+ * One of the level constants NONE, PW_ENCRYPTED, SECURE.
+ * @param authInfo
+ * Authentication information object. The implementation should fill in
+ * this object with the information entered by the user before
+ * returning.
+ *
+ * @retval true
+ * Authentication can proceed using the values in the authInfo
+ * object.
+ * @retval false
+ * Authentication should be cancelled, usually because the user did
+ * not provide username/password.
+ *
+ * @note Exceptions thrown from this function will be treated like a
+ * return value of false.
+ */
+ promptAuth: function(aChannel, aLevel, aAuthInfo) {
+ let hostRealm = {};
+ hostRealm.prePath = aChannel.URI.prePath;
+ hostRealm.realm = aAuthInfo.realm;
+ let port = aChannel.URI.port;
+ if (port == -1) {
+ let handler = Services.io.getProtocolHandler(aChannel.URI.scheme)
+ .QueryInterface(Components.interfaces.nsIProtocolHandler);
+ port = handler.defaultPort;
+ }
+ hostRealm.passwordRealm = aChannel.URI.host + ":" + port + " (" + aAuthInfo.realm + ")";
+
+ let pwInfo = this.getPasswordInfo(hostRealm);
+ aAuthInfo.username = pwInfo.username;
+ if (pwInfo && pwInfo.found) {
+ aAuthInfo.password = pwInfo.password;
+ return true;
+ } else {
+ let prompter2 = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Components.interfaces.nsIPromptFactory)
+ .getPrompt(this.mWindow, Components.interfaces.nsIAuthPrompt2);
+ return prompter2.promptAuth(aChannel, aLevel, aAuthInfo);
+ }
+ },
+
+ /**
+ * Asynchronously prompt the user for a username and password.
+ * This has largely the same semantics as promptAuth(),
+ * but must return immediately after calling and return the entered
+ * data in a callback.
+ *
+ * If the user closes the dialog using a cancel button or similar,
+ * the callback's nsIAuthPromptCallback::onAuthCancelled method must be
+ * called.
+ * Calling nsICancelable::cancel on the returned object SHOULD close the
+ * dialog and MUST call nsIAuthPromptCallback::onAuthCancelled on the provided
+ * callback.
+ *
+ * @throw NS_ERROR_NOT_IMPLEMENTED
+ * Asynchronous authentication prompts are not supported;
+ * the caller should fall back to promptUsernameAndPassword().
+ */
+ asyncPromptAuth: function(aChannel, // nsIChannel
+ aCallback, // nsIAuthPromptCallback
+ aContext, // nsISupports
+ aLevel, // PRUint32
+ aAuthInfo) { // nsIAuthInformation
+ let self = this;
+ let promptlistener = {
+ onPromptStart: function() {
+ res = self.promptAuth(aChannel, aLevel, aAuthInfo);
+
+ if (res) {
+ gAuthCache.setAuthInfo(hostKey, aAuthInfo);
+ this.onPromptAuthAvailable();
+ return true;
+ }
+
+ this.onPromptCanceled();
+ return false;
+ },
+
+ onPromptAuthAvailable: function() {
+ let authInfo = gAuthCache.retrieveAuthInfo(hostKey);
+ if (authInfo) {
+ aAuthInfo.username = authInfo.username;
+ aAuthInfo.password = authInfo.password;
+ }
+ aCallback.onAuthAvailable(aContext, aAuthInfo);
+ },
+
+ onPromptCanceled: function() {
+ gAuthCache.retrieveAuthInfo(hostKey);
+ aCallback.onAuthCancelled(aContext, true);
+ }
+ };
+
+ let hostKey = aChannel.URI.prePath + ":" + aAuthInfo.realm;
+ gAuthCache.planForAuthInfo(hostKey);
+
+ function queuePrompt() {
+ let asyncprompter = Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
+ .getService(Components.interfaces.nsIMsgAsyncPrompter);
+ asyncprompter.queueAsyncAuthPrompt(hostKey, false, promptlistener);
+ }
+
+ self.mWindow = cal.getCalendarWindow();
+
+ // the prompt will fail if we are too early
+ if (self.mWindow.document.readyState == "complete") {
+ queuePrompt();
+ } else {
+ self.mWindow.addEventListener("load", queuePrompt, true);
+ }
+ }
+};
+
+// Cache for authentication information since onAuthInformation in the prompt
+// listener is called without further information. If the password is not
+// saved, there is no way to retrieve it. We use ref counting to avoid keeping
+// the password in memory longer than needed.
+var gAuthCache = {
+ _authInfoCache: new Map(),
+ planForAuthInfo: function(hostKey) {
+ let authInfo = this._authInfoCache.get(hostKey);
+ if (authInfo) {
+ authInfo.refCnt++;
+ } else {
+ this._authInfoCache.set(hostKey, { refCnt: 1 });
+ }
+ },
+
+ setAuthInfo: function(hostKey, aAuthInfo) {
+ let authInfo = this._authInfoCache.get(hostKey);
+ if (authInfo) {
+ authInfo.username = aAuthInfo.username;
+ authInfo.password = aAuthInfo.password;
+ }
+ },
+
+ retrieveAuthInfo: function(hostKey) {
+ let authInfo = this._authInfoCache.get(hostKey);
+ if (authInfo) {
+ authInfo.refCnt--;
+
+ if (authInfo.refCnt == 0) {
+ this._authInfoCache.delete(hostKey);
+ }
+ }
+ return authInfo;
+ }
+};
diff --git a/calendar/base/modules/calExtract.jsm b/calendar/base/modules/calExtract.jsm
new file mode 100644
index 000000000..146ee11b4
--- /dev/null
+++ b/calendar/base/modules/calExtract.jsm
@@ -0,0 +1,1296 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["Extractor"];
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+/**
+* Initializes extraction
+*
+* @param fallbackLocale locale to use when others are not found or
+* detection is disabled
+* @param dayStart ambiguous hours earlier than this are considered to
+* be in the afternoon, when null then by default
+* set to 6
+* @param fixedLang whether to use only fallbackLocale for extraction
+*/
+function Extractor(fallbackLocale, dayStart, fixedLang) {
+ // url for multi locale AMO build
+ this.bundleUrl = "resource://calendar/chrome/calendar-LOCALE/locale/LOCALE/calendar/calendar-extract.properties";
+ // url for single locale python packaged build
+ this.packagedUrl = "jar:resource://calendar/chrome.jar!/calendar-LOCALE/locale/LOCALE/calendar/calendar-extract.properties";
+ this.fallbackLocale = fallbackLocale;
+ this.email = "";
+ this.marker = "--MARK--";
+ // this should never be found in an email
+ this.defPattern = "061dc19c-719f-47f3-b2b5-e767e6f02b7a";
+ this.collected = [];
+ this.numbers = [];
+ this.hourlyNumbers = [];
+ this.dailyNumbers = [];
+ this.allMonths = "";
+ this.months = [];
+ this.dayStart = 6;
+ this.now = new Date();
+ this.bundle = "";
+ this.overrides = {};
+ this.fixedLang = true;
+
+ if (dayStart != null) {
+ this.dayStart = dayStart;
+ }
+
+ if (fixedLang != null) {
+ this.fixedLang = fixedLang;
+ }
+
+ if (!this.checkBundle(fallbackLocale)) {
+ this.bundleUrl = this.packagedUrl;
+ cal.WARN("Your installed Lightning only includes a single locale, extracting event info from other languages is likely inaccurate. You can install Lightning from addons.mozilla.org manually for multiple locale support.");
+ }
+}
+
+Extractor.prototype = {
+ /**
+ * Removes confusing data like urls, timezones and phone numbers from email
+ * Also removes standard signatures and quoted content from previous emails
+ */
+ cleanup: function() {
+ // XXX remove earlier correspondence
+ // ideally this should be considered with lower certainty to fill in
+ // missing information
+
+ // remove last line preceeding quoted message and first line of the quote
+ this.email = this.email.replace(/\r?\n[^>].*\r?\n>+.*$/m, "");
+ // remove the rest of quoted content
+ this.email = this.email.replace(/^>+.*$/gm, "");
+
+ // urls often contain dates dates that can confuse extraction
+ this.email = this.email.replace(/https?:\/\/[^\s]+\s/gm, "");
+ this.email = this.email.replace(/www\.[^\s]+\s/gm, "");
+
+ // remove phone numbers
+ // TODO allow locale specific configuration of formats
+ this.email = this.email.replace(/\d-\d\d\d-\d\d\d-\d\d\d\d/gm, "");
+
+ // remove standard signature
+ this.email = this.email.replace(/\r?\n-- \r?\n[\S\s]+$/, "");
+
+ // XXX remove timezone info, for now
+ this.email = this.email.replace(/gmt[+-]\d{2}:\d{2}/gi, "");
+ },
+
+ checkBundle: function(locale) {
+ let path = this.bundleUrl.replace(/LOCALE/g, locale);
+ let bundle = Services.strings.createBundle(path);
+
+ try {
+ bundle.GetStringFromName("from.today");
+ return true;
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ avgNonAsciiCharCode: function() {
+ let sum = 0;
+ let cnt = 0;
+
+ for (let i = 0; i < this.email.length; i++) {
+ let char = this.email.charCodeAt(i);
+ if (char > 128) {
+ sum += char;
+ cnt++;
+ }
+ }
+
+ let nonAscii = sum / cnt || 0;
+ cal.LOG("[calExtract] Average non-ascii charcode: " + nonAscii);
+ return nonAscii;
+ },
+
+ setLanguage: function() {
+ let path;
+
+ if (this.fixedLang == true) {
+ if (this.checkBundle(this.fallbackLocale)) {
+ cal.LOG("[calExtract] Fixed locale was used to choose " +
+ this.fallbackLocale + " patterns.");
+ } else {
+ cal.LOG("[calExtract] " + this.fallbackLocale +
+ " patterns were not found. Using en-US instead");
+ this.fallbackLocale = "en-US";
+ }
+
+ path = this.bundleUrl.replace(/LOCALE/g, this.fallbackLocale);
+
+ let pref = "calendar.patterns.last.used.languages";
+ let lastUsedLangs = Preferences.get(pref, "");
+ if (lastUsedLangs == "") {
+ Preferences.set(pref, this.fallbackLocale);
+ } else {
+ let langs = lastUsedLangs.split(",");
+ let idx = langs.indexOf(this.fallbackLocale);
+ if (idx == -1) {
+ Preferences.set(pref, this.fallbackLocale + "," + lastUsedLangs);
+ } else {
+ langs.splice(idx, 1);
+ Preferences.set(pref, this.fallbackLocale + "," + langs.join(","));
+ }
+ }
+ } else {
+ let spellclass = "@mozilla.org/spellchecker/engine;1";
+ let mozISpellCheckingEngine = Components.interfaces.mozISpellCheckingEngine;
+ let spellchecker = Components.classes[spellclass]
+ .getService(mozISpellCheckingEngine);
+
+ let arr = {};
+ let cnt = {};
+ spellchecker.getDictionaryList(arr, cnt);
+ let dicts = arr.value;
+
+ if (dicts.length == 0) {
+ cal.LOG("[calExtract] There are no dictionaries installed and " +
+ "enabled. You might want to add some if date and time " +
+ "extraction from emails seems inaccurate.");
+ }
+
+ let patterns;
+ let words = this.email.split(/\s+/);
+ let most = 0;
+ let mostLocale;
+ for (let dict in dicts) {
+ // dictionary locale and patterns locale match
+ if (this.checkBundle(dicts[dict])) {
+ let time1 = (new Date()).getTime();
+ spellchecker.dictionary = dicts[dict];
+ let dur = (new Date()).getTime() - time1;
+ cal.LOG("[calExtract] Loading " + dicts[dict] +
+ " dictionary took " + dur + "ms");
+ patterns = dicts[dict];
+ // beginning of dictionary locale matches patterns locale
+ } else if (this.checkBundle(dicts[dict].substring(0, 2))) {
+ let time1 = (new Date()).getTime();
+ spellchecker.dictionary = dicts[dict];
+ let dur = (new Date()).getTime() - time1;
+ cal.LOG("[calExtract] Loading " + dicts[dict] +
+ " dictionary took " + dur + "ms");
+ patterns = dicts[dict].substring(0, 2);
+ // dictionary for which patterns aren't present
+ } else {
+ cal.LOG("[calExtract] Dictionary present, rules missing: " + dicts[dict]);
+ continue;
+ }
+
+ let correct = 0;
+ let total = 0;
+ for (let word in words) {
+ words[word] = words[word].replace(/[()\d,;:?!#\.]/g, "");
+ if (words[word].length >= 2) {
+ total++;
+ if (spellchecker.check(words[word])) {
+ correct++;
+ }
+ }
+ }
+
+ let percentage = correct / total * 100.0;
+ cal.LOG("[calExtract] " + dicts[dict] + " dictionary matches " +
+ percentage + "% of words");
+
+ if (percentage > 50.0 && percentage > most) {
+ mostLocale = patterns;
+ most = percentage;
+ }
+ }
+
+ let avgCharCode = this.avgNonAsciiCharCode();
+
+ // using dictionaries for language recognition with non-latin letters doesn't work
+ // very well, possibly because of bug 471799
+ if (avgCharCode > 48000 && avgCharCode < 50000) {
+ cal.LOG("[calExtract] Using ko patterns based on charcodes");
+ path = this.bundleUrl.replace(/LOCALE/g, "ko");
+ // is it possible to differentiate zh-TW and zh-CN?
+ } else if (avgCharCode > 24000 && avgCharCode < 32000) {
+ cal.LOG("[calExtract] Using zh-TW patterns based on charcodes");
+ path = this.bundleUrl.replace(/LOCALE/g, "zh-TW");
+ } else if (avgCharCode > 14000 && avgCharCode < 24000) {
+ cal.LOG("[calExtract] Using ja patterns based on charcodes");
+ path = this.bundleUrl.replace(/LOCALE/g, "ja");
+ // Bulgarian also looks like that
+ } else if (avgCharCode > 1000 && avgCharCode < 1200) {
+ cal.LOG("[calExtract] Using ru patterns based on charcodes");
+ path = this.bundleUrl.replace(/LOCALE/g, "ru");
+ // dictionary based
+ } else if (most > 0) {
+ cal.LOG("[calExtract] Using " + mostLocale + " patterns based on dictionary");
+ path = this.bundleUrl.replace(/LOCALE/g, mostLocale);
+ // fallbackLocale matches patterns exactly
+ } else if (this.checkBundle(this.fallbackLocale)) {
+ cal.LOG("[calExtract] Falling back to " + this.fallbackLocale);
+ path = this.bundleUrl.replace(/LOCALE/g, this.fallbackLocale);
+ // beginning of fallbackLocale matches patterns
+ } else if (this.checkBundle(this.fallbackLocale.substring(0, 2))) {
+ this.fallbackLocale = this.fallbackLocale.substring(0, 2);
+ cal.LOG("[calExtract] Falling back to " + this.fallbackLocale);
+ path = this.bundleUrl.replace(/LOCALE/g, this.fallbackLocale);
+ } else {
+ cal.LOG("[calExtract] Using en-US");
+ path = this.bundleUrl.replace(/LOCALE/g, "en-US");
+ }
+ }
+ this.bundle = Services.strings.createBundle(path);
+ },
+
+ /**
+ * Extracts dates, times and durations from email
+ *
+ * @param body email body
+ * @param now reference time against which relative times are interpreted,
+ * when null current time is used
+ * @param sel selection object of email content, when defined times
+ * outside selection are disgarded
+ * @param title email title
+ * @return sorted list of extracted datetime objects
+ */
+ extract: function(title, body, now, sel) {
+ let initial = {};
+ this.collected = [];
+ this.email = title + "\r\n" + body;
+ if (now != null) {
+ this.now = now;
+ }
+
+ initial.year = now.getFullYear();
+ initial.month = now.getMonth() + 1;
+ initial.day = now.getDate();
+ initial.hour = now.getHours();
+ initial.minute = now.getMinutes();
+
+ this.collected.push({
+ year: initial.year,
+ month: initial.month,
+ day: initial.day,
+ hour: initial.hour,
+ minute: initial.minute,
+ relation: "start"
+ });
+
+ this.cleanup();
+ cal.LOG("[calExtract] Email after processing for extraction: \n" + this.email);
+
+ this.overrides = JSON.parse(Preferences.get("calendar.patterns.override", "{}"));
+ this.setLanguage();
+
+ for (let i = 0; i <= 31; i++) {
+ this.numbers[i] = this.getPatterns("number." + i);
+ }
+ this.dailyNumbers = this.numbers.join(this.marker);
+
+ this.hourlyNumbers = this.numbers[0] + this.marker;
+ for (let i = 1; i <= 22; i++) {
+ this.hourlyNumbers += this.numbers[i] + this.marker;
+ }
+ this.hourlyNumbers += this.numbers[23];
+
+ this.hourlyNumbers = this.hourlyNumbers.replace(/\|/g, this.marker);
+ this.dailyNumbers = this.dailyNumbers.replace(/\|/g, this.marker);
+
+ for (let i = 0; i < 12; i++) {
+ this.months[i] = this.getPatterns("month." + (i + 1));
+ }
+ this.allMonths = this.months.join(this.marker).replace(/\|/g, this.marker);
+
+ // time
+ this.extractTime("from.noon", "start", 12, 0);
+ this.extractTime("until.noon", "end", 12, 0);
+
+ this.extractHour("from.hour", "start", "none");
+ this.extractHour("from.hour.am", "start", "ante");
+ this.extractHour("from.hour.pm", "start", "post");
+ this.extractHour("until.hour", "end", "none");
+ this.extractHour("until.hour.am", "end", "ante");
+ this.extractHour("until.hour.pm", "end", "post");
+
+ this.extractHalfHour("from.half.hour.before", "start", "ante");
+ this.extractHalfHour("until.half.hour.before", "end", "ante");
+ this.extractHalfHour("from.half.hour.after", "start", "post");
+ this.extractHalfHour("until.half.hour.after", "end", "post");
+
+ this.extractHourMinutes("from.hour.minutes", "start", "none");
+ this.extractHourMinutes("from.hour.minutes.am", "start", "ante");
+ this.extractHourMinutes("from.hour.minutes.pm", "start", "post");
+ this.extractHourMinutes("until.hour.minutes", "end", "none");
+ this.extractHourMinutes("until.hour.minutes.am", "end", "ante");
+ this.extractHourMinutes("until.hour.minutes.pm", "end", "post");
+
+ // date
+ this.extractRelativeDay("from.today", "start", 0);
+ this.extractRelativeDay("from.tomorrow", "start", 1);
+ this.extractRelativeDay("until.tomorrow", "end", 1);
+ this.extractWeekDay("from.weekday.", "start");
+ this.extractWeekDay("until.weekday.", "end");
+ this.extractDate("from.ordinal.date", "start");
+ this.extractDate("until.ordinal.date", "end");
+
+ this.extractDayMonth("from.month.day", "start");
+ this.extractDayMonthYear("from.year.month.day", "start");
+ this.extractDayMonth("until.month.day", "end");
+ this.extractDayMonthYear("until.year.month.day", "end");
+ this.extractDayMonthName("from.monthname.day", "start");
+ this.extractDayMonthNameYear("from.year.monthname.day", "start");
+ this.extractDayMonthName("until.monthname.day", "end");
+ this.extractDayMonthNameYear("until.year.monthname.day", "end");
+
+ // duration
+ this.extractDuration("duration.minutes", 1);
+ this.extractDuration("duration.hours", 60);
+ this.extractDuration("duration.days", 60 * 24);
+
+ if (sel !== undefined && sel !== null) {
+ this.markSelected(sel, title);
+ }
+ this.markContained();
+ this.collected = this.collected.sort(this.sort);
+
+ return this.collected;
+ },
+
+ extractDayMonthYear: function(pattern, relation) {
+ let alts = this.getRepPatterns(pattern, ["(\\d{1,2})", "(\\d{1,2})",
+ "(\\d{2,4})"]);
+ let res;
+ for (let alt in alts) {
+ let positions = alts[alt].positions;
+ let re = new RegExp(alts[alt].pattern, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let day = parseInt(res[positions[1]], 10);
+ let month = parseInt(res[positions[2]], 10);
+ let year = parseInt(this.normalizeYear(res[positions[3]]), 10);
+
+ if (this.isValidDay(day) && this.isValidMonth(month) &&
+ this.isValidYear(year)) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(year, month, day, null, null,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ }
+ }
+ }
+ }
+ },
+
+ extractDayMonthNameYear: function(pattern, relation) {
+ let alts = this.getRepPatterns(pattern, ["(\\d{1,2})",
+ "(" + this.allMonths + ")",
+ "(\\d{2,4})"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let positions = alts[alt].positions;
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let day = parseInt(res[positions[1]], 10);
+ let month = res[positions[2]];
+ let year = parseInt(this.normalizeYear(res[positions[3]]), 10);
+
+ if (this.isValidDay(day)) {
+ for (let i = 0; i < 12; i++) {
+ if (this.months[i].split("|").includes(month.toLowerCase())) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(year, i + 1, day, null, null,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ extractRelativeDay: function(pattern, relation, offset) {
+ let re = new RegExp(this.getPatterns(pattern), "ig");
+ let res;
+ if ((res = re.exec(this.email)) != null) {
+ if (!this.limitChars(res, this.email)) {
+ let item = new Date(this.now.getTime() + 60 * 60 * 24 * 1000 * offset);
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(item.getFullYear(), item.getMonth() + 1, item.getDate(),
+ null, null,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ }
+ }
+ },
+
+ extractDayMonthName: function(pattern, relation) {
+ let alts = this.getRepPatterns(pattern,
+ ["(\\d{1,2}" + this.marker + this.dailyNumbers + ")",
+ "(" + this.allMonths + ")"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let positions = alts[alt].positions;
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let day = this.parseNumber(res[positions[1]], this.numbers);
+ let month = res[positions[2]];
+
+ if (this.isValidDay(day)) {
+ for (let i = 0; i < 12; i++) {
+ let months = this.unescape(this.months[i]).split("|");
+ if (months.includes(month.toLowerCase())) {
+ let date = { year: this.now.getFullYear(), month: i + 1, day: day };
+ if (this.isPastDate(date, this.now)) {
+ // find next such date
+ let item = new Date(this.now.getTime());
+ while (true) {
+ item.setDate(item.getDate() + 1);
+ if (item.getMonth() == date.month - 1 &&
+ item.getDate() == date.day) {
+ date.year = item.getFullYear();
+ break;
+ }
+ }
+ }
+
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(date.year, date.month, date.day, null, null,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ extractDayMonth: function(pattern, relation) {
+ let alts = this.getRepPatterns(pattern, ["(\\d{1,2})", "(\\d{1,2})"]);
+ let res;
+ for (let alt in alts) {
+ let re = new RegExp(alts[alt].pattern, "ig");
+ let positions = alts[alt].positions;
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let day = parseInt(res[positions[1]], 10);
+ let month = parseInt(res[positions[2]], 10);
+
+ if (this.isValidMonth(month) && this.isValidDay(day)) {
+ let date = { year: this.now.getFullYear(), month: month, day: day };
+
+ if (this.isPastDate(date, this.now)) {
+ // find next such date
+ let item = new Date(this.now.getTime());
+ while (true) {
+ item.setDate(item.getDate() + 1);
+ if (item.getMonth() == date.month - 1 &&
+ item.getDate() == date.day) {
+ date.year = item.getFullYear();
+ break;
+ }
+ }
+ }
+
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(date.year, date.month, date.day, null, null,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ }
+ }
+ }
+ }
+ },
+
+ extractDate: function(pattern, relation) {
+ let alts = this.getRepPatterns(pattern,
+ ["(\\d{1,2}" + this.marker + this.dailyNumbers + ")"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let day = this.parseNumber(res[1], this.numbers);
+ if (this.isValidDay(day)) {
+ let item = new Date(this.now.getTime());
+ if (this.now.getDate() != day) {
+ // find next nth date
+ while (true) {
+ item.setDate(item.getDate() + 1);
+ if (item.getDate() == day) {
+ break;
+ }
+ }
+ }
+
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(item.getFullYear(), item.getMonth() + 1, day,
+ null, null,
+ rev.start, rev.end,
+ rev.pattern, rev.relation, pattern, true);
+ }
+ }
+ }
+ }
+ },
+
+ extractWeekDay: function(pattern, relation) {
+ let days = [];
+ for (let i = 0; i < 7; i++) {
+ days[i] = this.getPatterns(pattern + i);
+ let re = new RegExp(days[i], "ig");
+ let res = re.exec(this.email);
+ if (res) {
+ if (!this.limitChars(res, this.email)) {
+ let date = new Date();
+ date.setDate(this.now.getDate());
+ date.setMonth(this.now.getMonth());
+ date.setYear(this.now.getFullYear());
+
+ let diff = (i - date.getDay() + 7) % 7;
+ date.setDate(date.getDate() + diff);
+
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(date.getFullYear(), date.getMonth() + 1, date.getDate(),
+ null, null,
+ rev.start, rev.end,
+ rev.pattern, rev.relation, pattern + i, true);
+ }
+ }
+ }
+ },
+
+ extractHour: function(pattern, relation, meridiem) {
+ let alts = this.getRepPatterns(pattern,
+ ["(\\d{1,2}" + this.marker + this.hourlyNumbers + ")"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let hour = this.parseNumber(res[1], this.numbers);
+
+ if (meridiem == "ante" && hour == 12) {
+ hour = hour - 12;
+ } else if (meridiem == "post" && hour != 12) {
+ hour = hour + 12;
+ } else {
+ hour = this.normalizeHour(hour);
+ }
+
+ if (this.isValidHour(res[1])) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(null, null, null, hour, 0,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern, true);
+ }
+ }
+ }
+ }
+ },
+
+ extractHalfHour: function(pattern, relation, direction) {
+ let alts = this.getRepPatterns(pattern,
+ ["(\\d{1,2}" + this.marker + this.hourlyNumbers + ")"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let hour = this.parseNumber(res[1], this.numbers);
+
+ hour = this.normalizeHour(hour);
+ if (direction == "ante") {
+ if (hour == 1) {
+ hour = 12;
+ } else {
+ hour = hour - 1;
+ }
+ }
+
+ if (this.isValidHour(hour)) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(null, null, null, hour, 30,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern, true);
+ }
+ }
+ }
+ }
+ },
+
+ extractHourMinutes: function(pattern, relation, meridiem) {
+ let alts = this.getRepPatterns(pattern, ["(\\d{1,2})", "(\\d{2})"]);
+ let res;
+ for (let alt in alts) {
+ let positions = alts[alt].positions;
+ let re = new RegExp(alts[alt].pattern, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let hour = parseInt(res[positions[1]], 10);
+ let minute = parseInt(res[positions[2]], 10);
+
+ if (meridiem == "ante" && hour == 12) {
+ hour = hour - 12;
+ } else if (meridiem == "post" && hour != 12) {
+ hour = hour + 12;
+ } else {
+ hour = this.normalizeHour(hour);
+ }
+
+ if (this.isValidHour(hour) && this.isValidMinute(hour)) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(null, null, null, hour, minute,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ }
+ }
+ }
+ }
+ },
+
+ extractTime: function(pattern, relation, hour, minute) {
+ let re = new RegExp(this.getPatterns(pattern), "ig");
+ let res;
+ if ((res = re.exec(this.email)) != null) {
+ if (!this.limitChars(res, this.email)) {
+ let rev = this.prefixSuffixStartEnd(res, relation, this.email);
+ this.guess(null, null, null, hour, minute,
+ rev.start, rev.end, rev.pattern, rev.relation, pattern);
+ }
+ }
+ },
+
+ extractDuration: function(pattern, unit) {
+ let alts = this.getRepPatterns(pattern,
+ ["(\\d{1,2}" + this.marker + this.dailyNumbers + ")"]);
+ let res;
+ for (let alt in alts) {
+ let exp = alts[alt].pattern.split(this.marker).join("|");
+ let re = new RegExp(exp, "ig");
+
+ while ((res = re.exec(this.email)) != null) {
+ if (!this.limitNums(res, this.email) && !this.limitChars(res, this.email)) {
+ let length = this.parseNumber(res[1], this.numbers);
+ let guess = {};
+ let rev = this.prefixSuffixStartEnd(res, "duration", this.email);
+ guess.duration = length * unit;
+ guess.start = rev.start;
+ guess.end = rev.end;
+ guess.str = rev.pattern;
+ guess.relation = rev.relation;
+ guess.pattern = pattern;
+ this.collected.push(guess);
+ }
+ }
+ }
+ },
+
+ markContained: function() {
+ for (let outer = 0; outer < this.collected.length; outer++) {
+ for (let inner = 0; inner < this.collected.length; inner++) {
+ // included but not exactly the same
+ if (outer != inner &&
+ this.collected[outer].start && this.collected[outer].end &&
+ this.collected[inner].start && this.collected[inner].end &&
+ this.collected[inner].start >= this.collected[outer].start &&
+ this.collected[inner].end <= this.collected[outer].end &&
+ !(this.collected[inner].start == this.collected[outer].start &&
+ this.collected[inner].end == this.collected[outer].end)) {
+ cal.LOG("[calExtract] " + this.collected[outer].str + " found as well, disgarding " + this.collected[inner].str);
+ this.collected[inner].relation = "notadatetime";
+ }
+ }
+ }
+ },
+
+ markSelected: function(sel, title) {
+ if (sel.rangeCount > 0) {
+ // mark the ones to not use
+ for (let i = 0; i < sel.rangeCount; i++) {
+ cal.LOG("[calExtract] Selection " + i + " is " + sel);
+ for (let j = 0; j < this.collected.length; j++) {
+ let selection = sel.getRangeAt(i).toString();
+
+ if (!selection.includes(this.collected[j].str) &&
+ !title.includes(this.collected[j].str) &&
+ this.collected[j].start != null) { // always keep email date, needed for tasks
+ cal.LOG("[calExtract] Marking " + JSON.stringify(this.collected[j]) + " as notadatetime");
+ this.collected[j].relation = "notadatetime";
+ }
+ }
+ }
+ }
+ },
+
+ sort: function(one, two) {
+ let rc;
+ // sort the guess from email date as the last one
+ if (one.start == null && two.start != null) {
+ return 1;
+ } else if (one.start != null && two.start == null) {
+ return -1;
+ } else if (one.start == null && two.start == null) {
+ return 0;
+ // sort dates before times
+ } else if (one.year != null && two.year == null) {
+ return -1;
+ } else if (one.year == null && two.year != null) {
+ return 1;
+ } else if (one.year != null && two.year != null) {
+ rc = (one.year > two.year) - (one.year < two.year);
+ if (rc == 0) {
+ rc = (one.month > two.month) - (one.month < two.month);
+ if (rc == 0) {
+ rc = (one.day > two.day) - (one.day < two.day);
+ }
+ }
+ return rc;
+ } else {
+ rc = (one.hour > two.hour) - (one.hour < two.hour);
+ if (rc == 0) {
+ rc = (one.minute > two.minute) - (one.minute < two.minute);
+ }
+ return rc;
+ }
+ },
+
+ /**
+ * Guesses start time from list of guessed datetimes
+ *
+ * @param isTask whether start time should be guessed for task or event
+ * @return datetime object for start time
+ */
+ guessStart: function(isTask) {
+ let startTimes = this.collected.filter(val => val.relation == "start");
+ if (startTimes.length == 0) {
+ return {};
+ }
+
+ for (let val in startTimes) {
+ cal.LOG("[calExtract] Start: " + JSON.stringify(startTimes[val]));
+ }
+
+ let guess = {};
+ let wDayInit = startTimes.filter(val => val.day != null && val.start === undefined);
+
+ // with tasks we don't try to guess start but assume email date
+ if (isTask) {
+ guess.year = wDayInit[0].year;
+ guess.month = wDayInit[0].month;
+ guess.day = wDayInit[0].day;
+ guess.hour = wDayInit[0].hour;
+ guess.minute = wDayInit[0].minute;
+ return guess;
+ }
+
+ let wDay = startTimes.filter(val => val.day != null && val.start !== undefined);
+ let wDayNA = wDay.filter(val => val.ambiguous === undefined);
+
+ let wMinute = startTimes.filter(val => val.minute != null && val.start !== undefined);
+ let wMinuteNA = wMinute.filter(val => val.ambiguous === undefined);
+
+ if (wMinuteNA.length != 0) {
+ guess.hour = wMinuteNA[0].hour;
+ guess.minute = wMinuteNA[0].minute;
+ } else if (wMinute.length != 0) {
+ guess.hour = wMinute[0].hour;
+ guess.minute = wMinute[0].minute;
+ }
+
+ // first use unambiguous guesses
+ if (wDayNA.length != 0) {
+ guess.year = wDayNA[0].year;
+ guess.month = wDayNA[0].month;
+ guess.day = wDayNA[0].day;
+ // then also ambiguous ones
+ } else if (wDay.length != 0) {
+ guess.year = wDay[0].year;
+ guess.month = wDay[0].month;
+ guess.day = wDay[0].day;
+ // next possible day considering time
+ } else if (guess.hour != null &&
+ (wDayInit[0].hour > guess.hour ||
+ (wDayInit[0].hour == guess.hour &&
+ wDayInit[0].minute > guess.minute))) {
+ let nextDay = new Date(wDayInit[0].year, wDayInit[0].month - 1, wDayInit[0].day);
+ nextDay.setTime(nextDay.getTime() + 60 * 60 * 24 * 1000);
+ guess.year = nextDay.getFullYear();
+ guess.month = nextDay.getMonth() + 1;
+ guess.day = nextDay.getDate();
+ // and finally when nothing was found then use initial guess from send time
+ } else {
+ guess.year = wDayInit[0].year;
+ guess.month = wDayInit[0].month;
+ guess.day = wDayInit[0].day;
+ }
+
+ cal.LOG("[calExtract] Start picked: " + JSON.stringify(guess));
+ return guess;
+ },
+
+ /**
+ * Guesses end time from list of guessed datetimes relative to start time
+ *
+ * @param start start time to consider when guessing
+ * @param isTask whether start time should be guessed for task or event
+ * @return datetime object for end time
+ */
+ guessEnd: function(start, isTask) {
+ let guess = {};
+ let endTimes = this.collected.filter(val => val.relation == "end");
+ let durations = this.collected.filter(val => val.relation == "duration");
+ if (endTimes.length == 0 && durations.length == 0) {
+ return {};
+ } else {
+ for (let val in endTimes) {
+ cal.LOG("[calExtract] End: " + JSON.stringify(endTimes[val]));
+ }
+
+ let wDay = endTimes.filter(val => val.day != null);
+ let wDayNA = wDay.filter(val => val.ambiguous === undefined);
+ let wMinute = endTimes.filter(val => val.minute != null);
+ let wMinuteNA = wMinute.filter(val => val.ambiguous === undefined);
+
+ // first set non-ambiguous dates
+ let pos = isTask == true ? 0 : wDayNA.length - 1;
+ if (wDayNA.length != 0) {
+ guess.year = wDayNA[pos].year;
+ guess.month = wDayNA[pos].month;
+ guess.day = wDayNA[pos].day;
+ // then ambiguous dates
+ } else if (wDay.length != 0) {
+ pos = isTask == true ? 0 : wDay.length - 1;
+ guess.year = wDay[pos].year;
+ guess.month = wDay[pos].month;
+ guess.day = wDay[pos].day;
+ }
+
+ // then non-ambiguous times
+ if (wMinuteNA.length != 0) {
+ pos = isTask == true ? 0 : wMinuteNA.length - 1;
+ guess.hour = wMinuteNA[pos].hour;
+ guess.minute = wMinuteNA[pos].minute;
+ if (guess.day == null || guess.day == start.day) {
+ if (wMinuteNA[pos].hour < start.hour ||
+ (wMinuteNA[pos].hour == start.hour &&
+ wMinuteNA[pos].minute < start.minute)) {
+ let nextDay = new Date(start.year, start.month - 1, start.day);
+ nextDay.setTime(nextDay.getTime() + 60 * 60 * 24 * 1000);
+ guess.year = nextDay.getFullYear();
+ guess.month = nextDay.getMonth() + 1;
+ guess.day = nextDay.getDate();
+ }
+ }
+ // and ambiguous times
+ } else if (wMinute.length != 0) {
+ pos = isTask == true ? 0 : wMinute.length - 1;
+ guess.hour = wMinute[pos].hour;
+ guess.minute = wMinute[pos].minute;
+ if (guess.day == null || guess.day == start.day) {
+ if (wMinute[pos].hour < start.hour ||
+ (wMinute[pos].hour == start.hour &&
+ wMinute[pos].minute < start.minute)) {
+ let nextDay = new Date(start.year, start.month - 1, start.day);
+ nextDay.setTime(nextDay.getTime() + 60 * 60 * 24 * 1000);
+ guess.year = nextDay.getFullYear();
+ guess.month = nextDay.getMonth() + 1;
+ guess.day = nextDay.getDate();
+ }
+ }
+ }
+
+ // fill in date when time was guessed
+ if (guess.minute != null && guess.day == null) {
+ guess.year = start.year;
+ guess.month = start.month;
+ guess.day = start.day;
+ }
+
+ // fill in end from total duration
+ if (guess.day == null && guess.hour == null) {
+ let duration = 0;
+
+ for (let val in durations) {
+ duration += durations[val].duration;
+ cal.LOG("[calExtract] Dur: " + JSON.stringify(durations[val]));
+ }
+
+ if (duration != 0) {
+ let startDate = new Date(start.year, start.month - 1, start.day);
+ if ("hour" in start) {
+ startDate.setHours(start.hour);
+ startDate.setMinutes(start.minute);
+ } else {
+ startDate.setHours(0);
+ startDate.setMinutes(0);
+ }
+
+ let endTime = new Date(startDate.getTime() + duration * 60 * 1000);
+ guess.year = endTime.getFullYear();
+ guess.month = endTime.getMonth() + 1;
+ guess.day = endTime.getDate();
+ if (!(endTime.getHours() == 0 && endTime.getMinutes() == 0)) {
+ guess.hour = endTime.getHours();
+ guess.minute = endTime.getMinutes();
+ }
+ }
+ }
+
+ // no zero or negative length events/tasks
+ let startTime = new Date(start.year || 0, start.month - 1 || 0, start.day || 0,
+ start.hour || 0, start.minute || 0).getTime();
+ let guessTime = new Date(guess.year || 0, guess.month - 1 || 0, guess.day || 0,
+ guess.hour || 0, guess.minute || 0).getTime();
+ if (guessTime <= startTime) {
+ guess.year = null;
+ guess.month = null;
+ guess.day = null;
+ guess.hour = null;
+ guess.minute = null;
+ }
+
+ if (guess.year != null && guess.minute == null && isTask) {
+ guess.hour = 0;
+ guess.minute = 0;
+ }
+
+ cal.LOG("[calExtract] End picked: " + JSON.stringify(guess));
+ return guess;
+ }
+ },
+
+ getPatterns: function(name) {
+ let value;
+ try {
+ value = this.bundle.GetStringFromName(name);
+ if (value.trim() == "") {
+ cal.LOG("[calExtract] Pattern not found: " + name);
+ return this.defPattern;
+ }
+
+ let vals = this.cleanPatterns(value).split("|");
+ for (let idx = vals.length - 1; idx >= 0; idx--) {
+ if (vals[idx].trim() == "") {
+ vals.splice(idx, 1);
+ Components.utils.reportError("[calExtract] Faulty extraction pattern " +
+ value + " for " + name);
+ }
+ }
+
+ if (this.overrides[name] !== undefined &&
+ this.overrides[name].add !== undefined) {
+ let additions = this.overrides[name].add;
+ additions = this.cleanPatterns(additions).split("|");
+ for (let pattern in additions) {
+ vals.push(additions[pattern]);
+ cal.LOG("[calExtract] Added " + additions[pattern] + " to " + name);
+ }
+ }
+
+ if (this.overrides[name] !== undefined &&
+ this.overrides[name].remove !== undefined) {
+ let removals = this.overrides[name].remove;
+ removals = this.cleanPatterns(removals).split("|");
+ for (let pattern in removals) {
+ let idx = vals.indexOf(removals[pattern]);
+ if (idx != -1) {
+ vals.splice(idx, 1);
+ cal.LOG("[calExtract] Removed " + removals[pattern] + " from " + name);
+ }
+ }
+ }
+
+ vals.sort((a, b) => b.length - a.length);
+ return vals.join("|");
+ } catch (ex) {
+ cal.LOG("[calExtract] Pattern not found: " + name);
+
+ // fake a value to avoid empty regexes creating endless loops
+ return this.defPattern;
+ }
+ },
+
+ getRepPatterns: function(name, replaceables) {
+ let alts = [];
+ let patterns = [];
+
+ try {
+ let value = this.bundle.GetStringFromName(name);
+ if (value.trim() == "") {
+ cal.LOG("[calExtract] Pattern empty: " + name);
+ return alts;
+ }
+
+ let vals = this.cleanPatterns(value).split("|");
+ for (let idx = vals.length - 1; idx >= 0; idx--) {
+ if (vals[idx].trim() == "") {
+ vals.splice(idx, 1);
+ Components.utils.reportError("[calExtract] Faulty extraction pattern " +
+ value + " for " + name);
+ }
+ }
+
+ if (this.overrides[name] !== undefined &&
+ this.overrides[name].add !== undefined) {
+ let additions = this.overrides[name].add;
+ additions = this.cleanPatterns(additions).split("|");
+ for (let pattern in additions) {
+ vals.push(additions[pattern]);
+ cal.LOG("[calExtract] Added " + additions[pattern] + " to " + name);
+ }
+ }
+
+ if (this.overrides[name] !== undefined &&
+ this.overrides[name].remove !== undefined) {
+ let removals = this.overrides[name].remove;
+ removals = this.cleanPatterns(removals).split("|");
+ for (let pattern in removals) {
+ let idx = vals.indexOf(removals[pattern]);
+ if (idx != -1) {
+ vals.splice(idx, 1);
+ cal.LOG("[calExtract] Removed " + removals[pattern] + " from " + name);
+ }
+ }
+ }
+
+ vals.sort((a, b) => b.length - a.length);
+ for (let val in vals) {
+ let pattern = vals[val];
+ for (let cnt = 1; cnt <= replaceables.length; cnt++) {
+ pattern = pattern.split("#" + cnt).join(replaceables[cnt - 1]);
+ }
+ patterns.push(pattern);
+ }
+
+ for (let val in vals) {
+ let positions = [];
+ if (replaceables.length == 1) {
+ positions[1] = 1;
+ } else {
+ positions = this.getPositionsFor(vals[val], name, replaceables.length);
+ }
+ alts[val] = { pattern: patterns[val], positions: positions };
+ }
+ } catch (ex) {
+ cal.LOG("[calExtract] Pattern not found: " + name);
+ }
+ return alts;
+ },
+
+ getPositionsFor: function(str, name, count) {
+ let positions = [];
+ let re = /#(\d)/g;
+ let match;
+ let i = 0;
+ while ((match = re.exec(str))) {
+ i++;
+ positions[parseInt(match[1], 10)] = i;
+ }
+
+ // correctness checking
+ for (i = 1; i <= count; i++) {
+ if (positions[i] === undefined) {
+ Components.utils.reportError("[calExtract] Faulty extraction pattern " + name +
+ ", missing parameter #" + i);
+ }
+ }
+ return positions;
+ },
+
+ cleanPatterns: function(pattern) {
+ // remove whitespace around | if present
+ let value = pattern.replace(/\s*\|\s*/g, "|");
+ // allow matching for patterns with missing or excessive whitespace
+ return this.sanitize(value).replace(/\s+/g, "\\s*");
+ },
+
+ isValidYear: function(year) {
+ return (year >= 2000 && year <= 2050);
+ },
+
+ isValidMonth: function(month) {
+ return (month >= 1 && month <= 12);
+ },
+
+ isValidDay: function(day) {
+ return (day >= 1 && day <= 31);
+ },
+
+ isValidHour: function(hour) {
+ return (hour >= 0 && hour <= 23);
+ },
+
+ isValidMinute: function(minute) {
+ return (minute >= 0 && minute <= 59);
+ },
+
+ isPastDate: function(date, referenceDate) {
+ // avoid changing original refDate
+ let refDate = new Date(referenceDate.getTime());
+ refDate.setHours(0);
+ refDate.setMinutes(0);
+ refDate.setSeconds(0);
+ refDate.setMilliseconds(0);
+ let jsDate;
+ if (date.day != null) {
+ jsDate = new Date(date.year, date.month - 1, date.day);
+ }
+ return jsDate < refDate;
+ },
+
+ normalizeHour: function(hour) {
+ if (hour < this.dayStart && hour <= 11) {
+ return hour + 12;
+ }
+ return hour;
+ },
+
+ normalizeYear: function(year) {
+ return (year.length == 2) ? "20" + year : year;
+ },
+
+ limitNums: function(res, email) {
+ let pattern = email.substring(res.index, res.index + res[0].length);
+ let before = email.charAt(res.index - 1);
+ let after = email.charAt(res.index + res[0].length);
+ let result = (/\d/.exec(before) && /\d/.exec(pattern.charAt(0))) ||
+ (/\d/.exec(pattern.charAt(pattern.length - 1)) && /\d/.exec(after));
+ return result != null;
+ },
+
+ limitChars: function(res, email) {
+ let alphabet = this.getPatterns("alphabet");
+ // for languages without regular alphabet surrounding characters are ignored
+ if (alphabet == this.defPattern) {
+ return false;
+ }
+
+ let pattern = email.substring(res.index, res.index + res[0].length);
+ let before = email.charAt(res.index - 1);
+ let after = email.charAt(res.index + res[0].length);
+
+ let re = new RegExp("[" + alphabet + "]");
+ let result = (re.exec(before) && re.exec(pattern.charAt(0))) ||
+ (re.exec(pattern.charAt(pattern.length - 1)) && re.exec(after));
+ return result != null;
+ },
+
+ prefixSuffixStartEnd: function(res, relation, email) {
+ let pattern = email.substring(res.index, res.index + res[0].length);
+ let prev = email.substring(0, res.index);
+ let next = email.substring(res.index + res[0].length);
+ let prefixSuffix = {
+ start: res.index,
+ end: res.index + res[0].length,
+ pattern: pattern,
+ relation: relation
+ };
+ let char = "\\s*";
+ let psres;
+
+ let re = new RegExp("(" + this.getPatterns("end.prefix") + ")" + char + "$", "ig");
+ if ((psres = re.exec(prev)) != null) {
+ prefixSuffix.relation = "end";
+ prefixSuffix.start = psres.index;
+ prefixSuffix.pattern = psres[0] + pattern;
+ }
+
+ re = new RegExp("^" + char + "(" + this.getPatterns("end.suffix") + ")", "ig");
+ if ((psres = re.exec(next)) != null) {
+ prefixSuffix.relation = "end";
+ prefixSuffix.end = prefixSuffix.end + psres[0].length;
+ prefixSuffix.pattern = pattern + psres[0];
+ }
+
+ re = new RegExp("(" + this.getPatterns("start.prefix") + ")" + char + "$", "ig");
+ if ((psres = re.exec(prev)) != null) {
+ prefixSuffix.relation = "start";
+ prefixSuffix.start = psres.index;
+ prefixSuffix.pattern = psres[0] + pattern;
+ }
+
+ re = new RegExp("^" + char + "(" + this.getPatterns("start.suffix") + ")", "ig");
+ if ((psres = re.exec(next)) != null) {
+ prefixSuffix.relation = "start";
+ prefixSuffix.end = prefixSuffix.end + psres[0].length;
+ prefixSuffix.pattern = pattern + psres[0];
+ }
+
+ re = new RegExp("\\s(" + this.getPatterns("no.datetime.prefix") + ")" + char + "$", "ig");
+
+ if ((psres = re.exec(prev)) != null) {
+ prefixSuffix.relation = "notadatetime";
+ }
+
+ re = new RegExp("^" + char + "(" + this.getPatterns("no.datetime.suffix") + ")", "ig");
+ if ((psres = re.exec(next)) != null) {
+ prefixSuffix.relation = "notadatetime";
+ }
+
+ return prefixSuffix;
+ },
+
+ parseNumber: function(numberString, numbers) {
+ let number = parseInt(numberString, 10);
+ // number comes in as plain text, numbers are already adjusted for usage
+ // in regular expression
+ let cleanNumberString = this.cleanPatterns(numberString);
+ if (isNaN(number)) {
+ for (let i = 0; i <= 31; i++) {
+ let numberparts = numbers[i].split("|");
+ if (numberparts.includes(cleanNumberString.toLowerCase())) {
+ return i;
+ }
+ }
+ return -1;
+ } else {
+ return number;
+ }
+ },
+
+ guess: function(year, month, day, hour, minute, start, end, str,
+ relation, pattern, ambiguous) {
+ let dateGuess = {
+ year: year,
+ month: month,
+ day: day,
+ hour: hour,
+ minute: minute,
+ start: start,
+ end: end,
+ str: str,
+ relation: relation,
+ pattern: pattern,
+ ambiguous: ambiguous
+ };
+
+ // past dates are kept for containment checks
+ if (this.isPastDate(dateGuess, this.now)) {
+ dateGuess.relation = "notadatetime";
+ }
+ this.collected.push(dateGuess);
+ },
+
+ sanitize: function(str) {
+ return str.replace(/[-[\]{}()*+?.,\\^$]/g, "\\$&");
+ },
+
+ unescape: function(str) {
+ return str.replace(/\\([\.])/g, "$1");
+ }
+};
diff --git a/calendar/base/modules/calHashedArray.jsm b/calendar/base/modules/calHashedArray.jsm
new file mode 100644
index 000000000..4a53d6521
--- /dev/null
+++ b/calendar/base/modules/calHashedArray.jsm
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+var EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+
+/**
+ * An unsorted array of hashable items with some extra functions to quickly
+ * retrieve the item by its hash id.
+ *
+ * Performance Considerations:
+ * - Accessing items is fast
+ * - Adding items is fast (they are added to the end)
+ * - Deleting items is O(n)
+ * - Modifying items is fast.
+ */
+cal.HashedArray = function() {
+ this.clear();
+};
+
+cal.HashedArray.prototype = {
+ mArray: null,
+ mHash: null,
+
+ mBatch: 0,
+ mFirstDirty: -1,
+
+ /**
+ * Returns a copy of the internal array. Note this is a shallow copy.
+ */
+ get arrayCopy() {
+ return this.mArray.concat([]);
+ },
+
+ /**
+ * The function to retrieve the hashId given the item. This function can be
+ * overridden by implementations, in case the added items are not instances
+ * of calIItemBase.
+ *
+ * @param item The item to get the hashId for
+ * @return The hashId of the item
+ */
+ hashAccessor: function(item) {
+ return item.hashId;
+ },
+
+ /**
+ * Returns the item, given its index in the array
+ *
+ * @param index The index of the item to retrieve.
+ * @return The retrieved item.
+ */
+ itemByIndex: function(index) {
+ return this.mArray[index];
+ },
+
+ /**
+ * Returns the item, given its hashId
+ *
+ * @param id The hashId of the item to retrieve.
+ * @return The retrieved item.
+ */
+ itemById: function(id) {
+ if (this.mBatch > 0) {
+ throw "Accessing Array by ID not supported in batch mode ";
+ }
+ return (id in this.mHash ? this.mArray[this.mHash[id]] : null);
+ },
+
+ /**
+ * Returns the index of the given item. This function is cheap performance
+ * wise, since it uses the hash
+ *
+ * @param item The item to search for.
+ * @return The index of the item.
+ */
+ indexOf: function(item) {
+ if (this.mBatch > 0) {
+ throw "Accessing Array Indexes not supported in batch mode";
+ }
+ let hashId = this.hashAccessor(item);
+ return (hashId in this.mHash ? this.mHash[hashId] : -1);
+ },
+
+ /**
+ * Remove the item with the given hashId.
+ *
+ * @param id The id of the item to be removed
+ */
+ removeById: function(id) {
+ if (this.mBatch > 0) {
+ throw "Remvoing by ID in batch mode is not supported"; /* TODO */
+ }
+ let index = this.mHash[id];
+ delete this.mHash[id];
+ this.mArray.splice(index, 1);
+ this.reindex(index);
+ },
+
+ /**
+ * Remove the item at the given index.
+ *
+ * @param index The index of the item to remove.
+ */
+ removeByIndex: function(index) {
+ delete this.mHash[this.hashAccessor(this.mArray[index])];
+ this.mArray.splice(index, 1);
+ this.reindex(index);
+ },
+
+ /**
+ * Clear the whole array, removing all items. This also resets batch mode.
+ */
+ clear: function() {
+ this.mHash = {};
+ this.mArray = [];
+ this.mFirstDirty = -1;
+ this.mBatch = 0;
+ },
+
+ /**
+ * Add the item to the array
+ *
+ * @param item The item to add.
+ * @return The index of the added item.
+ */
+ addItem: function(item) {
+ let index = this.mArray.length;
+ this.mArray.push(item);
+ this.reindex(index);
+ return index;
+ },
+
+ /**
+ * Modifies the item in the array. If the item is already in the array, then
+ * it is replaced by the passed item. Otherwise, the item is added to the
+ * array.
+ *
+ * @param item The item to modify.
+ * @return The (new) index.
+ */
+ modifyItem: function(item) {
+ let hashId = this.hashAccessor(item);
+ if (hashId in this.mHash) {
+ let index = this.mHash[this.hashAccessor(item)];
+ this.mArray[index] = item;
+ return index;
+ } else {
+ return this.addItem(item);
+ }
+ },
+
+ /**
+ * Reindexes the items in the array. This function is mostly used
+ * internally. All parameters are inclusive. The ranges are automatically
+ * swapped if from > to.
+ *
+ * @param from (optional) The index to start indexing from. If left
+ * out, defaults to 0.
+ * @param to (optional) The index to end indexing on. If left out,
+ * defaults to the array length.
+ */
+ reindex: function(from, to) {
+ if (this.mArray.length == 0) {
+ return;
+ }
+
+ from = (from === undefined ? 0 : from);
+ to = (to === undefined ? this.mArray.length - 1 : to);
+
+ from = Math.min(this.mArray.length - 1, Math.max(0, from));
+ to = Math.min(this.mArray.length - 1, Math.max(0, to));
+
+ if (from > to) {
+ let tmp = from;
+ from = to;
+ to = tmp;
+ }
+
+ if (this.mBatch > 0) {
+ // No indexing in batch mode, but remember from where to index.
+ this.mFirstDirty = Math.min(Math.max(0, this.mFirstDirty), from);
+ return;
+ }
+
+ for (let idx = from; idx <= to; idx++) {
+ this.mHash[this.hashAccessor(this.mArray[idx])] = idx;
+ }
+ },
+
+ startBatch: function() {
+ this.mBatch++;
+ },
+
+ endBatch: function() {
+ this.mBatch = Math.max(0, this.mBatch - 1);
+
+ if (this.mBatch == 0 && this.mFirstDirty > -1) {
+ this.reindex(this.mFirstDirty);
+ this.mFirstDirty = -1;
+ }
+ },
+
+ /**
+ * Iterator to allow iterating the hashed array object.
+ */
+ [Symbol.iterator]: function* () {
+ yield* this.mArray;
+ }
+};
+
+/**
+ * Sorted hashed array. The array always stays sorted.
+ *
+ * Performance Considerations:
+ * - Accessing items is fast
+ * - Adding and deleting items is O(n)
+ * - Modifying items is fast.
+ */
+cal.SortedHashedArray = function(comparator) {
+ cal.HashedArray.apply(this, arguments);
+ if (!comparator) {
+ throw "Sorted Hashed Array needs a comparator";
+ }
+ this.mCompFunc = comparator;
+};
+
+cal.SortedHashedArray.prototype = {
+ __proto__: cal.HashedArray.prototype,
+
+ mCompFunc: null,
+
+ addItem: function(item) {
+ let newIndex = cal.binaryInsert(this.mArray, item, this.mCompFunc, false);
+ this.reindex(newIndex);
+ return newIndex;
+ },
+
+ modifyItem: function(item) {
+ let hashId = this.hashAccessor(item);
+ if (hashId in this.mHash) {
+ let cmp = this.mCompFunc(item, this.mArray[this.mHash[hashId]]);
+ if (cmp == 0) {
+ // The item will be at the same index, we just need to replace it
+ this.mArray[this.mHash[hashId]] = item;
+ return this.mHash[hashId];
+ } else {
+ let oldIndex = this.mHash[hashId];
+
+ let newIndex = cal.binaryInsert(this.mArray, item, this.mCompFunc, false);
+ this.mArray.splice(oldIndex, 1);
+ this.reindex(oldIndex, newIndex);
+ return newIndex;
+ }
+ } else {
+ return this.addItem(item);
+ }
+ }
+};
diff --git a/calendar/base/modules/calItemUtils.jsm b/calendar/base/modules/calItemUtils.jsm
new file mode 100644
index 000000000..7d825b31c
--- /dev/null
+++ b/calendar/base/modules/calItemUtils.jsm
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["itemDiff"];
+
+Components.utils.import("resource://calendar/modules/calHashedArray.jsm");
+
+/**
+ * Given two sets of items, find out which items were added, changed or
+ * removed.
+ *
+ * The general flow is to first use load/load1 methods to load the engine with
+ * the first set of items, then use difference/difference1 to load the set of
+ * items to diff against. Afterwards, call the complete method to tell the
+ * engine that no more items are coming.
+ *
+ * You can then access the mAddedItems/mModifiedItems/mDeletedItems attributes to
+ * get the items that were changed during the process.
+ */
+function itemDiff() {
+ this.reset();
+}
+
+itemDiff.prototype = {
+ STATE_INITIAL: 1,
+ STATE_LOADING: 2,
+ STATE_DIFFERING: 4,
+ STATE_COMPLETED: 8,
+
+ state: 1,
+ mInitialItems: null,
+
+ mModifiedItems: null,
+ mModifiedOldItems: null,
+ mAddedItems: null,
+ mDeletedItems: null,
+
+ /**
+ * Expect the difference engine to be in the given state.
+ *
+ * @param aState The state to be in
+ * @param aMethod The method name expecting the state
+ */
+ _expectState: function(aState, aMethod) {
+ if ((this.state & aState) == 0) {
+ throw new Error("itemDiff method " + aMethod +
+ " called while in unexpected state " + this.state);
+ }
+ },
+
+ /**
+ * Load the difference engine with one item, see load.
+ *
+ * @param item The item to load
+ */
+ load1: function(item) {
+ this.load([item]);
+ },
+
+ /**
+ * Loads an array of items. This step cannot be executed
+ * after calling the difference methods.
+ *
+ * @param items The array of items to load
+ */
+ load: function(items) {
+ this._expectState(this.STATE_INITIAL | this.STATE_LOADING, "load");
+
+ for (let item of items) {
+ this.mInitialItems[item.hashId] = item;
+ }
+
+ this.state = this.STATE_LOADING;
+ },
+
+ /**
+ * Calculates the difference for the passed item, see difference.
+ *
+ * @param item The item to calculate difference with
+ */
+ difference1: function(item) {
+ this.difference([item]);
+ },
+
+ /**
+ * Calculate the difference for the array of items. This method should be
+ * called after all load methods and before the complete method.
+ *
+ * @param items The array of items to calculate difference with
+ */
+ difference: function(items) {
+ this._expectState(this.STATE_INITIAL | this.STATE_LOADING | this.STATE_DIFFERING, "difference");
+
+ this.mModifiedOldItems.startBatch();
+ this.mModifiedItems.startBatch();
+ this.mAddedItems.startBatch();
+
+ for (let item of items) {
+ if (item.hashId in this.mInitialItems) {
+ let oldItem = this.mInitialItems[item.hashId];
+ this.mModifiedOldItems.addItem(oldItem);
+ this.mModifiedItems.addItem(item);
+ } else {
+ this.mAddedItems.addItem(item);
+ }
+ delete this.mInitialItems[item.hashId];
+ }
+
+ this.mModifiedOldItems.endBatch();
+ this.mModifiedItems.endBatch();
+ this.mAddedItems.endBatch();
+
+ this.state = this.STATE_DIFFERING;
+ },
+
+ /**
+ * Tell the engine that all load and difference calls have been made, this
+ * makes sure that all item states are correctly returned.
+ */
+ complete: function() {
+ this._expectState(this.STATE_INITIAL | this.STATE_LOADING | this.STATE_DIFFERING, "complete");
+
+ this.mDeletedItems.startBatch();
+
+ for (let hashId in this.mInitialItems) {
+ let item = this.mInitialItems[hashId];
+ this.mDeletedItems.addItem(item);
+ }
+
+ this.mDeletedItems.endBatch();
+ this.mInitialItems = {};
+
+ this.state = this.STATE_COMPLETED;
+ },
+
+ /** @return a HashedArray containing the new version of the modified items */
+ get modifiedItems() {
+ this._expectState(this.STATE_COMPLETED, "get modifiedItems");
+ return this.mModifiedItems;
+ },
+
+ /** @return a HashedArray containing the old version of the modified items */
+ get modifiedOldItems() {
+ this._expectState(this.STATE_COMPLETED, "get modifiedOldItems");
+ return this.mModifiedOldItems;
+ },
+
+ /** @return a HashedArray containing added items */
+ get addedItems() {
+ this._expectState(this.STATE_COMPLETED, "get addedItems");
+ return this.mAddedItems;
+ },
+
+ /** @return a HashedArray containing deleted items */
+ get deletedItems() {
+ this._expectState(this.STATE_COMPLETED, "get deletedItems");
+ return this.mDeletedItems;
+ },
+
+ /** @return the number of loaded items */
+ get count() {
+ return Object.keys(this.mInitialItems).length;
+ },
+
+ /**
+ * Resets the difference engine to its initial state.
+ */
+ reset: function() {
+ this.mInitialItems = {};
+ this.mModifiedItems = new cal.HashedArray();
+ this.mModifiedOldItems = new cal.HashedArray();
+ this.mAddedItems = new cal.HashedArray();
+ this.mDeletedItems = new cal.HashedArray();
+ this.state = this.STATE_INITIAL;
+ }
+};
diff --git a/calendar/base/modules/calIteratorUtils.jsm b/calendar/base/modules/calIteratorUtils.jsm
new file mode 100644
index 000000000..f036dfe20
--- /dev/null
+++ b/calendar/base/modules/calIteratorUtils.jsm
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+
+/**
+ * Iterates an array of items, i.e. the passed item including all
+ * overridden instances of a recurring series.
+ *
+ * @param items array of items
+ */
+cal.itemIterator = function* (items) {
+ for (let item of items) {
+ yield item;
+ let rec = item.recurrenceInfo;
+ if (rec) {
+ for (let exid of rec.getExceptionIds({})) {
+ yield rec.getExceptionFor(exid);
+ }
+ }
+ }
+};
+
+/**
+ * Runs the body() function once for each item in the iterator using the event
+ * queue to make sure other actions could run inbetween. When all iterations are
+ * done (and also when cal.forEach.BREAK is returned), calls the completed()
+ * function if passed.
+ *
+ * If you would like to break or continue inside the body(), return either
+ * cal.forEach.BREAK or cal.forEach.CONTINUE
+ *
+ * Note since the event queue is used, this function will return immediately,
+ * before the iteration is complete. If you need to run actions after the real
+ * for each loop, use the optional completed() function.
+ *
+ * @param iter The Iterator or the plain Object to go through in this
+ * loop.
+ * @param body The function called for each iteration. Its parameter is
+ * the single item from the iterator.
+ * @param completed [optional] The function called after the loop completes.
+ */
+cal.forEach = function(iterable, body, completed) {
+ // This should be a const one day, lets keep it a pref for now though until we
+ // find a sane value.
+ let LATENCY = Preferences.get("calendar.threading.latency", 250);
+
+ if (typeof iterable == "object" && !iterable[Symbol.iterator]) {
+ iterable = Object.entries(iterable);
+ }
+
+ let ourIter = iterable[Symbol.iterator]();
+ let currentThread = Services.tm.currentThread;
+
+ // This is our dispatcher, it will be used for the iterations
+ let dispatcher = {
+ run: function() {
+ let startTime = (new Date()).getTime();
+ while (((new Date()).getTime() - startTime) < LATENCY) {
+ let next = ourIter.next();
+ let done = next.done;
+
+ if (!done) {
+ let rc = body(next.value);
+ if (rc == cal.forEach.BREAK) {
+ done = true;
+ }
+ }
+
+ if (done) {
+ if (completed) {
+ completed();
+ }
+ return;
+ }
+ }
+
+ currentThread.dispatch(this, currentThread.DISPATCH_NORMAL);
+ }
+ };
+
+ currentThread.dispatch(dispatcher, currentThread.DISPATCH_NORMAL);
+};
+
+cal.forEach.CONTINUE = 1;
+cal.forEach.BREAK = 2;
+
+/**
+ * "ical" namespace. Used for all iterators (and possibly other functions) that
+ * are related to libical.
+ */
+cal.ical = {
+ /**
+ * Yields all subcomponents in all calendars in the passed component.
+ * - If the passed component is an XROOT (contains multiple calendars),
+ * then go through all VCALENDARs in it and get their subcomponents.
+ * - If the passed component is a VCALENDAR, iterate through its direct
+ * subcomponents.
+ * - Otherwise assume the passed component is the item itself and yield
+ * only the passed component.
+ *
+ * This iterator can only be used in a for..of block:
+ * for (let component of cal.ical.calendarComponentIterator(aComp)) { ... }
+ *
+ * @param aComponent The component to iterate given the above rules.
+ * @param aCompType The type of item to iterate.
+ * @return The iterator that yields all items.
+ */
+ calendarComponentIterator: function* (aComponent, aCompType) {
+ let compType = (aCompType || "ANY");
+ if (aComponent && aComponent.componentType == "VCALENDAR") {
+ yield* cal.ical.subcomponentIterator(aComponent, compType);
+ } else if (aComponent && aComponent.componentType == "XROOT") {
+ for (let calComp of cal.ical.subcomponentIterator(aComponent, "VCALENDAR")) {
+ yield* cal.ical.subcomponentIterator(calComp, compType);
+ }
+ } else if (aComponent && (compType == "ANY" || compType == aComponent.componentType)) {
+ yield aComponent;
+ }
+ },
+
+ /**
+ * Use to iterate through all subcomponents of a calIIcalComponent. This
+ * iterators depth is 1, this means no sub-sub-components will be iterated.
+ *
+ * This iterator can only be used in a for() block:
+ * for (let component in cal.ical.subcomponentIterator(aComp)) { ... }
+ *
+ * @param aComponent The component who's subcomponents to iterate.
+ * @param aSubcomp (optional) the specific subcomponent to
+ * enumerate. If not given, "ANY" will be used.
+ * @return An iterator object to iterate the properties.
+ */
+ subcomponentIterator: function* (aComponent, aSubcomp) {
+ let subcompName = (aSubcomp || "ANY");
+ for (let subcomp = aComponent.getFirstSubcomponent(subcompName);
+ subcomp;
+ subcomp = aComponent.getNextSubcomponent(subcompName)) {
+ yield subcomp;
+ }
+ },
+
+ /**
+ * Use to iterate through all properties of a calIIcalComponent.
+ * This iterator can only be used in a for() block:
+ * for (let property in cal.ical.propertyIterator(aComp)) { ... }
+ *
+ * @param aComponent The component to iterate.
+ * @param aProperty (optional) the specific property to enumerate.
+ * If not given, "ANY" will be used.
+ * @return An iterator object to iterate the properties.
+ */
+ propertyIterator: function* (aComponent, aProperty) {
+ let propertyName = (aProperty || "ANY");
+ for (let prop = aComponent.getFirstProperty(propertyName);
+ prop;
+ prop = aComponent.getNextProperty(propertyName)) {
+ yield prop;
+ }
+ },
+
+ /**
+ * Use to iterate through all parameters of a calIIcalProperty.
+ * This iterator behaves similar to the object iterator. Possible uses:
+ * for (let paramName in cal.ical.paramIterator(prop)) { ... }
+ * or:
+ * for (let [paramName, paramValue] of cal.ical.paramIterator(prop)) { ... }
+ *
+ * @param aProperty The property to iterate.
+ * @return An iterator object to iterate the properties.
+ */
+ paramIterator: function* (aProperty) {
+ let paramSet = new Set();
+ for (let paramName = aProperty.getFirstParameterName();
+ paramName;
+ paramName = aProperty.getNextParameterName()) {
+ // Workaround to avoid infinite loop when the property
+ // contains duplicate parameters (bug 875739 for libical)
+ if (!paramSet.has(paramName)) {
+ yield [paramName, aProperty.getParameter(paramName)];
+ paramSet.add(paramName);
+ }
+ }
+ }
+};
diff --git a/calendar/base/modules/calItipUtils.jsm b/calendar/base/modules/calItipUtils.jsm
new file mode 100644
index 000000000..04cfc14e0
--- /dev/null
+++ b/calendar/base/modules/calItipUtils.jsm
@@ -0,0 +1,1676 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Scheduling and iTIP helper code
+ */
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+cal.itip = {
+ /**
+ * Gets the sequence/revision number, either of the passed item or
+ * the last received one of an attendee; see
+ * <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.1>.
+ */
+ getSequence: function(item) {
+ let seq = null;
+
+ let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
+ if (wrappedItem) {
+ seq = wrappedItem.getProperty("RECEIVED-SEQUENCE");
+ } else if (item) {
+ // Unless the below is standardized, we store the last original
+ // REQUEST/PUBLISH SEQUENCE in X-MOZ-RECEIVED-SEQUENCE to test against it
+ // when updates come in:
+ seq = item.getProperty("X-MOZ-RECEIVED-SEQUENCE");
+ if (seq === null) {
+ seq = item.getProperty("SEQUENCE");
+ }
+
+ // Make sure we don't have a pre Outlook 2007 appointment, but if we do
+ // use Microsoft's Sequence number. I <3 MS
+ if ((seq === null) || (seq == "0")) {
+ seq = item.getProperty("X-MICROSOFT-CDO-APPT-SEQUENCE");
+ }
+ }
+
+ if (seq === null) {
+ return 0;
+ } else {
+ seq = parseInt(seq, 10);
+ return (isNaN(seq) ? 0 : seq);
+ }
+ },
+
+ /**
+ * Gets the stamp date-time, either of the passed item or
+ * the last received one of an attendee; see
+ * <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.2>.
+ */
+ getStamp: function(item) {
+ let dtstamp = null;
+
+ let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
+ if (wrappedItem) {
+ let stamp = wrappedItem.getProperty("RECEIVED-DTSTAMP");
+ if (stamp) {
+ dtstamp = cal.createDateTime(stamp);
+ }
+ } else if (item) {
+ // Unless the below is standardized, we store the last original
+ // REQUEST/PUBLISH DTSTAMP in X-MOZ-RECEIVED-DTSTAMP to test against it
+ // when updates come in:
+ let stamp = item.getProperty("X-MOZ-RECEIVED-DTSTAMP");
+ if (stamp) {
+ dtstamp = cal.createDateTime(stamp);
+ } else {
+ // xxx todo: are there similar X-MICROSOFT-CDO properties to be considered here?
+ dtstamp = item.stampTime;
+ }
+ }
+
+ return dtstamp;
+ },
+
+ /**
+ * Compares sequences and/or stamps of two items
+ *
+ * @param {calIEvent|calIToDo|calIAttendee} aItem1
+ * @param {calIEvent|calIToDo|calIAttendee} aItem2
+ * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+ */
+ compare: function(aItem1, aItem2) {
+ let comp = cal.itip.compareSequence(aItem1, aItem2);
+ if (comp == 0) {
+ comp = cal.itip.compareStamp(aItem1, aItem2);
+ }
+ return comp;
+ },
+
+ /**
+ * Compares sequences of two items
+ *
+ * @param {calIEvent|calIToDo|calIAttendee} aItem1
+ * @param {calIEvent|calIToDo|calIAttendee} aItem2
+ * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+ */
+ compareSequence: function(aItem1, aItem2) {
+ let seq1 = cal.itip.getSequence(aItem1);
+ let seq2 = cal.itip.getSequence(aItem2);
+ if (seq1 > seq2) {
+ return 1;
+ } else if (seq1 < seq2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ },
+
+ /**
+ * Compares stamp of two items
+ *
+ * @param {calIEvent|calIToDo|calIAttendee} aItem1
+ * @param {calIEvent|calIToDo|calIAttendee} aItem2
+ * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+ */
+ compareStamp: function(aItem1, aItem2) {
+ let st1 = cal.itip.getStamp(aItem1);
+ let st2 = cal.itip.getStamp(aItem2);
+ if (st1 && st2) {
+ return st1.compare(st2);
+ } else if (!st1 && st2) {
+ return -1;
+ } else if (st1 && !st2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ /**
+ * Checks if the given calendar is a scheduling calendar. This means it
+ * needs an organizer id and an itip transport. It should also be writable.
+ *
+ * @param calendar The calendar to check
+ * @return True, if its a scheduling calendar.
+ */
+ isSchedulingCalendar: function(calendar) {
+ return cal.isCalendarWritable(calendar) &&
+ calendar.getProperty("organizerId") &&
+ calendar.getProperty("itip.transport");
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Given an nsIMsgDBHdr and an imipMethod, set up the given itip item.
+ *
+ * @param itipItem The item to set up
+ * @param imipMethod The received imip method
+ * @param aMsgHdr Information about the received email
+ */
+ initItemFromMsgData: function(itipItem, imipMethod, aMsgHdr) {
+ // set the sender of the itip message
+ itipItem.sender = cal.itip.getMessageSender(aMsgHdr);
+
+ // Get the recipient identity and save it with the itip item.
+ itipItem.identity = cal.itip.getMessageRecipient(aMsgHdr);
+
+ // We are only called upon receipt of an invite, so ensure that isSend
+ // is false.
+ itipItem.isSend = false;
+
+ // XXX Get these from preferences
+ itipItem.autoResponse = Components.interfaces.calIItipItem.USER;
+
+ if (imipMethod && imipMethod.length != 0 && imipMethod.toLowerCase() != "nomethod") {
+ itipItem.receivedMethod = imipMethod.toUpperCase();
+ } else { // There is no METHOD in the content-type header (spec violation).
+ // Fall back to using the one from the itipItem's ICS.
+ imipMethod = itipItem.receivedMethod;
+ }
+ cal.LOG("iTIP method: " + imipMethod);
+
+ let isWritableCalendar = function(aCalendar) {
+ /* TODO: missing ACL check for existing items (require callback API) */
+ return cal.itip.isSchedulingCalendar(aCalendar) &&
+ cal.userCanAddItemsToCalendar(aCalendar);
+ };
+
+ let writableCalendars = cal.getCalendarManager().getCalendars({}).filter(isWritableCalendar);
+ if (writableCalendars.length > 0) {
+ let compCal = Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
+ .createInstance(Components.interfaces.calICompositeCalendar);
+ writableCalendars.forEach(compCal.addCalendar, compCal);
+ itipItem.targetCalendar = compCal;
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets the suggested text to be shown when an imip item has been processed.
+ * This text is ready localized and can be displayed to the user.
+ *
+ * @param aStatus The status of the processing (i.e NS_OK, an error code)
+ * @param aOperationType An operation type from calIOperationListener
+ * @return The suggested text.
+ */
+ getCompleteText: function(aStatus, aOperationType) {
+ function _gs(strName, param) {
+ return cal.calGetString("lightning", strName, param, "lightning");
+ }
+
+ let text = "";
+ const cIOL = Components.interfaces.calIOperationListener;
+ if (Components.isSuccessCode(aStatus)) {
+ switch (aOperationType) {
+ case cIOL.ADD: text = _gs("imipAddedItemToCal2"); break;
+ case cIOL.MODIFY: text = _gs("imipUpdatedItem2"); break;
+ case cIOL.DELETE: text = _gs("imipCanceledItem2"); break;
+ }
+ } else {
+ text = _gs("imipBarProcessingFailed", [aStatus.toString(16)]);
+ }
+ return text;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets a text describing the given itip method. The text is of the form
+ * "This Message contains a ... ".
+ *
+ * @param method The method to describe.
+ * @return The localized text about the method.
+ */
+ getMethodText: function(method) {
+ function _gs(strName) {
+ return cal.calGetString("lightning", strName, null, "lightning");
+ }
+
+ switch (method) {
+ case "REFRESH": return _gs("imipBarRefreshText");
+ case "REQUEST": return _gs("imipBarRequestText");
+ case "PUBLISH": return _gs("imipBarPublishText");
+ case "CANCEL": return _gs("imipBarCancelText");
+ case "REPLY": return _gs("imipBarReplyText");
+ case "COUNTER": return _gs("imipBarCounterText");
+ case "DECLINECOUNTER": return _gs("imipBarDeclineCounterText");
+ default:
+ cal.ERROR("Unknown iTIP method: " + method);
+ return _gs("imipBarUnsupportedText");
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets localized toolbar label about the message state and triggers buttons to show.
+ * This returns a JS object with the following structure:
+ *
+ * {
+ * label: "This is a desciptive text about the itip item",
+ * buttons: ["imipXXXButton", ...],
+ * hideMenuItem: ["imipXXXButton_Option", ...]
+ * }
+ *
+ * @see processItipItem This takes the same parameters as its optionFunc.
+ * @param itipItem The itipItem to query.
+ * @param rc The result of retrieving the item
+ * @param actionFunc The action function.
+ */
+ getOptionsText: function(itipItem, rc, actionFunc, foundItems) {
+ function _gs(strName) {
+ return cal.calGetString("lightning", strName, null, "lightning");
+ }
+ let imipLabel = null;
+ if (itipItem.receivedMethod) {
+ imipLabel = cal.itip.getMethodText(itipItem.receivedMethod);
+ }
+ let data = { label: imipLabel, buttons: [], hideMenuItems: [] };
+
+ let disallowedCounter = false;
+ if (foundItems && foundItems.length) {
+ let disallow = foundItems[0].getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ disallowedCounter = disallow && disallow == "TRUE";
+ }
+ if (rc == Components.interfaces.calIErrors.CAL_IS_READONLY) {
+ // No writable calendars, tell the user about it
+ data.label = _gs("imipBarNotWritable");
+ } else if (Components.isSuccessCode(rc) && !actionFunc) {
+ // This case, they clicked on an old message that has already been
+ // added/updated, we want to tell them that.
+ data.label = _gs("imipBarAlreadyProcessedText");
+ if (foundItems && foundItems.length) {
+ data.buttons.push("imipDetailsButton");
+ if (itipItem.receivedMethod == "COUNTER" && itipItem.sender) {
+ if (disallowedCounter) {
+ data.label = _gs("imipBarDisallowedCounterText");
+ } else {
+ let comparison;
+ for (let item of itipItem.getItemList({})) {
+ let attendees = cal.getAttendeesBySender(
+ item.getAttendees({}),
+ itipItem.sender
+ );
+ if (attendees.length == 1) {
+ let replyer = foundItems[0].getAttendeeById(attendees[0].id);
+ comparison = cal.itip.compareSequence(item, foundItems[0]);
+ if (comparison == 1) {
+ data.label = _gs("imipBarCounterErrorText");
+ break;
+ } else if (comparison == -1) {
+ data.label = _gs("imipBarCounterPreviousVersionText");
+ }
+ }
+ }
+ }
+ }
+ } else if (itipItem.receivedMethod == "REPLY") {
+ // The item has been previously removed from the available calendars or the calendar
+ // containing the item is not available
+ let delmgr = Components.classes["@mozilla.org/calendar/deleted-items-manager;1"]
+ .getService(Components.interfaces.calIDeletedItems);
+ let delTime = null;
+ let items = itipItem.getItemList({});
+ if (items && items.length) {
+ delTime = delmgr.getDeletedDate(items[0].id);
+ }
+ if (delTime) {
+ data.label = _gs("imipBarReplyToRecentlyRemovedItem", [delTime.toString()]);
+ } else {
+ data.label = _gs("imipBarReplyToNotExistingItem");
+ }
+ } else if (itipItem.receivedMethod == "DECLINECOUNTER") {
+ data.label = _gs("imipBarDeclineCounterText");
+ }
+ } else if (Components.isSuccessCode(rc)) {
+ cal.LOG("iTIP options on: " + actionFunc.method);
+ switch (actionFunc.method) {
+ case "PUBLISH:UPDATE":
+ case "REQUEST:UPDATE-MINOR":
+ data.label = _gs("imipBarUpdateText");
+ // falls through
+ case "REPLY":
+ data.buttons.push("imipUpdateButton");
+ break;
+ case "PUBLISH":
+ data.buttons.push("imipAddButton");
+ break;
+ case "REQUEST:UPDATE":
+ case "REQUEST:NEEDS-ACTION":
+ case "REQUEST": {
+ if (actionFunc.method == "REQUEST:UPDATE") {
+ data.label = _gs("imipBarUpdateText");
+ } else if (actionFunc.method == "REQUEST:NEEDS-ACTION") {
+ data.label = _gs("imipBarProcessedNeedsAction");
+ }
+
+ let isRecurringMaster = false;
+ for (let item of itipItem.getItemList({})) {
+ if (item.recurrenceInfo) {
+ isRecurringMaster = true;
+ }
+ }
+ if (itipItem.getItemList({}).length > 1 || isRecurringMaster) {
+ data.buttons.push("imipAcceptRecurrencesButton");
+ data.buttons.push("imipDeclineRecurrencesButton");
+ } else {
+ data.buttons.push("imipAcceptButton");
+ data.buttons.push("imipDeclineButton");
+ }
+ data.buttons.push("imipMoreButton");
+ // Use data.hideMenuItems.push("idOfMenuItem") to hide specific menuitems
+ // from the dropdown menu of a button. This might be useful to to remove
+ // a generally available option for a specific invitation, because the
+ // respective feature is not available for the calendar, the invitation
+ // is in or the feature is prohibited by the organizer
+ break;
+ }
+ case "CANCEL": {
+ data.buttons.push("imipDeleteButton");
+ break;
+ }
+ case "REFRESH": {
+ data.buttons.push("imipReconfirmButton");
+ break;
+ }
+ case "COUNTER": {
+ if (disallowedCounter) {
+ data.label = _gs("imipBarDisallowedCounterText");
+ }
+ data.buttons.push("imipDeclineCounterButton");
+ data.buttons.push("imipRescheduleButton");
+ break;
+ }
+ default:
+ data.label = _gs("imipBarUnsupportedText");
+ break;
+ }
+ } else {
+ data.label = _gs("imipBarUnsupportedText");
+ }
+
+ return data;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ * Retrieves the message sender.
+ *
+ * @param {nsIMsgHdr} aMsgHdr The message header to check.
+ * @return The email address of the intended recipient.
+ */
+ getMessageSender: function(aMsgHdr) {
+ let author = (aMsgHdr && aMsgHdr.author) || "";
+ let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Components.interfaces.nsIMsgCompFields);
+ let addresses = compFields.splitRecipients(author, true, {});
+ if (addresses.length != 1) {
+ cal.LOG("No unique email address for lookup in message.\r\n" + cal.STACK(20));
+ }
+ return addresses[0] || null;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Retrieves the intended recipient for this message.
+ *
+ * @param aMsgHdr The message to check.
+ * @return The email of the intended recipient.
+ */
+ getMessageRecipient: function(aMsgHdr) {
+ if (!aMsgHdr) {
+ return null;
+ }
+
+ let identities;
+ let actMgr = MailServices.accounts;
+ if (aMsgHdr.accountKey) {
+ // First, check if the message has an account key. If so, we can use the
+ // account identities to find the correct recipient
+ identities = actMgr.getAccount(aMsgHdr.accountKey).identities;
+ } else if (aMsgHdr.folder) {
+ // Without an account key, we have to revert back to using the server
+ identities = actMgr.getIdentitiesForServer(aMsgHdr.folder.server);
+ }
+
+ let emailMap = {};
+ if (!identities || identities.length == 0) {
+ let identity;
+ // If we were not able to retrieve identities above, then we have no
+ // choice but to revert to the default identity.
+ let defaultAccount = actMgr.defaultAccount;
+ if (defaultAccount) {
+ identity = defaultAccount.defaultIdentity;
+ }
+ if (!identity) {
+ // If there isn't a default identity (i.e Local Folders is your
+ // default identity), then go ahead and use the first available
+ // identity.
+ let allIdentities = actMgr.allIdentities;
+ if (allIdentities.length > 0) {
+ identity = allIdentities.queryElementAt(0, Components.interfaces.nsIMsgIdentity);
+ } else {
+ // If there are no identities at all, we cannot get a recipient.
+ return null;
+ }
+ }
+ emailMap[identity.email.toLowerCase()] = true;
+ } else {
+ // Build a map of usable email addresses
+ for (let i = 0; i < identities.length; i++) {
+ let identity = identities.queryElementAt(i, Components.interfaces.nsIMsgIdentity);
+ emailMap[identity.email.toLowerCase()] = true;
+ }
+ }
+
+ // First check the recipient list
+ let toList = MailServices.headerParser.makeFromDisplayAddress(aMsgHdr.recipients || "");
+ for (let recipient of toList) {
+ if (recipient.email.toLowerCase() in emailMap) {
+ // Return the first found recipient
+ return recipient;
+ }
+ }
+
+ // Maybe we are in the CC list?
+ let ccList = MailServices.headerParser.makeFromDisplayAddress(aMsgHdr.ccList || "");
+ for (let recipient of ccList) {
+ if (recipient.email.toLowerCase() in emailMap) {
+ // Return the first found recipient
+ return recipient;
+ }
+ }
+
+ // Hrmpf. Looks like delegation or maybe Bcc.
+ return null;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Prompt for the target calendar, if needed for the given method. This
+ * calendar will be set on the passed itip item.
+ *
+ * @param aMethod The method to check.
+ * @param aItipItem The itip item to set the target calendar on.
+ * @param aWindow The window to open the dialog on.
+ * @return True, if a calendar was selected or no selection is
+ * needed.
+ */
+ promptCalendar: function(aMethod, aItipItem, aWindow) {
+ let needsCalendar = false;
+ let targetCalendar = null;
+ switch (aMethod) {
+ // methods that don't require the calendar chooser:
+ case "REFRESH":
+ case "REQUEST:UPDATE":
+ case "REQUEST:UPDATE-MINOR":
+ case "PUBLISH:UPDATE":
+ case "REPLY":
+ case "CANCEL":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ needsCalendar = false;
+ break;
+ default:
+ needsCalendar = true;
+ break;
+ }
+
+ if (needsCalendar) {
+ let calendars = cal.getCalendarManager().getCalendars({}).filter(cal.itip.isSchedulingCalendar);
+
+ if (aItipItem.receivedMethod == "REQUEST") {
+ // try to further limit down the list to those calendars that
+ // are configured to a matching attendee;
+ let item = aItipItem.getItemList({})[0];
+ let matchingCals = calendars.filter(calendar => cal.getInvitedAttendee(item, calendar) != null);
+ // if there's none, we will show the whole list of calendars:
+ if (matchingCals.length > 0) {
+ calendars = matchingCals;
+ }
+ }
+
+ if (calendars.length == 0) {
+ let msg = cal.calGetString("lightning", "imipNoCalendarAvailable", null, "lightning");
+ aWindow.alert(msg);
+ } else if (calendars.length == 1) {
+ // There's only one calendar, so it's silly to ask what calendar
+ // the user wants to import into.
+ targetCalendar = calendars[0];
+ } else {
+ // Ask what calendar to import into
+ let args = {};
+ args.calendars = calendars;
+ args.onOk = (aCal) => { targetCalendar = aCal; };
+ args.promptText = cal.calGetString("calendar", "importPrompt");
+ aWindow.openDialog("chrome://calendar/content/chooseCalendarDialog.xul",
+ "_blank", "chrome,titlebar,modal,resizable", args);
+ }
+
+ if (targetCalendar) {
+ aItipItem.targetCalendar = targetCalendar;
+ }
+ }
+
+ return !needsCalendar || targetCalendar != null;
+ },
+
+ /**
+ * Clean up after the given iTIP item. This needs to be called once for each
+ * time processItipItem is called. May be called with a null itipItem in
+ * which case it will do nothing.
+ *
+ * @param itipItem The iTIP item to clean up for.
+ */
+ cleanupItipItem: function(itipItem) {
+ if (itipItem) {
+ let itemList = itipItem.getItemList({});
+ if (itemList.length > 0) {
+ // Again, we can assume the id is the same over all items per spec
+ ItipItemFinderFactory.cleanup(itemList[0].id);
+ }
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Checks the passed iTIP item and calls the passed function with options offered.
+ * Be sure to call cleanupItipItem at least once after calling this function.
+ *
+ * @param itipItem iTIP item
+ * @param optionsFunc function being called with parameters: itipItem, resultCode, actionFunc
+ * The action func has a property |method| showing the options:
+ * * REFRESH -- send the latest item (sent by attendee(s))
+ * * PUBLISH -- initial publish, no reply (sent by organizer)
+ * * PUBLISH:UPDATE -- update of a published item (sent by organizer)
+ * * REQUEST -- initial invitation (sent by organizer)
+ * * REQUEST:UPDATE -- rescheduling invitation, has major change (sent by organizer)
+ * * REQUEST:UPDATE-MINOR -- update of invitation, minor change (sent by organizer)
+ * * REPLY -- invitation reply (sent by attendee(s))
+ * * CANCEL -- invitation cancel (sent by organizer)
+ * * COUNTER -- counterproposal (sent by attendee)
+ * * DECLINECOUNTER -- denial of a counterproposal (sent by organizer)
+ */
+ processItipItem: function(itipItem, optionsFunc) {
+ switch (itipItem.receivedMethod.toUpperCase()) {
+ case "REFRESH":
+ case "PUBLISH":
+ case "REQUEST":
+ case "CANCEL":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ case "REPLY": {
+ // Per iTIP spec (new Draft 4), multiple items in an iTIP message MUST have
+ // same ID, this simplifies our searching, we can just look for Item[0].id
+ let itemList = itipItem.getItemList({});
+ if (!itipItem.targetCalendar) {
+ optionsFunc(itipItem, Components.interfaces.calIErrors.CAL_IS_READONLY);
+ } else if (itemList.length > 0) {
+ ItipItemFinderFactory.findItem(itemList[0].id, itipItem, optionsFunc);
+ } else if (optionsFunc) {
+ optionsFunc(itipItem, Components.results.NS_OK);
+ }
+ break;
+ }
+ default: {
+ if (optionsFunc) {
+ optionsFunc(itipItem, Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Scope: iTIP message sender
+ *
+ * Checks to see if e.g. attendees were added/removed or an item has been
+ * deleted and sends out appropriate iTIP messages.
+ */
+ checkAndSend: function(aOpType, aItem, aOriginalItem) {
+ // balance out parts of the modification vs delete confusion, deletion of occurrences
+ // are notified as parent modifications and modifications of occurrences are notified
+ // as mixed new-occurrence, old-parent (IIRC).
+ if (aOriginalItem && aItem.recurrenceInfo) {
+ if (aOriginalItem.recurrenceId && !aItem.recurrenceId) {
+ // sanity check: assure aItem doesn't refer to the master
+ aItem = aItem.recurrenceInfo.getOccurrenceFor(aOriginalItem.recurrenceId);
+ cal.ASSERT(aItem, "unexpected!");
+ if (!aItem) {
+ return;
+ }
+ }
+
+ if (aOriginalItem.recurrenceInfo && aItem.recurrenceInfo) {
+ // check whether the two differ only in EXDATEs
+ let clonedItem = aItem.clone();
+ let exdates = [];
+ for (let ritem of clonedItem.recurrenceInfo.getRecurrenceItems({})) {
+ let wrappedRItem = cal.wrapInstance(ritem, Components.interfaces.calIRecurrenceDate);
+ if (ritem.isNegative &&
+ wrappedRItem &&
+ !aOriginalItem.recurrenceInfo.getRecurrenceItems({}).some((recitem) => {
+ let wrappedR = cal.wrapInstance(recitem, Components.interfaces.calIRecurrenceDate);
+ return recitem.isNegative &&
+ wrappedR &&
+ wrappedR.date.compare(wrappedRItem.date) == 0;
+ })) {
+ exdates.push(wrappedRItem);
+ }
+ }
+ if (exdates.length > 0) {
+ // check whether really only EXDATEs have been added:
+ let recInfo = clonedItem.recurrenceInfo;
+ exdates.forEach(recInfo.deleteRecurrenceItem, recInfo);
+ if (cal.compareItemContent(clonedItem, aOriginalItem)) { // transition into "delete occurrence(s)"
+ // xxx todo: support multiple
+ aItem = aOriginalItem.recurrenceInfo.getOccurrenceFor(exdates[0].date);
+ aOriginalItem = null;
+ aOpType = Components.interfaces.calIOperationListener.DELETE;
+ }
+ }
+ }
+ }
+
+ let autoResponse = { value: false }; // controls confirm to send email only once
+
+ let invitedAttendee = cal.isInvitation(aItem) && cal.getInvitedAttendee(aItem);
+ if (invitedAttendee) { // actually is an invitation copy, fix attendee list to send REPLY
+ /* We check if the attendee id matches one of of the
+ * userAddresses. If they aren't equal, it means that
+ * someone is accepting invitations on behalf of an other user. */
+ if (aItem.calendar.aclEntry) {
+ let userAddresses = aItem.calendar.aclEntry.getUserAddresses({});
+ if (userAddresses.length > 0 &&
+ !cal.attendeeMatchesAddresses(invitedAttendee, userAddresses)) {
+ invitedAttendee = invitedAttendee.clone();
+ invitedAttendee.setProperty("SENT-BY", "mailto:" + userAddresses[0]);
+ }
+ }
+
+ if (aItem.organizer) {
+ let origInvitedAttendee = (aOriginalItem && aOriginalItem.getAttendeeById(invitedAttendee.id));
+
+ if (aOpType == Components.interfaces.calIOperationListener.DELETE) {
+ // in case the attendee has just deleted the item, we want to send out a DECLINED REPLY:
+ origInvitedAttendee = invitedAttendee;
+ invitedAttendee = invitedAttendee.clone();
+ invitedAttendee.participationStatus = "DECLINED";
+ }
+
+ // We want to send a REPLY send if:
+ // - there has been a PARTSTAT change
+ // - in case of an organizer SEQUENCE bump we'd go and reconfirm our PARTSTAT
+ if (!origInvitedAttendee ||
+ (origInvitedAttendee.participationStatus != invitedAttendee.participationStatus) ||
+ (aOriginalItem && (cal.itip.getSequence(aItem) != cal.itip.getSequence(aOriginalItem)))) {
+ aItem = aItem.clone();
+ aItem.removeAllAttendees();
+ aItem.addAttendee(invitedAttendee);
+ // we remove X-MS-OLK-SENDER to avoid confusing Outlook 2007+ (w/o Exchange)
+ // about the notification sender (see bug 603933)
+ if (aItem.hasProperty("X-MS-OLK-SENDER")) {
+ aItem.deleteProperty("X-MS-OLK-SENDER");
+ }
+ // if the event was delegated to the replying attendee, we may also notify also
+ // the delegator due to chapter 3.2.2.3. of RfC 5546
+ let replyTo = [];
+ let delegatorIds = invitedAttendee.getProperty("DELEGATED-FROM");
+ if (delegatorIds &&
+ Preferences.get("calendar.itip.notifyDelegatorOnReply", false)) {
+ let getDelegator = function(aDelegatorId) {
+ let delegator = aOriginalItem.getAttendeeById(aDelegatorId);
+ if (delegator) {
+ replyTo.push(delegator);
+ }
+ };
+ // Our backends currently do not support multi-value params. libical just
+ // swallows any value but the first, while ical.js fails to parse the item
+ // at all. Single values are handled properly by both backends though.
+ // Once bug 1206502 lands, ical.js will handle multi-value params, but
+ // we end up in different return types of getProperty. A native exposure of
+ // DELEGATED-FROM and DELEGATED-TO in calIAttendee may change this.
+ if (Array.isArray(delegatorIds)) {
+ for (let delegatorId of delegatorIds) {
+ getDelegator(delegatorId);
+ }
+ } else if (typeof delegatorIds == "string") {
+ getDelegator(delegatorIds);
+ }
+ }
+ replyTo.push(aItem.organizer);
+ sendMessage(aItem, "REPLY", replyTo, autoResponse);
+ }
+ }
+ return;
+ }
+
+ if (aItem.getProperty("X-MOZ-SEND-INVITATIONS") != "TRUE") { // Only send invitations/cancellations
+ // if the user checked the checkbox
+ return;
+ }
+
+ // special handling for invitation with event status cancelled
+ if (aItem.getAttendees({}).length > 0 &&
+ aItem.getProperty("STATUS") == "CANCELLED") {
+ if (cal.itip.getSequence(aItem) > 0) {
+ // make sure we send a cancellation and not an request
+ aOpType = Components.interfaces.calIOperationListener.DELETE;
+ } else {
+ // don't send an invitation, if the event was newly created and has status cancelled
+ return;
+ }
+ }
+
+ if (aOpType == Components.interfaces.calIOperationListener.DELETE) {
+ sendMessage(aItem, "CANCEL", aItem.getAttendees({}), autoResponse);
+ return;
+ } // else ADD, MODIFY:
+
+ let originalAtt = (aOriginalItem ? aOriginalItem.getAttendees({}) : []);
+ let itemAtt = aItem.getAttendees({});
+ let canceledAttendees = [];
+ let addedAttendees = [];
+
+ if (itemAtt.length > 0 || originalAtt.length > 0) {
+ let attMap = {};
+ for (let att of originalAtt) {
+ attMap[att.id.toLowerCase()] = att;
+ }
+
+ for (let att of itemAtt) {
+ if (att.id.toLowerCase() in attMap) {
+ // Attendee was in original item.
+ delete attMap[att.id.toLowerCase()];
+ } else {
+ // Attendee only in new item
+ addedAttendees.push(att);
+ }
+ }
+
+ for (let id in attMap) {
+ let cancAtt = attMap[id];
+ canceledAttendees.push(cancAtt);
+ }
+ }
+
+ // setting default value to control for sending (cancellation) messages
+ // this will be set to false, once the user cancels sending manually
+ let sendOut = true;
+ // Check to see if some part of the item was updated, if so, re-send REQUEST
+ if (!aOriginalItem || (cal.itip.compare(aItem, aOriginalItem) > 0)) { // REQUEST
+ // check whether it's a simple UPDATE (no SEQUENCE change) or real (RE)REQUEST,
+ // in case of time or location/description change.
+ let isMinorUpdate = (aOriginalItem && (cal.itip.getSequence(aItem) == cal.itip.getSequence(aOriginalItem)));
+
+ if (!isMinorUpdate || !cal.compareItemContent(stripUserData(aItem), stripUserData(aOriginalItem))) {
+ let requestItem = aItem.clone();
+ if (!requestItem.organizer) {
+ requestItem.organizer = createOrganizer(requestItem.calendar);
+ }
+
+ // Fix up our attendees for invitations using some good defaults
+ let recipients = [];
+ let reqItemAtt = requestItem.getAttendees({});
+ if (!isMinorUpdate) {
+ requestItem.removeAllAttendees();
+ }
+ for (let attendee of reqItemAtt) {
+ if (!isMinorUpdate) {
+ attendee = attendee.clone();
+ if (!attendee.role) {
+ attendee.role = "REQ-PARTICIPANT";
+ }
+ attendee.participationStatus = "NEEDS-ACTION";
+ attendee.rsvp = "TRUE";
+ requestItem.addAttendee(attendee);
+ }
+ recipients.push(attendee);
+ }
+
+ // if send out should be limited to newly added attendees and no major
+ // props (attendee is not such) have changed, only the respective attendee
+ // is added to the recipient list while the attendee information in the
+ // ical is left to enable the new attendee to see who else is attending
+ // the event (if not prevented otherwise)
+ if (isMinorUpdate &&
+ addedAttendees.length > 0 &&
+ Preferences.get("calendar.itip.updateInvitationForNewAttendeesOnly", false)) {
+ recipients = addedAttendees;
+ }
+
+ if (recipients.length > 0) {
+ sendOut = sendMessage(requestItem, "REQUEST", recipients, autoResponse);
+ }
+ }
+ }
+
+ // Cancel the event for all canceled attendees
+ if (canceledAttendees.length > 0) {
+ let cancelItem = aOriginalItem.clone();
+ cancelItem.removeAllAttendees();
+ for (let att of canceledAttendees) {
+ cancelItem.addAttendee(att);
+ }
+ if (sendOut) {
+ sendMessage(cancelItem, "CANCEL", canceledAttendees, autoResponse);
+ }
+ }
+ },
+
+ /**
+ * Bumps the SEQUENCE in case of a major change; XXX todo may need more fine-tuning.
+ */
+ prepareSequence: function(newItem, oldItem) {
+ if (cal.isInvitation(newItem)) {
+ return newItem; // invitation copies don't bump the SEQUENCE
+ }
+
+ if (newItem.recurrenceId && !oldItem.recurrenceId && oldItem.recurrenceInfo) {
+ // XXX todo: there's still the bug that modifyItem is called with mixed occurrence/parent,
+ // find original occurrence
+ oldItem = oldItem.recurrenceInfo.getOccurrenceFor(newItem.recurrenceId);
+ cal.ASSERT(oldItem, "unexpected!");
+ if (!oldItem) {
+ return newItem;
+ }
+ }
+
+ let hashMajorProps = function(aItem) {
+ const majorProps = {
+ DTSTART: true,
+ DTEND: true,
+ DURATION: true,
+ DUE: true,
+ RDATE: true,
+ RRULE: true,
+ EXDATE: true,
+ STATUS: true,
+ LOCATION: true
+ };
+
+ let propStrings = [];
+ for (let item of cal.itemIterator([aItem])) {
+ for (let prop of cal.ical.propertyIterator(item.icalComponent)) {
+ if (prop.propertyName in majorProps) {
+ propStrings.push(item.recurrenceId + "#" + prop.icalString);
+ }
+ }
+ }
+ propStrings.sort();
+ return propStrings.join("");
+ };
+
+ let hash1 = hashMajorProps(newItem);
+ let hash2 = hashMajorProps(oldItem);
+ if (hash1 != hash2) {
+ newItem = newItem.clone();
+ // bump SEQUENCE, it never decreases (mind undo scenario here)
+ newItem.setProperty("SEQUENCE",
+ String(Math.max(cal.itip.getSequence(oldItem),
+ cal.itip.getSequence(newItem)) + 1));
+ }
+
+ return newItem;
+ },
+
+ /**
+ * Returns a copy of an itipItem with modified properties and items build from scratch
+ * Use itipItem.clone() instead if only a simple copy is required
+ *
+ * @param {calIItipItem} aItipItem ItipItem to derive a new one from
+ * @param {Array} aItems calIEvent or calITodo items to be contained in the new itipItem
+ * @param {JsObject} aProps Properties to be different in the new itipItem
+ * @return {calIItipItem}
+ */
+ getModifiedItipItem: function(aItipItem, aItems=[], aProps={}) {
+ let itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
+ .createInstance(Components.interfaces.calIItipItem);
+ let serializedItems = "";
+ for (let item of aItems) {
+ serializedItems += cal.getSerializedItem(item);
+ }
+ itipItem.init(serializedItems);
+
+ itipItem.autoResponse = ("autoResponse" in aProps) ? aProps.autoResponse : aItipItem.autoResponse;
+ itipItem.identity = ("identity" in aProps) ? aProps.identity : aItipItem.identity;
+ itipItem.isSend = ("isSend" in aProps) ? aProps.isSend : aItipItem.isSend;
+ itipItem.localStatus = ("localStatus" in aProps) ? aProps.localStatus : aItipItem.localStatus;
+ itipItem.receivedMethod = ("receivedMethod" in aProps) ? aProps.receivedMethod : aItipItem.receivedMethod;
+ itipItem.responseMethod = ("responseMethod" in aProps) ? aProps.responseMethod : aItipItem.responseMethod;
+ itipItem.targetCalendar = ("targetCalendar" in aProps) ? aProps.targetCalendar : aItipItem.targetCalendar;
+
+ return itipItem;
+ },
+
+ /**
+ * A shortcut to send DECLINECOUNTER messages - for everything else use cal.itip.checkAndSend
+ *
+ * @param aItem iTIP item to be sent
+ * @param aMethod iTIP method
+ * @param aRecipientsList an array of calIAttendee objects the message should be sent to
+ * @param aAutoResponse an inout object whether the transport should ask before sending
+ */
+ sendDeclineCounterMessage: function(aItem, aMethod, aRecipientsList, aAutoResponse) {
+ if (aMethod == "DECLINECOUNTER") {
+ return sendMessage(aItem, aMethod, aRecipientsList, aAutoResponse);
+ }
+ }
+};
+
+/** local to this module file
+ * Sets the received info either on the passed attendee or item object.
+ *
+ * @param item either calIAttendee or calIItemBase
+ * @param itipItemItem received iTIP item
+ */
+function setReceivedInfo(item, itipItemItem) {
+ let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
+ item.setProperty(wrappedItem ? "RECEIVED-SEQUENCE"
+ : "X-MOZ-RECEIVED-SEQUENCE",
+ String(cal.itip.getSequence(itipItemItem)));
+ let dtstamp = cal.itip.getStamp(itipItemItem);
+ if (dtstamp) {
+ item.setProperty(wrappedItem ? "RECEIVED-DTSTAMP"
+ : "X-MOZ-RECEIVED-DTSTAMP",
+ dtstamp.getInTimezone(cal.UTC()).icalString);
+ }
+}
+
+/**
+ * Strips user specific data, e.g. categories and alarm settings and returns the stripped item.
+ */
+function stripUserData(item_) {
+ let item = item_.clone();
+ let stamp = item.stampTime;
+ let lastModified = item.lastModifiedTime;
+ item.clearAlarms();
+ item.alarmLastAck = null;
+ item.setCategories(0, []);
+ item.deleteProperty("RECEIVED-SEQUENCE");
+ item.deleteProperty("RECEIVED-DTSTAMP");
+ let propEnum = item.propertyEnumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ let pname = prop.name;
+ if (pname.substr(0, "X-MOZ-".length) == "X-MOZ-") {
+ item.deleteProperty(prop.name);
+ }
+ }
+ item.getAttendees({}).forEach((att) => {
+ att.deleteProperty("RECEIVED-SEQUENCE");
+ att.deleteProperty("RECEIVED-DTSTAMP");
+ });
+ item.setProperty("DTSTAMP", stamp);
+ item.setProperty("LAST-MODIFIED", lastModified); // need to be last to undirty the item
+ return item;
+}
+
+/** local to this module file
+ * Takes over relevant item information from iTIP item and sets received info.
+ *
+ * @param item the stored calendar item to update
+ * @param itipItemItem the received item
+ */
+function updateItem(item, itipItemItem) {
+ function updateUserData(newItem, oldItem) {
+ // preserve user settings:
+ newItem.generation = oldItem.generation;
+ newItem.clearAlarms();
+ for (let alarm of oldItem.getAlarms({})) {
+ newItem.addAlarm(alarm);
+ }
+ newItem.alarmLastAck = oldItem.alarmLastAck;
+ let cats = oldItem.getCategories({});
+ newItem.setCategories(cats.length, cats);
+ }
+
+ let newItem = item.clone();
+ newItem.icalComponent = itipItemItem.icalComponent;
+ setReceivedInfo(newItem, itipItemItem);
+ updateUserData(newItem, item);
+
+ let recInfo = itipItemItem.recurrenceInfo;
+ if (recInfo) {
+ // keep care of installing all overridden items, and mind existing alarms, categories:
+ for (let rid of recInfo.getExceptionIds({})) {
+ let excItem = recInfo.getExceptionFor(rid).clone();
+ cal.ASSERT(excItem, "unexpected!");
+ let newExc = newItem.recurrenceInfo.getOccurrenceFor(rid).clone();
+ newExc.icalComponent = excItem.icalComponent;
+ setReceivedInfo(newExc, itipItemItem);
+ let existingExcItem = item.recurrenceInfo && item.recurrenceInfo.getExceptionFor(rid);
+ if (existingExcItem) {
+ updateUserData(newExc, existingExcItem);
+ }
+ newItem.recurrenceInfo.modifyException(newExc, true);
+ }
+ }
+
+ return newItem;
+}
+
+/** local to this module file
+ * Copies the provider-specified properties from the itip item to the passed
+ * item. Special case property "METHOD" uses the itipItem's receivedMethod.
+ *
+ * @param itipItem The itip item containing the receivedMethod.
+ * @param itipItemItem The calendar item inside the itip item.
+ * @param item The target item to copy to.
+ */
+function copyProviderProperties(itipItem, itipItemItem, item) {
+ // Copy over itip properties to the item if requested by the provider
+ let copyProps = item.calendar.getProperty("itip.copyProperties") || [];
+ for (let prop of copyProps) {
+ if (prop == "METHOD") {
+ // Special case, this copies over the received method
+ item.setProperty("METHOD", itipItem.receivedMethod.toUpperCase());
+ } else if (itipItemItem.hasProperty(prop)) {
+ // Otherwise just copy from the item contained in the itipItem
+ item.setProperty(prop, itipItemItem.getProperty(prop));
+ }
+ }
+}
+
+/** local to this module file
+ * Creates an organizer calIAttendee object based on the calendar's configured organizer id.
+ *
+ * @return calIAttendee object
+ */
+function createOrganizer(aCalendar) {
+ let orgId = aCalendar.getProperty("organizerId");
+ if (!orgId) {
+ return null;
+ }
+ let organizer = cal.createAttendee();
+ organizer.id = orgId;
+ organizer.commonName = aCalendar.getProperty("organizerCN");
+ organizer.role = "REQ-PARTICIPANT";
+ organizer.participationStatus = "ACCEPTED";
+ organizer.isOrganizer = true;
+ return organizer;
+}
+
+/** local to this module file
+ * Sends an iTIP message using the passed item's calendar transport.
+ *
+ * @param aItem iTIP item to be sent
+ * @param aMethod iTIP method
+ * @param aRecipientsList an array of calIAttendee objects the message should be sent to
+ * @param autoResponse an inout object whether the transport should ask before sending
+ */
+function sendMessage(aItem, aMethod, aRecipientsList, autoResponse) {
+ if (aRecipientsList.length == 0) {
+ return false;
+ }
+ let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
+ if (calendar) {
+ if (calendar.QueryInterface(Components.interfaces.calISchedulingSupport)
+ .canNotify(aMethod, aItem)) {
+ // provider will handle that, so we return - we leave it also to the provider to
+ // deal with user canceled notifications (if possible), so set the return value
+ // to true as false would prevent any further notification within this cycle
+ return true;
+ }
+ }
+
+ let aTransport = aItem.calendar.getProperty("itip.transport");
+ if (!aTransport) { // can only send if there's a transport for the calendar
+ return false;
+ }
+ aTransport = aTransport.QueryInterface(Components.interfaces.calIItipTransport);
+
+ let _sendItem = function(aSendToList, aSendItem) {
+ let cIII = Components.interfaces.calIItipItem;
+ let itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
+ .createInstance(Components.interfaces.calIItipItem);
+ itipItem.init(cal.getSerializedItem(aSendItem));
+ itipItem.responseMethod = aMethod;
+ itipItem.targetCalendar = aSendItem.calendar;
+ itipItem.autoResponse = autoResponse && autoResponse.value ? cIII.AUTO : cIII.USER;
+ if (autoResponse) {
+ autoResponse.value = true; // auto every following
+ }
+ // XXX I don't know whether the below are used at all, since we don't use the itip processor
+ itipItem.isSend = true;
+
+ return aTransport.sendItems(aSendToList.length, aSendToList, itipItem);
+ };
+
+ // split up transport, if attendee undisclosure is requested
+ // and this is a message send by the organizer
+ if (aItem.getProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED") == "TRUE" &&
+ aMethod != "REPLY" &&
+ aMethod != "REFRESH" &&
+ aMethod != "COUNTER") {
+ for (let aRecipient of aRecipientsList) {
+ // create a list with a single recipient
+ let sendToList = [aRecipient];
+ // remove other recipients from vevent attendee list
+ let sendItem = aItem.clone();
+ sendItem.removeAllAttendees();
+ sendItem.addAttendee(aRecipient);
+ // send message
+ if (!_sendItem(sendToList, sendItem)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return _sendItem(aRecipientsList, aItem);
+ }
+}
+
+/** local to this module file
+ * An operation listener that is used on calendar operations which checks and sends further iTIP
+ * messages based on the calendar action.
+ *
+ * @param opListener operation listener to forward
+ * @param oldItem the previous item before modification (if any)
+ */
+function ItipOpListener(opListener, oldItem) {
+ this.mOpListener = opListener;
+ this.mOldItem = oldItem;
+}
+ItipOpListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ cal.ASSERT(Components.isSuccessCode(aStatus), "error on iTIP processing");
+ if (Components.isSuccessCode(aStatus)) {
+ cal.itip.checkAndSend(aOperationType, aDetail, this.mOldItem);
+ }
+ if (this.mOpListener) {
+ this.mOpListener.onOperationComplete(aCalendar,
+ aStatus,
+ aOperationType,
+ aId,
+ aDetail);
+ }
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ }
+};
+
+/** local to this module file
+ * Add a parameter SCHEDULE-AGENT=CLIENT to the item before it is
+ * created or updated so that the providers knows scheduling will
+ * be handled by the client.
+ *
+ * @param item item about to be added or updated
+ * @param calendar calendar into which the item is about to be added or updated
+ */
+function addScheduleAgentClient(item, calendar) {
+ if (calendar.getProperty("capabilities.autoschedule.supported") === true) {
+ if (item.organizer) {
+ item.organizer.setProperty("SCHEDULE-AGENT", "CLIENT");
+ }
+ }
+}
+
+var ItipItemFinderFactory = {
+ /** Map to save finder instances for given ids */
+ _findMap: {},
+
+ /**
+ * Create an item finder and track its progress. Be sure to clean up the
+ * finder for this id at some point.
+ *
+ * @param aId The item id to search for
+ * @param aItipItem The iTIP item used for processing
+ * @param aOptionsFunc The options function used for processing the found item
+ */
+ findItem: function(aId, aItipItem, aOptionsFunc) {
+ this.cleanup(aId);
+ let finder = new ItipItemFinder(aId, aItipItem, aOptionsFunc);
+ this._findMap[aId] = finder;
+ finder.findItem();
+ },
+
+ /**
+ * Clean up tracking for the given id. This needs to be called once for
+ * every time findItem is called.
+ *
+ * @param aId The item id to clean up for
+ */
+ cleanup: function(aId) {
+ if (aId in this._findMap) {
+ let finder = this._findMap[aId];
+ finder.destroy();
+ delete this._findMap[aId];
+ }
+ }
+};
+
+/** local to this module file
+ * An operation listener triggered by cal.itip.processItipItem() for lookup of the sent iTIP item's UID.
+ *
+ * @param itipItem sent iTIP item
+ * @param optionsFunc options func, see cal.itip.processItipItem()
+ */
+function ItipItemFinder(aId, itipItem, optionsFunc) {
+ this.mItipItem = itipItem;
+ this.mOptionsFunc = optionsFunc;
+ this.mSearchId = aId;
+}
+
+ItipItemFinder.prototype = {
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calIObserver,
+ Components.interfaces.calIOperationListener
+ ]),
+
+ mSearchId: null,
+ mItipItem: null,
+ mOptionsFunc: null,
+ mFoundItems: null,
+
+ findItem: function() {
+ this.mFoundItems = [];
+ this._unobserveChanges();
+ this.mItipItem.targetCalendar.getItem(this.mSearchId, this);
+ },
+
+ _observeChanges: function(aCalendar) {
+ this._unobserveChanges();
+ this.mObservedCalendar = aCalendar;
+
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.addObserver(this);
+ }
+ },
+ _unobserveChanges: function() {
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.removeObserver(this);
+ this.mObservedCalendar = null;
+ }
+ },
+
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onError: function() {},
+ onPropertyChanged: function() {},
+ onPropertyDeleting: function() {},
+ onLoad: function(aCalendar) {
+ // Its possible that the item was updated. We need to re-retrieve the
+ // items now.
+ this.findItem();
+ },
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ let refItem = aOldItem || aNewItem;
+ if (refItem.id == this.mSearchId) {
+ // Check existing found items to see if it already exists
+ let found = false;
+ for (let [idx, item] of Object.entries(this.mFoundItems)) {
+ if (item.id == refItem.id && item.calendar.id == refItem.calendar.id) {
+ if (aNewItem) {
+ this.mFoundItems.splice(idx, 1, aNewItem);
+ } else {
+ this.mFoundItems.splice(idx, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+
+ // If it hasn't been found and there is to add a item, add it to the end
+ if (!found && aNewItem) {
+ this.mFoundItems.push(aNewItem);
+ }
+ this.processFoundItems();
+ }
+ },
+
+ onAddItem: function(aItem) {
+ // onModifyItem is set up to also handle additions
+ this.onModifyItem(aItem, null);
+ },
+
+ onDeleteItem: function(aItem) {
+ // onModifyItem is set up to also handle deletions
+ this.onModifyItem(null, aItem);
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ this.processFoundItems();
+ },
+
+ destroy: function() {
+ this._unobserveChanges();
+ },
+
+ processFoundItems: function() {
+ let rc = Components.results.NS_OK;
+ const method = this.mItipItem.receivedMethod.toUpperCase();
+ let actionMethod = method;
+ let operations = [];
+
+ if (this.mFoundItems.length > 0) {
+ // Save the target calendar on the itip item
+ this.mItipItem.targetCalendar = this.mFoundItems[0].calendar;
+ this._observeChanges(this.mItipItem.targetCalendar);
+
+ cal.LOG("iTIP on " + method + ": found " + this.mFoundItems.length + " items.");
+ switch (method) {
+ // XXX todo: there's still a potential flaw, if multiple PUBLISH/REPLY/REQUEST on
+ // occurrences happen at once; those lead to multiple
+ // occurrence modifications. Since those modifications happen
+ // implicitly on the parent (ics/memory/storage calls modifyException),
+ // the generation check will fail. We should really consider to allow
+ // deletion/modification/addition of occurrences directly on the providers,
+ // which would ease client code a lot.
+ case "REFRESH":
+ case "PUBLISH":
+ case "REQUEST":
+ case "REPLY":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ for (let itipItemItem of this.mItipItem.getItemList({})) {
+ for (let item of this.mFoundItems) {
+ let rid = itipItemItem.recurrenceId; // XXX todo support multiple
+ if (rid) { // actually applies to individual occurrence(s)
+ if (item.recurrenceInfo) {
+ item = item.recurrenceInfo.getOccurrenceFor(rid);
+ if (!item) {
+ continue;
+ }
+ } else { // the item has been rescheduled with master:
+ itipItemItem = itipItemItem.parentItem;
+ }
+ }
+
+ switch (method) {
+ case "REFRESH": { // xxx todo test
+ let attendees = itipItemItem.getAttendees({});
+ cal.ASSERT(attendees.length == 1,
+ "invalid number of attendees in REFRESH!");
+ if (attendees.length > 0) {
+ let action = function(opListener) {
+ if (!item.organizer) {
+ let org = createOrganizer(item.calendar);
+ if (org) {
+ item = item.clone();
+ item.organizer = org;
+ }
+ }
+ sendMessage(item, "REQUEST", attendees, true /* don't ask */);
+ };
+ operations.push(action);
+ }
+ break;
+ }
+ case "PUBLISH":
+ cal.ASSERT(itipItemItem.getAttendees({}).length == 0,
+ "invalid number of attendees in PUBLISH!");
+ if (item.calendar.getProperty("itip.disableRevisionChecks") ||
+ cal.itip.compare(itipItemItem, item) > 0) {
+ let newItem = updateItem(item, itipItemItem);
+ let action = function(opListener) {
+ return newItem.calendar.modifyItem(newItem, item, opListener);
+ };
+ actionMethod = method + ":UPDATE";
+ operations.push(action);
+ }
+ break;
+ case "REQUEST": {
+ let newItem = updateItem(item, itipItemItem);
+ let att = cal.getInvitedAttendee(newItem);
+ if (!att) { // fall back to using configured organizer
+ att = createOrganizer(newItem.calendar);
+ if (att) {
+ att.isOrganizer = false;
+ }
+ }
+ if (att) {
+ let firstFoundItem = this.mFoundItems[0];
+ // again, fall back to using configured organizer if not found
+ let foundAttendee = firstFoundItem.getAttendeeById(att.id) || att;
+
+ // If the the user hasn't responded to the invitation yet and we
+ // are viewing the current representation of the item, show the
+ // accept/decline buttons. This means newer events will show the
+ // "Update" button and older events will show the "already
+ // processed" text.
+ if (foundAttendee.participationStatus == "NEEDS-ACTION" &&
+ (item.calendar.getProperty("itip.disableRevisionChecks") ||
+ cal.itip.compare(itipItemItem, item) == 0)) {
+ actionMethod = "REQUEST:NEEDS-ACTION";
+ operations.push((opListener, partStat) => {
+ let changedItem = firstFoundItem.clone();
+ changedItem.removeAttendee(foundAttendee);
+ foundAttendee = foundAttendee.clone();
+ if (partStat) {
+ foundAttendee.participationStatus = partStat;
+ }
+ changedItem.addAttendee(foundAttendee);
+
+ return changedItem.calendar.modifyItem(
+ changedItem, firstFoundItem, new ItipOpListener(opListener, firstFoundItem));
+ });
+ } else if (item.calendar.getProperty("itip.disableRevisionChecks") ||
+ cal.itip.compare(itipItemItem, item) > 0) {
+ addScheduleAgentClient(newItem, item.calendar);
+
+ let isMinorUpdate = cal.itip.getSequence(newItem) ==
+ cal.itip.getSequence(item);
+ actionMethod = (isMinorUpdate ? method + ":UPDATE-MINOR"
+ : method + ":UPDATE");
+ operations.push((opListener, partStat) => {
+ if (!partStat) { // keep PARTSTAT
+ let att_ = cal.getInvitedAttendee(item);
+ partStat = att_ ? att_.participationStatus : "NEEDS-ACTION";
+ }
+ newItem.removeAttendee(att);
+ att = att.clone();
+ att.participationStatus = partStat;
+ newItem.addAttendee(att);
+ return newItem.calendar.modifyItem(
+ newItem, item, new ItipOpListener(opListener, item));
+ });
+ }
+ }
+ break;
+ }
+ case "DECLINECOUNTER":
+ // nothing to do right now, but once countering is implemented,
+ // we probably need some action here to remove the proposal from
+ // the countering attendee's calendar
+ break;
+ case "COUNTER":
+ case "REPLY": {
+ let attendees = itipItemItem.getAttendees({});
+ if (method == "REPLY") {
+ cal.ASSERT(
+ attendees.length == 1,
+ "invalid number of attendees in REPLY!"
+ );
+ } else {
+ attendees = cal.getAttendeesBySender(
+ attendees,
+ this.mItipItem.sender
+ );
+ cal.ASSERT(
+ attendees.length == 1,
+ "ambiguous resolution of replying attendee in COUNTER!"
+ );
+ }
+ // we get the attendee from the event stored in the calendar
+ let replyer = item.getAttendeeById(attendees[0].id);
+ if (!replyer && method == "REPLY") {
+ // We accepts REPLYs also from previously uninvited
+ // attendees, so we always have one for REPLY
+ replyer = attendees[0];
+ }
+ let noCheck = item.calendar.getProperty(
+ "itip.disableRevisionChecks");
+ let revCheck = false;
+ if (replyer && !noCheck) {
+ revCheck = cal.itip.compare(itipItemItem, replyer) > 0;
+ if (revCheck && method == "COUNTER") {
+ revCheck = cal.itip.compareSequence(itipItemItem, item) == 0;
+ }
+ }
+
+ if (replyer && (noCheck || revCheck)) {
+ let newItem = item.clone();
+ newItem.removeAttendee(replyer);
+ replyer = replyer.clone();
+ setReceivedInfo(replyer, itipItemItem);
+ let newPS = itipItemItem.getAttendeeById(replyer.id)
+ .participationStatus;
+ replyer.participationStatus = newPS;
+ newItem.addAttendee(replyer);
+
+ // Make sure the provider-specified properties are copied over
+ copyProviderProperties(this.mItipItem, itipItemItem, newItem);
+
+ let action = function(opListener) {
+ // n.b.: this will only be processed in case of reply or
+ // declining the counter request - of sending the
+ // appropriate reply will be taken care within the
+ // opListener (defined in imip-bar.js)
+ // TODO: move that from imip-bar.js to here
+ return newItem.calendar.modifyItem(
+ newItem, item,
+ newItem.calendar.getProperty("itip.notify-replies")
+ ? new ItipOpListener(opListener, item)
+ : opListener);
+ };
+ operations.push(action);
+ }
+ break;
+ }
+ }
+ }
+ }
+ break;
+ case "CANCEL": {
+ let modifiedItems = {};
+ for (let itipItemItem of this.mItipItem.getItemList({})) {
+ for (let item of this.mFoundItems) {
+ let rid = itipItemItem.recurrenceId; // XXX todo support multiple
+ if (rid) { // actually a CANCEL of occurrence(s)
+ if (item.recurrenceInfo) {
+ // collect all occurrence deletions into a single parent modification:
+ let newItem = modifiedItems[item.id];
+ if (!newItem) {
+ newItem = item.clone();
+ modifiedItems[item.id] = newItem;
+
+ // Make sure the provider-specified properties are copied over
+ copyProviderProperties(this.mItipItem, itipItemItem, newItem);
+
+ operations.push(opListener => newItem.calendar.modifyItem(newItem, item, opListener));
+ }
+ newItem.recurrenceInfo.removeOccurrenceAt(rid);
+ } else if (item.recurrenceId && (item.recurrenceId.compare(rid) == 0)) {
+ // parentless occurrence to be deleted (future)
+ operations.push(opListener => item.calendar.deleteItem(item, opListener));
+ }
+ } else {
+ operations.push(opListener => item.calendar.deleteItem(item, opListener));
+ }
+ }
+ }
+ break;
+ }
+ default:
+ rc = Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ }
+ } else { // not found:
+ cal.LOG("iTIP on " + method + ": no existing items.");
+
+ // If the item was not found, observe the target calendar anyway.
+ // It will likely be the composite calendar, so we should update
+ // if an item was added or removed
+ this._observeChanges(this.mItipItem.targetCalendar);
+
+ for (let itipItemItem of this.mItipItem.getItemList({})) {
+ switch (method) {
+ case "REQUEST":
+ case "PUBLISH": {
+ let action = (opListener, partStat) => {
+ let newItem = itipItemItem.clone();
+ setReceivedInfo(newItem, itipItemItem);
+ newItem.parentItem.calendar = this.mItipItem.targetCalendar;
+ addScheduleAgentClient(newItem, this.mItipItem.targetCalendar);
+ if (partStat) {
+ if (partStat != "DECLINED") {
+ cal.alarms.setDefaultValues(newItem);
+ }
+ let att = cal.getInvitedAttendee(newItem);
+ if (!att) { // fall back to using configured organizer
+ att = createOrganizer(newItem.calendar);
+ if (att) {
+ att.isOrganizer = false;
+ newItem.addAttendee(att);
+ }
+ }
+ if (att) {
+ att.participationStatus = partStat;
+ } else {
+ cal.ASSERT(att, "no attendee to reply REQUEST!");
+ return null;
+ }
+ } else {
+ cal.ASSERT(itipItemItem.getAttendees({}).length == 0,
+ "invalid number of attendees in PUBLISH!");
+ }
+ return newItem.calendar.addItem(newItem,
+ method == "REQUEST"
+ ? new ItipOpListener(opListener, null)
+ : opListener);
+ };
+ operations.push(action);
+ break;
+ }
+ case "CANCEL": // has already been processed
+ case "REPLY": // item has been previously removed from the calendar
+ case "COUNTER": // the item has been previously removed form the calendar
+ break;
+ default:
+ rc = Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ }
+ }
+ }
+
+ cal.LOG("iTIP operations: " + operations.length);
+ let actionFunc = null;
+ if (operations.length > 0) {
+ actionFunc = function(opListener, partStat) {
+ for (let operation of operations) {
+ try {
+ operation(opListener, partStat);
+ } catch (exc) {
+ cal.ERROR(exc);
+ }
+ }
+ };
+ actionFunc.method = actionMethod;
+ }
+
+ this.mOptionsFunc(this.mItipItem, rc, actionFunc, this.mFoundItems);
+ },
+
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (Components.isSuccessCode(aStatus)) {
+ this.mFoundItems = this.mFoundItems.concat(aItems);
+ }
+ }
+};
diff --git a/calendar/base/modules/calPrintUtils.jsm b/calendar/base/modules/calPrintUtils.jsm
new file mode 100644
index 000000000..a44b02731
--- /dev/null
+++ b/calendar/base/modules/calPrintUtils.jsm
@@ -0,0 +1,200 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://calendar/modules/calViewUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+cal.print = {
+ /**
+ * Returns a simple key in the format YYYY-MM-DD for use in the table of
+ * dates to day boxes
+ *
+ * @param dt The date to translate
+ * @return YYYY-MM-DD
+ */
+ getDateKey: function(date) {
+ return date.year + "-" + date.month + "-" + date.day;
+ },
+
+ /**
+ * Add category styles to the document's "sheet" element. This is needed
+ * since the HTML created is serialized, so we can't dynamically set the
+ * styles and can be changed if the print formatter decides to return a
+ * DOM document instead.
+ *
+ * @param document The document that contains <style id="sheet"/>.
+ * @param categories Array of categories to insert rules for.
+ */
+ insertCategoryRules: function(document, categories) {
+ let sheet = document.getElementById("sheet");
+ sheet.insertedCategoryRules = sheet.insertedCategoryRules || {};
+
+ for (let category of categories) {
+ let prefName = cal.formatStringForCSSRule(category);
+ let color = Preferences.get("calendar.category.color." + prefName) || "transparent";
+ if (!(prefName in sheet.insertedCategoryRules)) {
+ sheet.insertedCategoryRules[prefName] = true;
+ let ruleAdd = ' .category-color-box[categories~="' + prefName + '"] { ' +
+ " border: 2px solid " + color + "; }\n";
+ sheet.textContent += ruleAdd;
+ }
+ }
+ },
+
+ /**
+ * Add calendar styles to the document's "sheet" element. This is needed
+ * since the HTML created is serialized, so we can't dynamically set the
+ * styles and can be changed if the print formatter decides to return a
+ * DOM document instead.
+ *
+ * @param document The document that contains <style id="sheet"/>.
+ * @param categories The calendar to insert a rule for.
+ */
+ insertCalendarRules: function(document, calendar) {
+ let sheet = document.getElementById("sheet");
+ let color = calendar.getProperty("color") || "#A8C2E1";
+ sheet.insertedCalendarRules = sheet.insertedCalendarRules || {};
+
+ if (!(calendar.id in sheet.insertedCalendarRules)) {
+ sheet.insertedCalendarRules[calendar.id] = true;
+ let formattedId = cal.formatStringForCSSRule(calendar.id);
+ let ruleAdd = ' .calendar-color-box[calendar-id="' + formattedId + '"] { ' +
+ " background-color: " + color + "; " +
+ " color: " + cal.getContrastingTextColor(color) + "; }\n";
+ sheet.textContent += ruleAdd;
+ }
+ },
+
+ /**
+ * Serializes the given item by setting marked nodes to the item's content.
+ * Has some expectations about the DOM document (in CSS-selector-speak), all
+ * following nodes MUST exist.
+ *
+ * - #item-template will be cloned and filled, and modified:
+ * - .item-interval gets the time interval of the item.
+ * - .item-title gets the item title
+ * - .category-color-box gets a 2px solid border in category color
+ * - .calendar-color-box gets background color of the calendar
+ *
+ * @param document The DOM Document to set things on
+ * @param item The item to serialize
+ * @param dayContainer The DOM Node to insert the container in
+ */
+ addItemToDaybox: function(document, item, boxDate, dayContainer) {
+ // Clone our template
+ let itemNode = document.getElementById("item-template").cloneNode(true);
+ itemNode.removeAttribute("id");
+ itemNode.item = item;
+
+ // Fill in details of the item
+ let itemInterval = cal.print.getItemIntervalString(item, boxDate);
+ itemNode.querySelector(".item-interval").textContent = itemInterval;
+ itemNode.querySelector(".item-title").textContent = item.title;
+
+ // Fill in category details
+ let categoriesArray = item.getCategories({});
+ if (categoriesArray.length > 0) {
+ let cssClassesArray = categoriesArray.map(cal.formatStringForCSSRule);
+ itemNode.querySelector(".category-color-box")
+ .setAttribute("categories", cssClassesArray.join(" "));
+
+ cal.print.insertCategoryRules(document, categoriesArray);
+ }
+
+ // Fill in calendar color
+ itemNode.querySelector(".calendar-color-box")
+ .setAttribute("calendar-id", cal.formatStringForCSSRule(item.calendar.id));
+ cal.print.insertCalendarRules(document, item.calendar);
+
+ // Add it to the day container in the right order
+ cal.binaryInsertNode(dayContainer, itemNode, item, cal.view.compareItems);
+ },
+
+ /**
+ * Serializes the given item by setting marked nodes to the item's
+ * content. Should be used for tasks with no start and due date. Has
+ * some expectations about the DOM document (in CSS-selector-speak),
+ * all following nodes MUST exist.
+ *
+ * - Nodes will be added to #task-container.
+ * - #task-list-box will have the "hidden" attribute removed.
+ * - #task-template will be cloned and filled, and modified:
+ * - .task-checkbox gets the "checked" attribute set, if completed
+ * - .task-title gets the item title.
+ *
+ * @param document The DOM Document to set things on
+ * @param item The item to serialize
+ */
+ addItemToDayboxNodate: function(document, item) {
+ let taskContainer = document.getElementById("task-container");
+ let taskNode = document.getElementById("task-template").cloneNode(true);
+ taskNode.removeAttribute("id");
+ taskNode.item = item;
+
+ let taskListBox = document.getElementById("tasks-list-box");
+ if (taskListBox.hasAttribute("hidden")) {
+ let tasksTitle = document.getElementById("tasks-title");
+ taskListBox.removeAttribute("hidden");
+ tasksTitle.textContent = cal.calGetString("calendar", "tasksWithNoDueDate");
+ }
+
+ // Fill in details of the task
+ if (item.isCompleted) {
+ taskNode.querySelector(".task-checkbox").setAttribute("checked", "checked");
+ }
+
+ taskNode.querySelector(".task-title").textContent = item.title;
+
+ let collator = cal.createLocaleCollator();
+ cal.binaryInsertNode(taskContainer, taskNode, item, (a, b) => collator.compareString(0, a, b), node => node.item.title);
+ },
+
+ /**
+ * Get time interval string for the given item. Returns an empty string for all-day items.
+ *
+ * @param aItem The item providing the interval
+ * @return The string describing the interval
+ */
+ getItemIntervalString: function(aItem, aBoxDate) {
+ // omit time label for all-day items
+ let startDate = aItem[cal.calGetStartDateProp(aItem)];
+ let endDate = aItem[cal.calGetEndDateProp(aItem)];
+ if ((startDate && startDate.isDate) || (endDate && endDate.isDate)) {
+ return "";
+ }
+
+ // check for tasks without start and/or due date
+ if (!startDate || !endDate) {
+ return cal.getDateFormatter().formatItemTimeInterval(aItem);
+ }
+
+ let dateFormatter = cal.getDateFormatter();
+ let defaultTimezone = cal.calendarDefaultTimezone();
+ startDate = startDate.getInTimezone(defaultTimezone);
+ endDate = endDate.getInTimezone(defaultTimezone);
+ let start = startDate.clone();
+ let end = endDate.clone();
+ start.isDate = true;
+ end.isDate = true;
+ if (start.compare(end) == 0) {
+ // Events that start and end in the same day.
+ return dateFormatter.formatTimeInterval(startDate, endDate);
+ } else {
+ // Events that span two or more days.
+ let compareStart = aBoxDate.compare(start);
+ let compareEnd = aBoxDate.compare(end);
+ if (compareStart == 0) {
+ return "\u21e4 " + dateFormatter.formatTime(startDate); // unicode '⇤'
+ } else if (compareStart > 0 && compareEnd < 0) {
+ return "\u21ff"; // unicode '↔'
+ } else if (compareEnd == 0) {
+ return "\u21e5 " + dateFormatter.formatTime(endDate); // unicode '⇥'
+ } else {
+ return "";
+ }
+ }
+ }
+};
diff --git a/calendar/base/modules/calProviderUtils.jsm b/calendar/base/modules/calProviderUtils.jsm
new file mode 100644
index 000000000..07bd069e2
--- /dev/null
+++ b/calendar/base/modules/calProviderUtils.jsm
@@ -0,0 +1,856 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAuthUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource:///modules/iteratorUtils.jsm");
+
+/*
+ * Provider helper code
+ */
+
+this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
+
+/**
+ * Prepare HTTP channel with standard request headers and upload
+ * data/content-type if needed
+ *
+ * @param arUri Channel Uri, will only be used for a new
+ * channel.
+ * @param aUploadData Data to be uploaded, if any. This may be a
+ * nsIInputStream or string data. In the
+ * latter case the string will be converted
+ * to an input stream.
+ * @param aContentType Value for Content-Type header, if any
+ * @param aNotificationCallbacks Calendar using channel
+ * @param aExisting An existing channel to modify (optional)
+ */
+cal.prepHttpChannel = function(aUri, aUploadData, aContentType, aNotificationCallbacks, aExisting) {
+ let channel = aExisting || Services.io.newChannelFromURI2(aUri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_NORMAL,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+ let httpchannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+
+ httpchannel.setRequestHeader("Accept", "text/xml", false);
+ httpchannel.setRequestHeader("Accept-Charset", "utf-8,*;q=0.1", false);
+ httpchannel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
+ httpchannel.notificationCallbacks = aNotificationCallbacks;
+
+ if (aUploadData) {
+ httpchannel = httpchannel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ let stream;
+ if (aUploadData instanceof Components.interfaces.nsIInputStream) {
+ // Make sure the stream is reset
+ stream = aUploadData.QueryInterface(Components.interfaces.nsISeekableStream);
+ stream.seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0);
+ } else {
+ // Otherwise its something that should be a string, convert it.
+ let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ stream = converter.convertToInputStream(aUploadData.toString());
+ }
+
+ httpchannel.setUploadStream(stream, aContentType, -1);
+ }
+
+ return httpchannel;
+};
+
+/**
+ * calSendHttpRequest; send prepared HTTP request
+ *
+ * @param aStreamLoader streamLoader for request
+ * @param aChannel channel for request
+ * @param aListener listener for method completion
+ */
+cal.sendHttpRequest = function(aStreamLoader, aChannel, aListener) {
+ aStreamLoader.init(aListener);
+ aChannel.asyncOpen(aStreamLoader, aChannel);
+};
+
+cal.createStreamLoader = function() {
+ return Components.classes["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Components.interfaces.nsIStreamLoader);
+};
+
+cal.convertByteArray = function(aResult, aResultLength, aCharset, aThrow) {
+ try {
+ let resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ resultConverter.charset = aCharset || "UTF-8";
+ return resultConverter.convertFromByteArray(aResult, aResultLength);
+ } catch (e) {
+ if (aThrow) {
+ throw e;
+ }
+ }
+ return null;
+};
+
+/**
+ * getInterface method for providers. This should be called in the context of
+ * the respective provider, i.e
+ *
+ * return cal.InterfaceRequestor_getInterface.apply(this, arguments);
+ *
+ * or
+ * ...
+ * getInterface: cal.InterfaceRequestor_getInterface,
+ * ...
+ *
+ * NOTE: If the server only provides one realm for all calendars, be sure that
+ * the |this| object implements calICalendar. In this case the calendar name
+ * will be appended to the realm. If you need that feature disabled, see the
+ * capabilities section of calICalendar.idl
+ *
+ * @param aIID The interface ID to return
+ */
+cal.InterfaceRequestor_getInterface = function(aIID) {
+ try {
+ // Try to query the this object for the requested interface but don't
+ // throw if it fails since that borks the network code.
+ return this.QueryInterface(aIID);
+ } catch (e) {
+ // Support Auth Prompt Interfaces
+ if (aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
+ if (!this.calAuthPrompt) {
+ this.calAuthPrompt = new cal.auth.Prompt();
+ }
+ return this.calAuthPrompt;
+ } else if (aIID.equals(Components.interfaces.nsIAuthPromptProvider) ||
+ aIID.equals(Components.interfaces.nsIPrompt)) {
+ return Services.ww.getNewPrompter(null);
+ } else if (aIID.equals(Components.interfaces.nsIBadCertListener2)) {
+ if (!this.badCertHandler) {
+ this.badCertHandler = new cal.BadCertHandler(this);
+ }
+ return this.badCertHandler;
+ } else {
+ Components.returnCode = e;
+ }
+ }
+ return null;
+};
+
+/**
+ * Bad Certificate Handler for Network Requests. Shows the Network Exception
+ * Dialog if a certificate Problem occurs.
+ */
+cal.BadCertHandler = function(thisProvider) {
+ this.thisProvider = thisProvider;
+};
+cal.BadCertHandler.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIBadCertListener2]),
+
+ notifyCertProblem: function(socketInfo, status, targetSite) {
+ // Unfortunately we can't pass js objects using the window watcher, so
+ // we'll just take the first available calendar window. We also need to
+ // do this on a timer so that the modal window doesn't block the
+ // network request.
+ let calWindow = cal.getCalendarWindow();
+
+ let timerCallback = {
+ thisProvider: this.thisProvider,
+ notify: function(timer) {
+ let params = {
+ exceptionAdded: false,
+ sslStatus: status,
+ prefetchCert: true,
+ location: targetSite
+ };
+ calWindow.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "",
+ "chrome,centerscreen,modal",
+ params);
+ if (this.thisProvider.canRefresh &&
+ params.exceptionAdded) {
+ // Refresh the provider if the
+ // exception certificate was added
+ this.thisProvider.refresh();
+ }
+ }
+ };
+ let timer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(timerCallback,
+ 0,
+ Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ return true;
+ }
+};
+
+/**
+ * Freebusy interval implementation. All parameters are optional.
+ *
+ * @param aCalId The calendar id to set up with.
+ * @param aFreeBusyType The type from calIFreeBusyInterval.
+ * @param aStart The start of the interval.
+ * @param aEnd The end of the interval.
+ * @return The fresh calIFreeBusyInterval.
+ */
+cal.FreeBusyInterval = function(aCalId, aFreeBusyType, aStart, aEnd) {
+ this.calId = aCalId;
+ this.interval = Components.classes["@mozilla.org/calendar/period;1"]
+ .createInstance(Components.interfaces.calIPeriod);
+ this.interval.start = aStart;
+ this.interval.end = aEnd;
+
+ this.freeBusyType = aFreeBusyType || Components.interfaces.calIFreeBusyInterval.UNKNOWN;
+};
+cal.FreeBusyInterval.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIFreeBusyInterval]),
+ calId: null,
+ interval: null,
+ freeBusyType: Components.interfaces.calIFreeBusyInterval.UNKNOWN
+};
+
+/**
+ * Gets the iTIP/iMIP transport if the passed calendar has configured email.
+ */
+cal.getImipTransport = function(aCalendar) {
+ // assure an identity is configured for the calendar
+ return (aCalendar.getProperty("imip.identity")
+ ? Components.classes["@mozilla.org/calendar/itip-transport;1?type=email"]
+ .getService(Components.interfaces.calIItipTransport)
+ : null);
+};
+
+/**
+ * Gets the configured identity and account of a particular calendar instance, or null.
+ *
+ * @param aCalendar Calendar instance
+ * @param outAccount Optional out value for account
+ * @return The configured identity
+ */
+cal.getEmailIdentityOfCalendar = function(aCalendar, outAccount) {
+ cal.ASSERT(aCalendar, "no calendar!", Components.results.NS_ERROR_INVALID_ARG);
+ let key = aCalendar.getProperty("imip.identity.key");
+ if (key === null) { // take default account/identity:
+ let findIdentity = function(account) {
+ if (account && account.identities.length) {
+ return account.defaultIdentity ||
+ account.identities.queryElementAt(0, Components.interfaces.nsIMsgIdentity);
+ }
+ return null;
+ };
+
+ let foundAccount = MailServices.accounts.defaultAccount;
+ let foundIdentity = findIdentity(foundAccount);
+
+ if (!foundAccount || !foundIdentity) {
+ let accounts = MailServices.accounts.accounts;
+ for (let account of fixIterator(accounts, Components.interfaces.nsIMsgAccount)) {
+ let identity = findIdentity(account);
+
+ if (account && identity) {
+ foundAccount = account;
+ foundIdentity = identity;
+ break;
+ }
+ }
+ }
+
+ if (outAccount) {
+ outAccount.value = foundIdentity ? foundAccount : null;
+ }
+ return foundIdentity;
+ } else {
+ if (key.length == 0) { // i.e. "None"
+ return null;
+ }
+ let identity = null;
+ cal.calIterateEmailIdentities((identity_, account) => {
+ if (identity_.key == key) {
+ identity = identity_;
+ if (outAccount) {
+ outAccount.value = account;
+ }
+ }
+ return (identity_.key != key);
+ });
+
+ if (!identity) {
+ // dangling identity:
+ cal.WARN("Calendar " + (aCalendar.uri ? aCalendar.uri.spec : aCalendar.id) +
+ " has a dangling E-Mail identity configured.");
+ }
+ return identity;
+ }
+};
+
+
+/**
+ * fromRFC3339
+ * Convert a RFC3339 compliant Date string to a calIDateTime.
+ *
+ * @param aStr The RFC3339 compliant Date String
+ * @param aTimezone The timezone this date string is most likely in
+ * @return A calIDateTime object
+ */
+cal.fromRFC3339 = function(aStr, aTimezone) {
+ // XXX I have not covered leapseconds (matches[8]), this might need to
+ // be done. The only reference to leap seconds I found is bug 227329.
+ //
+
+ // Create a DateTime instance (calUtils.js)
+ let dateTime = cal.createDateTime();
+
+ // Killer regex to parse RFC3339 dates
+ let re = new RegExp("^([0-9]{4})-([0-9]{2})-([0-9]{2})" +
+ "([Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?" +
+ "(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?");
+
+ let matches = re.exec(aStr);
+
+ if (!matches) {
+ return null;
+ }
+
+ // Set usual date components
+ dateTime.isDate = (matches[4] == null);
+
+ dateTime.year = matches[1];
+ dateTime.month = matches[2] - 1; // Jan is 0
+ dateTime.day = matches[3];
+
+ if (!dateTime.isDate) {
+ dateTime.hour = matches[5];
+ dateTime.minute = matches[6];
+ dateTime.second = matches[7];
+ }
+
+ // Timezone handling
+ if (matches[9] == "Z" || matches[9] == "z") {
+ // If the dates timezone is "Z" or "z", then this is UTC, no matter
+ // what timezone was passed
+ dateTime.timezone = cal.UTC();
+ } else if (matches[9] == null) {
+ // We have no timezone info, only a date. We have no way to
+ // know what timezone we are in, so lets assume we are in the
+ // timezone of our local calendar, or whatever was passed.
+
+ dateTime.timezone = aTimezone;
+ } else {
+ let offset_in_s = (matches[11] == "-" ? -1 : 1) *
+ ((matches[12] * 3600) + (matches[13] * 60));
+
+ // try local timezone first
+ dateTime.timezone = aTimezone;
+
+ // If offset does not match, go through timezones. This will
+ // give you the first tz in the alphabet and kill daylight
+ // savings time, but we have no other choice
+ if (dateTime.timezoneOffset != offset_in_s) {
+ // TODO A patch to Bug 363191 should make this more efficient.
+
+ let tzService = cal.getTimezoneService();
+ // Enumerate timezones, set them, check their offset
+ let enumerator = tzService.timezoneIds;
+ while (enumerator.hasMore()) {
+ let id = enumerator.getNext();
+ dateTime.timezone = tzService.getTimezone(id);
+ if (dateTime.timezoneOffset == offset_in_s) {
+ // This is our last step, so go ahead and return
+ return dateTime;
+ }
+ }
+ // We are still here: no timezone was found
+ dateTime.timezone = cal.UTC();
+ if (!dateTime.isDate) {
+ dateTime.hour += (matches[11] == "-" ? -1 : 1) * matches[12];
+ dateTime.minute += (matches[11] == "-" ? -1 : 1) * matches[13];
+ }
+ }
+ }
+ return dateTime;
+};
+
+/**
+ * toRFC3339
+ * Convert a calIDateTime to a RFC3339 compliant Date string
+ *
+ * @param aDateTime The calIDateTime object
+ * @return The RFC3339 compliant date string
+ */
+cal.toRFC3339 = function(aDateTime) {
+ if (!aDateTime) {
+ return "";
+ }
+
+ let full_tzoffset = aDateTime.timezoneOffset;
+ let tzoffset_hr = Math.floor(Math.abs(full_tzoffset) / 3600);
+
+ let tzoffset_mn = ((Math.abs(full_tzoffset) / 3600).toFixed(2) -
+ tzoffset_hr) * 60;
+
+ let str = aDateTime.year + "-" +
+ ("00" + (aDateTime.month + 1)).substr(-2) + "-" +
+ ("00" + aDateTime.day).substr(-2);
+
+ // Time and Timezone extension
+ if (!aDateTime.isDate) {
+ str += "T" +
+ ("00" + aDateTime.hour).substr(-2) + ":" +
+ ("00" + aDateTime.minute).substr(-2) + ":" +
+ ("00" + aDateTime.second).substr(-2);
+ if (aDateTime.timezoneOffset != 0) {
+ str += (full_tzoffset < 0 ? "-" : "+") +
+ ("00" + tzoffset_hr).substr(-2) + ":" +
+ ("00" + tzoffset_mn).substr(-2);
+ } else if (aDateTime.timezone.isFloating) {
+ // RFC3339 Section 4.3 Unknown Local Offset Convention
+ str += "-00:00";
+ } else {
+ // ZULU Time, according to ISO8601's timezone-offset
+ str += "Z";
+ }
+ }
+ return str;
+};
+
+cal.promptOverwrite = function(aMode, aItem) {
+ let window = cal.getCalendarWindow();
+ let args = {
+ item: aItem,
+ mode: aMode,
+ overwrite: false
+ };
+
+ window.openDialog("chrome://calendar/content/calendar-conflicts-dialog.xul",
+ "calendarConflictsDialog",
+ "chrome,titlebar,modal",
+ args);
+
+ return args.overwrite;
+};
+
+/**
+ * Observer bag implementation taking care to replay open batch notifications.
+ */
+cal.ObserverBag = function(iid) {
+ this.init(iid);
+};
+cal.ObserverBag.prototype = {
+ __proto__: cal.calListenerBag.prototype,
+
+ mBatchCount: 0,
+ notify: function(func, args) {
+ switch (func) {
+ case "onStartBatch":
+ ++this.mBatchCount;
+ break;
+ case "onEndBatch":
+ --this.mBatchCount;
+ break;
+ }
+ return this.__proto__.__proto__.notify.apply(this, arguments);
+ },
+
+ add: function(iface) {
+ if (this.__proto__.__proto__.add.apply(this, arguments) && (this.mBatchCount > 0)) {
+ // Replay batch notifications, because the onEndBatch notifications are yet to come.
+ // We may think about doing the reverse on remove, though I currently see no need:
+ for (let i = this.mBatchCount; i--;) {
+ iface.onStartBatch();
+ }
+ }
+ }
+};
+
+/**
+ * Base prototype to be used implementing a provider.
+ *
+ * @see e.g. providers/gdata
+ */
+cal.ProviderBase = function() {
+ cal.ASSERT("This prototype should only be inherited!");
+};
+cal.ProviderBase.mTransientProperties = {
+ "cache.uncachedCalendar": true,
+ "currentStatus": true,
+ "itip.transport": true,
+ "imip.identity": true,
+ "imip.account": true,
+ "imip.identity.disabled": true,
+ "organizerId": true,
+ "organizerCN": true
+};
+cal.ProviderBase.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.calICalendar,
+ Components.interfaces.calISchedulingSupport
+ ]),
+
+ mID: null,
+ mUri: null,
+ mACLEntry: null,
+ mObservers: null,
+ mProperties: null,
+
+ initProviderBase: function() {
+ this.wrappedJSObject = this;
+ this.mObservers = new cal.ObserverBag(Components.interfaces.calIObserver);
+ this.mProperties = {};
+ this.mProperties.currentStatus = Components.results.NS_OK;
+ },
+
+ get observers() {
+ return this.mObservers;
+ },
+
+ // attribute AUTF8String id;
+ get id() {
+ return this.mID;
+ },
+ set id(aValue) {
+ if (this.mID) {
+ throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
+ }
+ this.mID = aValue;
+
+ let calMgr = cal.getCalendarManager();
+
+ // make all properties persistent that have been set so far:
+ for (let aName in this.mProperties) {
+ if (!cal.ProviderBase.mTransientProperties[aName]) {
+ let value = this.mProperties[aName];
+ if (value !== null) {
+ calMgr.setCalendarPref_(this, aName, value);
+ }
+ }
+ }
+
+ let takeOverIfNotPresent = (oldPref, newPref, dontDeleteOldPref) => {
+ let val = calMgr.getCalendarPref_(this, oldPref);
+ if (val !== null) {
+ if (!dontDeleteOldPref) {
+ calMgr.deleteCalendarPref_(this, oldPref);
+ }
+ if (calMgr.getCalendarPref_(this, newPref) === null) {
+ calMgr.setCalendarPref_(this, newPref, val);
+ }
+ }
+ };
+
+ // takeover lightning calendar visibility from 0.5:
+ takeOverIfNotPresent("lightning-main-in-composite", "calendar-main-in-composite");
+ takeOverIfNotPresent("lightning-main-default", "calendar-main-default");
+
+ return aValue;
+ },
+
+ // attribute AUTF8String name;
+ get name() {
+ return this.getProperty("name");
+ },
+ set name(aValue) {
+ return this.setProperty("name", aValue);
+ },
+
+ // readonly attribute calICalendarACLManager aclManager;
+ get aclManager() {
+ const defaultACLProviderClass = "@mozilla.org/calendar/acl-manager;1?type=default";
+ let providerClass = this.getProperty("aclManagerClass");
+ if (!providerClass || !Components.classes[providerClass]) {
+ providerClass = defaultACLProviderClass;
+ }
+ return Components.classes[providerClass].getService(Components.interfaces.calICalendarACLManager);
+ },
+
+ // readonly attribute calICalendarACLEntry aclEntry;
+ get aclEntry() {
+ return this.mACLEntry;
+ },
+
+ // attribute calICalendar superCalendar;
+ get superCalendar() {
+ // If we have a superCalendar, check this calendar for a superCalendar.
+ // This will make sure the topmost calendar is returned
+ return (this.mSuperCalendar ? this.mSuperCalendar.superCalendar : this);
+ },
+ set superCalendar(val) {
+ return (this.mSuperCalendar = val);
+ },
+
+ // attribute nsIURI uri;
+ get uri() {
+ return this.mUri;
+ },
+ set uri(aValue) {
+ return (this.mUri = aValue);
+ },
+
+ // attribute boolean readOnly;
+ get readOnly() {
+ return this.getProperty("readOnly");
+ },
+ set readOnly(aValue) {
+ return this.setProperty("readOnly", aValue);
+ },
+
+ // readonly attribute boolean canRefresh;
+ get canRefresh() {
+ return false;
+ },
+
+ // void startBatch();
+ mBatchCount: 0,
+ startBatch: function() {
+ if (this.mBatchCount++ == 0) {
+ this.mObservers.notify("onStartBatch");
+ }
+ },
+
+ endBatch: function() {
+ if (this.mBatchCount > 0) {
+ if (--this.mBatchCount == 0) {
+ this.mObservers.notify("onEndBatch");
+ }
+ } else {
+ cal.ASSERT(this.mBatchCount > 0, "unexepcted endBatch!");
+ }
+ },
+
+ notifyPureOperationComplete: function(aListener, aStatus, aOperationType, aId, aDetail) {
+ if (aListener) {
+ try {
+ aListener.onOperationComplete(this.superCalendar, aStatus, aOperationType, aId, aDetail);
+ } catch (exc) {
+ cal.ERROR(exc);
+ }
+ }
+ },
+
+ notifyOperationComplete: function(aListener, aStatus, aOperationType, aId, aDetail, aExtraMessage) {
+ this.notifyPureOperationComplete(aListener, aStatus, aOperationType, aId, aDetail);
+
+ if (aStatus == Components.interfaces.calIErrors.OPERATION_CANCELLED) {
+ return; // cancellation doesn't change current status, no notification
+ }
+ if (Components.isSuccessCode(aStatus)) {
+ this.setProperty("currentStatus", aStatus);
+ } else {
+ if (aDetail instanceof Components.interfaces.nsIException) {
+ this.notifyError(aDetail); // will set currentStatus
+ } else {
+ this.notifyError(aStatus, aDetail); // will set currentStatus
+ }
+ this.notifyError(aOperationType == Components.interfaces.calIOperationListener.GET
+ ? Components.interfaces.calIErrors.READ_FAILED
+ : Components.interfaces.calIErrors.MODIFICATION_FAILED,
+ aExtraMessage || "");
+ }
+ },
+
+ // for convenience also callable with just an exception
+ notifyError: function(aErrNo, aMessage) {
+ if (aErrNo == Components.interfaces.calIErrors.OPERATION_CANCELLED) {
+ return; // cancellation doesn't change current status, no notification
+ }
+ if (aErrNo instanceof Components.interfaces.nsIException) {
+ if (!aMessage) {
+ aMessage = aErrNo.message;
+ }
+ aErrNo = aErrNo.result;
+ }
+ this.setProperty("currentStatus", aErrNo);
+ this.observers.notify("onError", [this.superCalendar, aErrNo, aMessage]);
+ },
+
+ mTransientPropertiesMode: false,
+ get transientProperties() {
+ return this.mTransientPropertiesMode;
+ },
+ set transientProperties(value) {
+ return (this.mTransientPropertiesMode = value);
+ },
+
+ // nsIVariant getProperty(in AUTF8String aName);
+ getProperty: function(aName) {
+ switch (aName) {
+ case "itip.transport": // iTIP/iMIP default:
+ return cal.getImipTransport(this);
+ case "itip.notify-replies": // iTIP/iMIP default:
+ return Preferences.get("calendar.itip.notify-replies", false);
+ // temporary hack to get the uncached calendar instance:
+ case "cache.uncachedCalendar":
+ return this;
+ }
+
+ let ret = this.mProperties[aName];
+ if (ret === undefined) {
+ ret = null;
+ switch (aName) {
+ case "imip.identity": // we want to cache the identity object a little, because
+ // it is heavily used by the invitation checks
+ ret = cal.getEmailIdentityOfCalendar(this);
+ break;
+ case "imip.account": {
+ let outAccount = {};
+ if (cal.getEmailIdentityOfCalendar(this, outAccount)) {
+ ret = outAccount.value;
+ }
+ break;
+ }
+ case "organizerId": { // itip/imip default: derived out of imip.identity
+ let identity = this.getProperty("imip.identity");
+ ret = (identity
+ ? ("mailto:" + identity.QueryInterface(Components.interfaces.nsIMsgIdentity).email)
+ : null);
+ break;
+ }
+ case "organizerCN": { // itip/imip default: derived out of imip.identity
+ let identity = this.getProperty("imip.identity");
+ ret = (identity
+ ? identity.QueryInterface(Components.interfaces.nsIMsgIdentity).fullName
+ : null);
+ break;
+ }
+ }
+ if ((ret === null) &&
+ !cal.ProviderBase.mTransientProperties[aName] &&
+ !this.transientProperties) {
+ if (this.id) {
+ ret = cal.getCalendarManager().getCalendarPref_(this, aName);
+ }
+ switch (aName) {
+ case "suppressAlarms":
+ if (this.getProperty("capabilities.alarms.popup.supported") === false) {
+ // If popup alarms are not supported,
+ // automatically suppress alarms
+ ret = true;
+ }
+ break;
+ }
+ }
+ this.mProperties[aName] = ret;
+ }
+// cal.LOG("getProperty(\"" + aName + "\"): " + ret);
+ return ret;
+ },
+
+ // void setProperty(in AUTF8String aName, in nsIVariant aValue);
+ setProperty: function(aName, aValue) {
+ let oldValue = this.getProperty(aName);
+ if (oldValue != aValue) {
+ this.mProperties[aName] = aValue;
+ switch (aName) {
+ case "imip.identity.key": // invalidate identity and account object if key is set:
+ delete this.mProperties["imip.identity"];
+ delete this.mProperties["imip.account"];
+ delete this.mProperties.organizerId;
+ delete this.mProperties.organizerCN;
+ break;
+ }
+ if (!this.transientProperties &&
+ !cal.ProviderBase.mTransientProperties[aName] &&
+ this.id) {
+ cal.getCalendarManager().setCalendarPref_(this, aName, aValue);
+ }
+ this.mObservers.notify("onPropertyChanged",
+ [this.superCalendar, aName, aValue, oldValue]);
+ }
+ return aValue;
+ },
+
+ // void deleteProperty(in AUTF8String aName);
+ deleteProperty: function(aName) {
+ this.mObservers.notify("onPropertyDeleting", [this.superCalendar, aName]);
+ delete this.mProperties[aName];
+ cal.getCalendarManager().deleteCalendarPref_(this, aName);
+ },
+
+ // calIOperation refresh
+ refresh: function() {
+ return null;
+ },
+
+ // void addObserver( in calIObserver observer );
+ addObserver: function(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+
+ // void removeObserver( in calIObserver observer );
+ removeObserver: function(aObserver) {
+ this.mObservers.remove(aObserver);
+ },
+
+ // calISchedulingSupport: Implementation corresponding to our iTIP/iMIP support
+ isInvitation: function(aItem) {
+ if (!this.mACLEntry || !this.mACLEntry.hasAccessControl) {
+ // No ACL support - fallback to the old method
+ let id = this.getProperty("organizerId");
+ if (id) {
+ let org = aItem.organizer;
+ if (!org || !org.id || (org.id.toLowerCase() == id.toLowerCase())) {
+ return false;
+ }
+ return (aItem.getAttendeeById(id) != null);
+ }
+ return false;
+ }
+
+ let org = aItem.organizer;
+ if (!org || !org.id) {
+ // HACK
+ // if we don't have an organizer, this is perhaps because it's an exception
+ // to a recurring event. We check the parent item.
+ if (aItem.parentItem) {
+ org = aItem.parentItem.organizer;
+ if (!org || !org.id) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ // We check if :
+ // - the organizer of the event is NOT within the owner's identities of this calendar
+ // - if the one of the owner's identities of this calendar is in the attendees
+ let ownerIdentities = this.mACLEntry.getOwnerIdentities({});
+ for (let i = 0; i < ownerIdentities.length; i++) {
+ let identity = "mailto:" + ownerIdentities[i].email.toLowerCase();
+ if (org.id.toLowerCase() == identity) {
+ return false;
+ }
+
+ if (aItem.getAttendeeById(identity) != null) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ getInvitedAttendee: function(aItem) {
+ let id = this.getProperty("organizerId");
+ let attendee = (id ? aItem.getAttendeeById(id) : null);
+
+ if (!attendee && this.mACLEntry && this.mACLEntry.hasAccessControl) {
+ let ownerIdentities = this.mACLEntry.getOwnerIdentities({});
+ if (ownerIdentities.length > 0) {
+ let identity;
+ for (let i = 0; !attendee && i < ownerIdentities.length; i++) {
+ identity = "mailto:" + ownerIdentities[i].email.toLowerCase();
+ attendee = aItem.getAttendeeById(identity);
+ }
+ }
+ }
+
+ return attendee;
+ },
+
+ canNotify: function(aMethod, aItem) {
+ return false; // use outbound iTIP for all
+ }
+};
diff --git a/calendar/base/modules/calRecurrenceUtils.jsm b/calendar/base/modules/calRecurrenceUtils.jsm
new file mode 100644
index 000000000..ac7cf5fdb
--- /dev/null
+++ b/calendar/base/modules/calRecurrenceUtils.jsm
@@ -0,0 +1,405 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported recurrenceRule2String, splitRecurrenceRules, checkRecurrenceRule */
+
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["recurrenceRule2String", "splitRecurrenceRules", "checkRecurrenceRule"];
+
+/**
+ * This function takes the recurrence info passed as argument and creates a
+ * literal string representing the repeat pattern in natural language.
+ *
+ * @param recurrenceInfo An item's recurrence info to parse.
+ * @param startDate The start date to base rules on.
+ * @param endDate The end date to base rules on.
+ * @param allDay If true, the pattern should assume an allday item.
+ * @return A human readable string describing the recurrence.
+ */
+function recurrenceRule2String(recurrenceInfo, startDate, endDate, allDay) {
+ function getRString(name, args) {
+ return cal.calGetString("calendar-event-dialog", name, args);
+ }
+ function day_of_week(day) {
+ return Math.abs(day) % 8;
+ }
+ function day_position(day) {
+ return (Math.abs(day) - day_of_week(day)) / 8 * (day < 0 ? -1 : 1);
+ }
+ function nounClass(aDayString, aRuleString) {
+ // Select noun class (grammatical gender) for rule string
+ let nounClassStr = getRString(aDayString + "Nounclass");
+ return aRuleString + nounClassStr.substr(0, 1).toUpperCase() +
+ nounClassStr.substr(1);
+ }
+ function pluralWeekday(aDayString) {
+ let plural = getRString("pluralForWeekdays") == "true";
+ return (plural ? aDayString + "Plural" : aDayString);
+ }
+ function everyWeekDay(aByDay) {
+ // Checks if aByDay contains only values from 1 to 7 with any order.
+ let mask = aByDay.reduce((value, item) => value | (1 << item), 1);
+ return aByDay.length == 7 && mask == Math.pow(2, 8) - 1;
+ }
+
+
+ // Retrieve a valid recurrence rule from the currently
+ // set recurrence info. Bail out if there's more
+ // than a single rule or something other than a rule.
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ if (rrules[0].length == 1) {
+ let rule = cal.wrapInstance(rrules[0][0], Components.interfaces.calIRecurrenceRule);
+ // Currently we allow only for BYDAY, BYMONTHDAY, BYMONTH rules.
+ if (rule &&
+ !checkRecurrenceRule(rule, ["BYSECOND",
+ "BYMINUTE",
+ // "BYDAY",
+ "BYHOUR",
+ // "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ // "BYMONTH",
+ "BYSETPOS"])) {
+ let dateFormatter = cal.getDateFormatter();
+ let ruleString;
+ if (rule.type == "DAILY") {
+ if (checkRecurrenceRule(rule, ["BYDAY"])) {
+ let days = rule.getComponent("BYDAY", {});
+ let weekdays = [2, 3, 4, 5, 6];
+ if (weekdays.length == days.length) {
+ let i;
+ for (i = 0; i < weekdays.length; i++) {
+ if (weekdays[i] != days[i]) {
+ break;
+ }
+ }
+ if (i == weekdays.length) {
+ ruleString = getRString("repeatDetailsRuleDaily4");
+ }
+ } else {
+ return null;
+ }
+ } else {
+ let dailyString = getRString("dailyEveryNth");
+ ruleString = PluralForm.get(rule.interval, dailyString)
+ .replace("#1", rule.interval);
+ }
+ } else if (rule.type == "WEEKLY") {
+ // weekly recurrence, currently we
+ // support a single 'BYDAY'-rule only.
+ if (checkRecurrenceRule(rule, ["BYDAY"])) {
+ // create a string like 'Monday, Tuesday and Wednesday'
+ let days = rule.getComponent("BYDAY", {});
+ let weekdays = "";
+ // select noun class (grammatical gender) according to the
+ // first day of the list
+ let weeklyString = nounClass("repeatDetailsDay" + days[0], "weeklyNthOn");
+ for (let i = 0; i < days.length; i++) {
+ if (rule.interval == 1) {
+ weekdays += getRString(pluralWeekday("repeatDetailsDay" + days[i]));
+ } else {
+ weekdays += getRString("repeatDetailsDay" + days[i]);
+ }
+ if (days.length > 1 && i == (days.length - 2)) {
+ weekdays += " " + getRString("repeatDetailsAnd") + " ";
+ } else if (i < days.length - 1) {
+ weekdays += ", ";
+ }
+ }
+
+ weeklyString = getRString(weeklyString, [weekdays]);
+ ruleString = PluralForm.get(rule.interval, weeklyString)
+ .replace("#2", rule.interval);
+ } else {
+ let weeklyString = getRString("weeklyEveryNth");
+ ruleString = PluralForm.get(rule.interval, weeklyString)
+ .replace("#1", rule.interval);
+ }
+ } else if (rule.type == "MONTHLY") {
+ if (checkRecurrenceRule(rule, ["BYDAY"])) {
+ let byday = rule.getComponent("BYDAY", {});
+ if (everyWeekDay(byday)) {
+ // Rule every day of the month.
+ ruleString = getRString("monthlyEveryDayOfNth");
+ ruleString = PluralForm.get(rule.interval, ruleString)
+ .replace("#2", rule.interval);
+ } else {
+ // For rules with generic number of weekdays with and
+ // without "position" prefix we build two separate
+ // strings depending on the position and then join them.
+ // Notice: we build the description string but currently
+ // the UI can manage only rules with only one weekday.
+ let weekdaysString_every = "";
+ let weekdaysString_position = "";
+ let firstDay = byday[0];
+ for (let i = 0; i < byday.length; i++) {
+ if (day_position(byday[i]) == 0) {
+ if (!weekdaysString_every) {
+ firstDay = byday[i];
+ }
+ weekdaysString_every += getRString(pluralWeekday("repeatDetailsDay" + byday[i])) + ", ";
+ } else {
+ if (day_position(byday[i]) < -1 || day_position(byday[i]) > 5) {
+ // We support only weekdays with -1 as negative
+ // position ('THE LAST ...').
+ return null;
+ }
+
+ let duplicateWeekday = byday.some((element) => {
+ return (day_position(element) == 0 &&
+ day_of_week(byday[i]) == day_of_week(element));
+ });
+ if (duplicateWeekday) {
+ // Prevent to build strings such as for example:
+ // "every Monday and the second Monday...".
+ continue;
+ }
+
+ let ordinalString = "repeatOrdinal" + day_position(byday[i]);
+ let dayString = "repeatDetailsDay" + day_of_week(byday[i]);
+ ordinalString = nounClass(dayString, ordinalString);
+ ordinalString = getRString(ordinalString);
+ dayString = getRString(dayString);
+ let stringOrdinalWeekday = getRString("ordinalWeekdayOrder",
+ [ordinalString, dayString]);
+ weekdaysString_position += stringOrdinalWeekday + ", ";
+ }
+ }
+ let weekdaysString = weekdaysString_every + weekdaysString_position;
+ weekdaysString = weekdaysString.slice(0, -2)
+ .replace(/,(?= [^,]*$)/,
+ " " + getRString("repeatDetailsAnd"));
+
+ let monthlyString = weekdaysString_every ? "monthlyEveryOfEvery" : "monthlyRuleNthOfEvery";
+ monthlyString = nounClass("repeatDetailsDay" + day_of_week(firstDay), monthlyString);
+ monthlyString = getRString(monthlyString, [weekdaysString]);
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#2", rule.interval);
+ }
+ } else if (checkRecurrenceRule(rule, ["BYMONTHDAY"])) {
+ let component = rule.getComponent("BYMONTHDAY", {});
+
+ // First, find out if the 'BYMONTHDAY' component contains
+ // any elements with a negative value lesser than -1 ("the
+ // last day"). If so we currently don't support any rule
+ if (component.some(element => element < -1)) {
+ // we don't support any other combination for now...
+ return getRString("ruleTooComplex");
+ } else if (component.length == 1 && component[0] == -1) {
+ // i.e. one day, the last day of the month
+ let monthlyString = getRString("monthlyLastDayOfNth");
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#1", rule.interval);
+ } else {
+ // i.e. one or more monthdays every N months.
+
+ // Build a string with a list of days separated with commas.
+ let day_string = "";
+ let lastDay = false;
+ for (let i = 0; i < component.length; i++) {
+ if (component[i] == -1) {
+ lastDay = true;
+ continue;
+ }
+ day_string += dateFormatter.formatDayWithOrdinal(component[i]) + ", ";
+ }
+ if (lastDay) {
+ day_string += getRString("monthlyLastDay") + ", ";
+ }
+ day_string = day_string.slice(0, -2)
+ .replace(/,(?= [^,]*$)/,
+ " " + getRString("repeatDetailsAnd"));
+
+ // Add the word "day" in plural form to the list of days then
+ // compose the final string with the interval of months
+ let monthlyDayString = getRString("monthlyDaysOfNth_day", [day_string]);
+ monthlyDayString = PluralForm.get(component.length, monthlyDayString);
+ let monthlyString = getRString("monthlyDaysOfNth", [monthlyDayString]);
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#2", rule.interval);
+ }
+ } else {
+ let monthlyString = getRString("monthlyDaysOfNth", [startDate.day]);
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#2", rule.interval);
+ }
+ } else if (rule.type == "YEARLY") {
+ let bymonthday = null;
+ let bymonth = null;
+ if (checkRecurrenceRule(rule, ["BYMONTHDAY"])) {
+ bymonthday = rule.getComponent("BYMONTHDAY", {});
+ }
+ if (checkRecurrenceRule(rule, ["BYMONTH"])) {
+ bymonth = rule.getComponent("BYMONTH", {});
+ }
+ if ((bymonth && bymonth.length > 1) ||
+ (bymonthday && (bymonthday.length > 1 || bymonthday[0] < -1))) {
+ // Don't build a string for a recurrence rule that the UI
+ // currently can't show completely (with more than one month
+ // or than one monthday, or bymonthdays lesser than -1).
+ return getRString("ruleTooComplex");
+ }
+
+ if (checkRecurrenceRule(rule, ["BYMONTHDAY"]) &&
+ (checkRecurrenceRule(rule, ["BYMONTH"]) || !checkRecurrenceRule(rule, ["BYDAY"]))) {
+ // RRULE:FREQ=YEARLY;BYMONTH=x;BYMONTHDAY=y.
+ // RRULE:FREQ=YEARLY;BYMONTHDAY=x (takes the month from the start date).
+ let monthNumber = bymonth ? bymonth[0] : (startDate.month + 1);
+ let month = getRString("repeatDetailsMonth" + monthNumber);
+ let monthDay = bymonthday[0] == -1 ? getRString("monthlyLastDay")
+ : dateFormatter.formatDayWithOrdinal(bymonthday[0]);
+ let yearlyString = getRString("yearlyNthOn", [month, monthDay]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ } else if (checkRecurrenceRule(rule, ["BYMONTH"]) &&
+ checkRecurrenceRule(rule, ["BYDAY"])) {
+ // RRULE:FREQ=YEARLY;BYMONTH=x;BYDAY=y1,y2,....
+ let byday = rule.getComponent("BYDAY", {});
+ let month = getRString("repeatDetailsMonth" + bymonth[0]);
+ if (everyWeekDay(byday)) {
+ // Every day of the month.
+ let yearlyString = "yearlyEveryDayOf";
+ yearlyString = getRString(yearlyString, [month]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#2", rule.interval);
+ } else if (byday.length == 1) {
+ let dayString = "repeatDetailsDay" + day_of_week(byday[0]);
+ if (day_position(byday[0]) == 0) {
+ // Every any weekday.
+ let yearlyString = "yearlyOnEveryNthOfNth";
+ yearlyString = nounClass(dayString, yearlyString);
+ let day = getRString(pluralWeekday(dayString));
+ yearlyString = getRString(yearlyString, [day, month]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ } else if (day_position(byday[0]) >= -1 ||
+ day_position(byday[0]) <= 5) {
+ // The first|the second|...|the last Monday, Tuesday, ..., day.
+ let yearlyString = "yearlyNthOnNthOf";
+ yearlyString = nounClass(dayString, yearlyString);
+ let ordinalString = "repeatOrdinal" + day_position(byday[0]);
+ ordinalString = nounClass(dayString, ordinalString);
+ let ordinal = getRString(ordinalString);
+ let day = getRString(dayString);
+ yearlyString = getRString(yearlyString, [ordinal, day, month]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#4", rule.interval);
+ } else {
+ return getRString("ruleTooComplex");
+ }
+ } else {
+ // Currently we don't support yearly rules with
+ // more than one BYDAY element or exactly 7 elements
+ // with all the weekdays (the "every day" case).
+ return getRString("ruleTooComplex");
+ }
+ } else if (checkRecurrenceRule(rule, ["BYMONTH"])) {
+ // RRULE:FREQ=YEARLY;BYMONTH=x (takes the day from the start date).
+ let month = getRString("repeatDetailsMonth" + bymonth[0]);
+ let yearlyString = getRString("yearlyNthOn", [month, startDate.day]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ } else {
+ let month = getRString("repeatDetailsMonth" + (startDate.month + 1));
+ let yearlyString = getRString("yearlyNthOn", [month, startDate.day]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ }
+ }
+
+ let kDefaultTimezone = cal.calendarDefaultTimezone();
+
+ let detailsString;
+ if (!endDate || allDay) {
+ if (rule.isFinite) {
+ if (rule.isByCount) {
+ let countString = getRString("repeatCountAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate)]);
+ detailsString = PluralForm.get(rule.count, countString)
+ .replace("#3", rule.count);
+ } else {
+ let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
+ detailsString = getRString("repeatDetailsUntilAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatDateShort(untilDate)]);
+ }
+ } else {
+ detailsString = getRString("repeatDetailsInfiniteAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate)]);
+ }
+ } else if (rule.isFinite) {
+ if (rule.isByCount) {
+ let countString = getRString("repeatCount",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate)]);
+ detailsString = PluralForm.get(rule.count, countString)
+ .replace("#5", rule.count);
+ } else {
+ let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
+ detailsString = getRString("repeatDetailsUntil",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatDateShort(untilDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate)]);
+ }
+ } else {
+ detailsString = getRString("repeatDetailsInfinite",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate)]);
+ }
+ return detailsString;
+ }
+ }
+ return null;
+}
+
+/**
+ * Split rules into negative and positive rules.
+ *
+ * @param recurrenceInfo An item's recurrence info to parse.
+ * @return An array with two elements: an array of positive
+ * rules and an array of negative rules.
+ */
+function splitRecurrenceRules(recurrenceInfo) {
+ let ritems = recurrenceInfo.getRecurrenceItems({});
+ let rules = [];
+ let exceptions = [];
+ for (let ritem of ritems) {
+ if (ritem.isNegative) {
+ exceptions.push(ritem);
+ } else {
+ rules.push(ritem);
+ }
+ }
+ return [rules, exceptions];
+}
+
+/**
+ * Check if a recurrence rule's component is valid.
+ *
+ * @see calIRecurrenceRule
+ * @param aRule The recurrence rule to check.
+ * @param aArray An array of component names to check.
+ * @return Returns true if the rule is valid.
+ */
+function checkRecurrenceRule(aRule, aArray) {
+ for (let comp of aArray) {
+ let ruleComp = aRule.getComponent(comp, {});
+ if (ruleComp && ruleComp.length > 0) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/calendar/base/modules/calUtils.jsm b/calendar/base/modules/calUtils.jsm
new file mode 100644
index 000000000..84216089e
--- /dev/null
+++ b/calendar/base/modules/calUtils.jsm
@@ -0,0 +1,1001 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// New code must not load/import calUtils.js, but should use calUtils.jsm.
+
+var gCalThreadingEnabled;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+// Usually the backend loader gets loaded via profile-after-change, but in case
+// a calendar component hooks in earlier, its very likely it will use calUtils.
+// Getting the service here will load if its not already loaded
+Components.classes["@mozilla.org/calendar/backend-loader;1"].getService();
+
+this.EXPORTED_SYMBOLS = ["cal"];
+var cal = {
+ // new code should land here,
+ // and more code should be moved from calUtils.js into this object to avoid
+ // clashes with other extensions
+
+ getDragService: generateServiceAccessor("@mozilla.org/widget/dragservice;1",
+ Components.interfaces.nsIDragService),
+
+ /**
+ * Loads an array of calendar scripts into the passed scope.
+ *
+ * @param scriptNames an array of calendar script names
+ * @param scope scope to load into
+ * @param baseDir base dir; defaults to calendar-js/
+ */
+ loadScripts: function(scriptNames, scope, baseDir) {
+ if (!baseDir) {
+ baseDir = __LOCATION__.parent.parent.clone();
+ baseDir.append("calendar-js");
+ }
+
+ for (let script of scriptNames) {
+ if (!script) {
+ // If the array element is null, then just skip this script.
+ continue;
+ }
+ let scriptFile = baseDir.clone();
+ scriptFile.append(script);
+ let scriptUrlSpec = Services.io.newFileURI(scriptFile).spec;
+ try {
+ Services.scriptloader.loadSubScript(scriptUrlSpec, scope);
+ } catch (exc) {
+ Components.utils.reportError(exc + " (" + scriptUrlSpec + ")");
+ }
+ }
+ },
+
+ loadingNSGetFactory: function(scriptNames, components, scope) {
+ return function(cid) {
+ if (!this.inner) {
+ let global = Components.utils.getGlobalForObject(scope);
+ cal.loadScripts(scriptNames, global);
+ if (typeof components == "function") {
+ components = components.call(global);
+ }
+ this.inner = XPCOMUtils.generateNSGetFactory(components);
+ }
+ return this.inner(cid);
+ };
+ },
+
+ /**
+ * Schedules execution of the passed function to the current thread's queue.
+ */
+ postPone: function(func) {
+ if (this.threadingEnabled) {
+ Services.tm.currentThread.dispatch({ run: func },
+ Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
+ } else {
+ func();
+ }
+ },
+
+ /**
+ * Create an adapter for the given interface. If passed, methods will be
+ * added to the template object, otherwise a new object will be returned.
+ *
+ * @param iface The interface to adapt, either using
+ * Components.interfaces or the name as a string.
+ * @param template (optional) A template object to extend
+ * @return If passed the adapted template object, otherwise a
+ * clean adapter.
+ *
+ * Currently supported interfaces are:
+ * - calIObserver
+ * - calICalendarManagerObserver
+ * - calIOperationListener
+ * - calICompositeObserver
+ */
+ createAdapter: function(iface, template) {
+ let methods;
+ let adapter = template || {};
+ switch (iface.name || iface) {
+ case "calIObserver":
+ methods = ["onStartBatch", "onEndBatch", "onLoad", "onAddItem",
+ "onModifyItem", "onDeleteItem", "onError",
+ "onPropertyChanged", "onPropertyDeleting"];
+ break;
+ case "calICalendarManagerObserver":
+ methods = ["onCalendarRegistered", "onCalendarUnregistering",
+ "onCalendarDeleting"];
+ break;
+ case "calIOperationListener":
+ methods = ["onGetResult", "onOperationComplete"];
+ break;
+ case "calICompositeObserver":
+ methods = ["onCalendarAdded", "onCalendarRemoved",
+ "onDefaultCalendarChanged"];
+ break;
+ default:
+ methods = [];
+ break;
+ }
+
+ for (let method of methods) {
+ if (!(method in template)) {
+ adapter[method] = function() {};
+ }
+ }
+ adapter.QueryInterface = XPCOMUtils.generateQI([iface]);
+
+ return adapter;
+ },
+
+ get threadingEnabled() {
+ if (gCalThreadingEnabled === undefined) {
+ gCalThreadingEnabled = !Preferences.get("calendar.threading.disabled", false);
+ }
+ return gCalThreadingEnabled;
+ },
+
+ /*
+ * Checks whether a calendar supports events
+ *
+ * @param aCalendar
+ */
+ isEventCalendar: function(aCalendar) {
+ return (aCalendar.getProperty("capabilities.events.supported") !== false);
+ },
+
+ /*
+ * Checks whether a calendar supports tasks
+ *
+ * @param aCalendar
+ */
+ isTaskCalendar: function(aCalendar) {
+ return (aCalendar.getProperty("capabilities.tasks.supported") !== false);
+ },
+
+ /**
+ * Checks whether a timezone lacks a definition.
+ */
+ isPhantomTimezone: function(timezone) {
+ return (!timezone.icalComponent && !timezone.isUTC && !timezone.isFloating);
+ },
+
+ /**
+ * Shifts an item by the given timely offset.
+ *
+ * @param item an item
+ * @param offset an offset (calIDuration)
+ */
+ shiftItem: function(item, offset) {
+ // When modifying dates explicitly using the setters is important
+ // since those may triggers e.g. calIRecurrenceInfo::onStartDateChange
+ // or invalidate other properties. Moreover don't modify the date-time objects
+ // without cloning, because changes cannot be calculated if doing so.
+ if (cal.isEvent(item)) {
+ let date = item.startDate.clone();
+ date.addDuration(offset);
+ item.startDate = date;
+ date = item.endDate.clone();
+ date.addDuration(offset);
+ item.endDate = date;
+ } else /* isToDo */ {
+ if (item.entryDate) {
+ let date = item.entryDate.clone();
+ date.addDuration(offset);
+ item.entryDate = date;
+ }
+ if (item.dueDate) {
+ let date = item.dueDate.clone();
+ date.addDuration(offset);
+ item.dueDate = date;
+ }
+ }
+ },
+
+ /**
+ * Returns a copy of an event that
+ * - has a relation set to the original event
+ * - has the same organizer but
+ * - has any attendee removed
+ * Intended to get a copy of a normal event invitation that behaves as if the PUBLISH method
+ * was chosen instead.
+ *
+ * @param aItem original item
+ * @param aUid (optional) UID to use for the new item
+ */
+ getPublishLikeItemCopy: function(aItem, aUid) {
+ // avoid changing aItem
+ let item = aItem.clone();
+ // reset to a new UUID if applicable
+ item.id = aUid || cal.getUUID();
+ // add a relation to the original item
+ let relation = cal.createRelation();
+ relation.relId = aItem.id;
+ relation.relType = "SIBLING";
+ item.addRelation(relation);
+ // remove attendees
+ item.removeAllAttendees();
+ if (!aItem.isMutable) {
+ item = item.makeImmutable();
+ }
+ return item;
+ },
+
+ /**
+ * Shortcut function to serialize an item (including all overridden items).
+ */
+ getSerializedItem: function(aItem) {
+ let serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
+ .createInstance(Components.interfaces.calIIcsSerializer);
+ serializer.addItems([aItem], 1);
+ return serializer.serializeToString();
+ },
+
+ /**
+ * Shortcut function to check whether an item is an invitation copy.
+ */
+ isInvitation: function(aItem) {
+ let isInvitation = false;
+ let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
+ if (calendar) {
+ isInvitation = calendar.isInvitation(aItem);
+ }
+ return isInvitation;
+ },
+
+ /**
+ * Returns a basically checked recipient list - malformed elements will be removed
+ *
+ * @param string aRecipients a comma-seperated list of e-mail addresses
+ * @return string a comma-seperated list of e-mail addresses
+ */
+ validateRecipientList: function(aRecipients) {
+ let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Components.interfaces.nsIMsgCompFields);
+ // Resolve the list considering also configured common names
+ let members = compFields.splitRecipients(aRecipients, false, {});
+ let list = [];
+ let prefix = "";
+ for (let member of members) {
+ if (prefix != "") {
+ // the previous member had no email address - this happens if a recipients CN
+ // contains a ',' or ';' (splitRecipients(..) behaves wrongly here and produces an
+ // additional member with only the first CN part of that recipient and no email
+ // address while the next has the second part of the CN and the according email
+ // address) - we still need to identify the original delimiter to append it to the
+ // prefix
+ let memberCnPart = member.match(/(.*) <.*>/);
+ if (memberCnPart) {
+ let pattern = new RegExp(prefix + "([;,] *)" + memberCnPart[1]);
+ let delimiter = aRecipients.match(pattern);
+ if (delimiter) {
+ prefix = prefix + delimiter[1];
+ }
+ }
+ }
+ let parts = (prefix + member).match(/(.*)( <.*>)/);
+ if (parts) {
+ if (parts[2] == " <>") {
+ // CN but no email address - we keep the CN part to prefix the next member's CN
+ prefix = parts[1];
+ } else {
+ // CN with email address
+ let commonName = parts[1].trim();
+ // in case of any special characters in the CN string, we make sure to enclose
+ // it with dquotes - simple spaces don't require dquotes
+ if (commonName.match(/[\-\[\]{}()*+?.,;\\\^$|#\f\n\r\t\v]/)) {
+ commonName = '"' + commonName.replace(/\\"|"/, "").trim() + '"';
+ }
+ list.push(commonName + parts[2]);
+ prefix = "";
+ }
+ } else if (member.length) {
+ // email address only
+ list.push(member);
+ prefix = "";
+ }
+ }
+ return list.join(", ");
+ },
+
+ /**
+ * Shortcut function to check whether an item is an invitation copy and
+ * has a participation status of either NEEDS-ACTION or TENTATIVE.
+ *
+ * @param aItem either calIAttendee or calIItemBase
+ */
+ isOpenInvitation: function(aItem) {
+ let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
+ if (!wrappedItem) {
+ aItem = cal.getInvitedAttendee(aItem);
+ }
+ if (aItem) {
+ switch (aItem.participationStatus) {
+ case "NEEDS-ACTION":
+ case "TENTATIVE":
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Prepends a mailto: prefix to an email address like string
+ *
+ * @param {string} the string to prepend the prefix if not already there
+ * @return {string} the string with prefix
+ */
+ prependMailTo: function(aId) {
+ return aId.replace(/^(?:mailto:)?(.*)@/i, "mailto:$1@");
+ },
+
+ /**
+ * Removes an existing mailto: prefix from an attendee id
+ *
+ * @param {string} the string to remove the prefix from if any
+ * @return {string} the string without prefix
+ */
+ removeMailTo: function(aId) {
+ return aId.replace(/^mailto:/i, "");
+ },
+
+ /**
+ * Resolves delegated-to/delegated-from calusers for a given attendee to also include the
+ * respective CNs if available in a given set of attendees
+ *
+ * @param aAttendee {calIAttendee} The attendee to resolve the delegation information for
+ * @param aAttendees {Array} An array of calIAttendee objects to look up
+ * @return {Object} An object with string attributes for delegators and delegatees
+ */
+ resolveDelegation: function(aAttendee, aAttendees) {
+ let attendees = aAttendees || [aAttendee];
+
+ // this will be replaced by a direct property getter in calIAttendee
+ let delegators = [];
+ let delegatees = [];
+ let delegatorProp = aAttendee.getProperty("DELEGATED-FROM");
+ if (delegatorProp) {
+ delegators = typeof delegatorProp == "string" ? [delegatorProp] : delegatorProp;
+ }
+ let delegateeProp = aAttendee.getProperty("DELEGATED-TO");
+ if (delegateeProp) {
+ delegatees = typeof delegateeProp == "string" ? [delegateeProp] : delegateeProp;
+ }
+
+ for (let att of attendees) {
+ let resolveDelegation = function(e, i, a) {
+ if (e == att.id) {
+ a[i] = att.toString();
+ }
+ };
+ delegators.forEach(resolveDelegation);
+ delegatees.forEach(resolveDelegation);
+ }
+ return {
+ delegatees: delegatees.join(", "),
+ delegators: delegators.join(", ")
+ };
+ },
+
+ /**
+ * Shortcut function to get the invited attendee of an item.
+ */
+ getInvitedAttendee: function(aItem, aCalendar) {
+ if (!aCalendar) {
+ aCalendar = aItem.calendar;
+ }
+ let invitedAttendee = null;
+ let calendar = cal.wrapInstance(aCalendar, Components.interfaces.calISchedulingSupport);
+ if (calendar) {
+ invitedAttendee = calendar.getInvitedAttendee(aItem);
+ }
+ return invitedAttendee;
+ },
+
+ /**
+ * Returns all attendees from given set of attendees matching based on the attendee id
+ * or a sent-by parameter compared to the specified email address
+ *
+ * @param {Array} aAttendees An array of calIAttendee objects
+ * @param {String} aEmailAddress A string containing the email address for lookup
+ * @return {Array} Returns an array of matching attendees
+ */
+ getAttendeesBySender: function(aAttendees, aEmailAddress) {
+ let attendees = [];
+ // we extract the email address to make it work also for a raw header value
+ let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Components.interfaces.nsIMsgCompFields);
+ let addresses = compFields.splitRecipients(aEmailAddress, true, {});
+ if (addresses.length == 1) {
+ let searchFor = cal.prependMailTo(addresses[0]);
+ aAttendees.forEach(aAttendee => {
+ if ([aAttendee.id, aAttendee.getProperty("SENT-BY")].includes(searchFor)) {
+ attendees.push(aAttendee);
+ }
+ });
+ } else {
+ cal.WARN("No unique email address for lookup!");
+ }
+ return attendees;
+ },
+
+ /**
+ * Returns a wellformed email string like 'attendee@example.net',
+ * 'Common Name <attendee@example.net>' or '"Name, Common" <attendee@example.net>'
+ *
+ * @param {calIAttendee} aAttendee - the attendee to check
+ * @param {boolean} aIncludeCn - whether or not to return also the CN if available
+ * @return {string} valid email string or an empty string in case of error
+ */
+ getAttendeeEmail: function(aAttendee, aIncludeCn) {
+ // If the recipient id is of type urn, we need to figure out the email address, otherwise
+ // we fall back to the attendee id
+ let email = aAttendee.id.match(/^urn:/i) ? aAttendee.getProperty("EMAIL") || "" : aAttendee.id;
+ // Strip leading "mailto:" if it exists.
+ email = email.replace(/^mailto:/i, "");
+ // We add the CN if requested and available
+ let commonName = aAttendee.commonName;
+ if (aIncludeCn && email.length > 0 && commonName && commonName.length > 0) {
+ if (commonName.match(/[,;]/)) {
+ commonName = '"' + commonName + '"';
+ }
+ commonName = commonName + " <" + email + ">";
+ if (cal.validateRecipientList(commonName) == commonName) {
+ email = commonName;
+ }
+ }
+ return email;
+ },
+
+ /**
+ * Provides a string to use in email "to" header for given attendees
+ *
+ * @param {array} aAttendees - array of calIAttendee's to check
+ * @return {string} Valid string to use in a 'to' header of an email
+ */
+ getRecipientList: function(aAttendees) {
+ let cbEmail = function(aVal, aInd, aArr) {
+ let email = cal.getAttendeeEmail(aVal, true);
+ if (!email.length) {
+ cal.LOG("Dropping invalid recipient for email transport: " + aVal.toString());
+ }
+ return email;
+ };
+ return aAttendees.map(cbEmail)
+ .filter(aVal => aVal.length > 0)
+ .join(", ");
+ },
+
+ /**
+ * Returns the default transparency to apply for an event depending on whether its an all-day event
+ *
+ * @param aIsAllDay If true, the default transparency for all-day events is returned
+ */
+ getEventDefaultTransparency: function(aIsAllDay) {
+ let transp = null;
+ if (aIsAllDay) {
+ transp = Preferences.get("calendar.events.defaultTransparency.allday.transparent", false)
+ ? "TRANSPARENT"
+ : "OPAQUE";
+ } else {
+ transp = Preferences.get("calendar.events.defaultTransparency.standard.transparent", false)
+ ? "TRANSPARENT"
+ : "OPAQUE";
+ }
+ return transp;
+ },
+
+ // The below functions will move to some different place once the
+ // unifinder tress are consolidated.
+
+ compareNativeTime: function(a, b) {
+ if (a < b) {
+ return -1;
+ } else if (a > b) {
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ compareNativeTimeFilledAsc: function(a, b) {
+ if (a == b) {
+ return 0;
+ }
+
+ // In this filter, a zero time (not set) is always at the end.
+ if (a == -62168601600000000) { // value for (0000/00/00 00:00:00)
+ return 1;
+ }
+ if (b == -62168601600000000) { // value for (0000/00/00 00:00:00)
+ return -1;
+ }
+
+ return (a < b ? -1 : 1);
+ },
+
+ compareNativeTimeFilledDesc: function(a, b) {
+ if (a == b) {
+ return 0;
+ }
+
+ // In this filter, a zero time (not set) is always at the end.
+ if (a == -62168601600000000) { // value for (0000/00/00 00:00:00)
+ return 1;
+ }
+ if (b == -62168601600000000) { // value for (0000/00/00 00:00:00)
+ return -1;
+ }
+
+ return (a < b ? 1 : -1);
+ },
+
+ compareNumber: function(a, b) {
+ a = Number(a);
+ b = Number(b);
+ if (a < b) {
+ return -1;
+ } else if (a > b) {
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ sortEntryComparer: function(sortType, modifier) {
+ switch (sortType) {
+ case "number":
+ return function(sortEntryA, sortEntryB) {
+ let nsA = cal.sortEntryKey(sortEntryA);
+ let nsB = cal.sortEntryKey(sortEntryB);
+ return cal.compareNumber(nsA, nsB) * modifier;
+ };
+ case "date":
+ return function(sortEntryA, sortEntryB) {
+ let nsA = cal.sortEntryKey(sortEntryA);
+ let nsB = cal.sortEntryKey(sortEntryB);
+ return cal.compareNativeTime(nsA, nsB) * modifier;
+ };
+ case "date_filled":
+ return function(sortEntryA, sortEntryB) {
+ let nsA = cal.sortEntryKey(sortEntryA);
+ let nsB = cal.sortEntryKey(sortEntryB);
+ if (modifier == 1) {
+ return cal.compareNativeTimeFilledAsc(nsA, nsB);
+ } else {
+ return cal.compareNativeTimeFilledDesc(nsA, nsB);
+ }
+ };
+ case "string":
+ return function(sortEntryA, sortEntryB) {
+ let seA = cal.sortEntryKey(sortEntryA);
+ let seB = cal.sortEntryKey(sortEntryB);
+ if (seA.length == 0 || seB.length == 0) {
+ // sort empty values to end (so when users first sort by a
+ // column, they can see and find the desired values in that
+ // column without scrolling past all the empty values).
+ return -(seA.length - seB.length) * modifier;
+ }
+ let collator = cal.createLocaleCollator();
+ let comparison = collator.compareString(0, seA, seB);
+ return comparison * modifier;
+ };
+ default:
+ return function(sortEntryA, sortEntryB) {
+ return 0;
+ };
+ }
+ },
+
+ getItemSortKey: function(aItem, aKey, aStartTime) {
+ switch (aKey) {
+ case "priority":
+ return aItem.priority || 5;
+
+ case "title":
+ return aItem.title || "";
+
+ case "entryDate":
+ return cal.nativeTime(aItem.entryDate);
+
+ case "startDate":
+ return cal.nativeTime(aItem.startDate);
+
+ case "dueDate":
+ return cal.nativeTime(aItem.dueDate);
+
+ case "endDate":
+ return cal.nativeTime(aItem.endDate);
+
+ case "completedDate":
+ return cal.nativeTime(aItem.completedDate);
+
+ case "percentComplete":
+ return aItem.percentComplete;
+
+ case "categories":
+ return aItem.getCategories({}).join(", ");
+
+ case "location":
+ return aItem.getProperty("LOCATION") || "";
+
+ case "status":
+ if (cal.isToDo(aItem)) {
+ return ["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"].indexOf(aItem.status);
+ } else {
+ return ["TENTATIVE", "CONFIRMED", "CANCELLED"].indexOf(aItem.status);
+ }
+ case "calendar":
+ return aItem.calendar.name || "";
+
+ default:
+ return null;
+ }
+ },
+
+ getSortTypeForSortKey: function(aSortKey) {
+ switch (aSortKey) {
+ case "title":
+ case "categories":
+ case "location":
+ case "calendar":
+ return "string";
+
+ // All dates use "date_filled"
+ case "completedDate":
+ case "startDate":
+ case "endDate":
+ case "dueDate":
+ case "entryDate":
+ return "date_filled";
+
+ case "priority":
+ case "percentComplete":
+ case "status":
+ return "number";
+ default:
+ return "unknown";
+ }
+ },
+
+ nativeTimeOrNow: function(calDateTime, sortStartedTime) {
+ // Treat null/0 as 'now' when sort started, so incomplete tasks stay current.
+ // Time is computed once per sort (just before sort) so sort is stable.
+ if (calDateTime == null) {
+ return sortStartedTime.nativeTime;
+ }
+ let nativeTime = calDateTime.nativeTime;
+ if (nativeTime == -62168601600000000) { // nativeTime value for (0000/00/00 00:00:00)
+ return sortStartedTime;
+ }
+ return nativeTime;
+ },
+
+ nativeTime: function(calDateTime) {
+ if (calDateTime == null) {
+ return -62168601600000000; // ns value for (0000/00/00 00:00:00)
+ }
+ return calDateTime.nativeTime;
+ },
+
+ /**
+ * Returns a calIDateTime corresponding to a javascript Date.
+ *
+ * @param aDate a javascript date
+ * @param aTimezone (optional) a timezone that should be enforced
+ * @returns a calIDateTime
+ *
+ * @warning Use of this function is strongly discouraged. calIDateTime should
+ * be used directly whenever possible.
+ * If you pass a timezone, then the passed jsDate's timezone will be ignored,
+ * but only its local time portions are be taken.
+ */
+ jsDateToDateTime: function(aDate, aTimezone) {
+ let newDate = cal.createDateTime();
+ if (aTimezone) {
+ newDate.resetTo(aDate.getFullYear(),
+ aDate.getMonth(),
+ aDate.getDate(),
+ aDate.getHours(),
+ aDate.getMinutes(),
+ aDate.getSeconds(),
+ aTimezone);
+ } else {
+ newDate.nativeTime = aDate.getTime() * 1000;
+ }
+ return newDate;
+ },
+
+ /**
+ * Convert a calIDateTime to a Javascript date object. This is the
+ * replacement for the former .jsDate property.
+ *
+ * @param cdt The calIDateTime instnace
+ * @return The Javascript date equivalent.
+ */
+ dateTimeToJsDate: function(cdt) {
+ if (cdt.timezone.isFloating) {
+ return new Date(cdt.year, cdt.month, cdt.day,
+ cdt.hour, cdt.minute, cdt.second);
+ } else {
+ return new Date(cdt.nativeTime / 1000);
+ }
+ },
+
+ sortEntry: function(aItem) {
+ let key = cal.getItemSortKey(aItem, this.mSortKey, this.mSortStartedDate);
+ return { mSortKey: key, mItem: aItem };
+ },
+
+ sortEntryItem: function(sortEntry) {
+ return sortEntry.mItem;
+ },
+
+ sortEntryKey: function(sortEntry) {
+ return sortEntry.mSortKey;
+ },
+
+ createLocaleCollator: function() {
+ return Components.classes["@mozilla.org/intl/collation-factory;1"]
+ .getService(Components.interfaces.nsICollationFactory)
+ .CreateCollation(Services.locale.getApplicationLocale());
+ },
+
+ /**
+ * Sort an array of strings according to the current locale.
+ * Modifies aStringArray, returning it sorted.
+ */
+ sortArrayByLocaleCollator: function(aStringArray) {
+ let localeCollator = cal.createLocaleCollator();
+ function compare(a, b) { return localeCollator.compareString(0, a, b); }
+ aStringArray.sort(compare);
+ return aStringArray;
+ },
+
+ /**
+ * Gets the month name string in the right form depending on a base string.
+ *
+ * @param aMonthNum The month numer to get, 1-based.
+ * @param aBundleName The Bundle to get the string from
+ * @param aStringBase The base string name, .monthFormat will be appended
+ */
+ formatMonth: function(aMonthNum, aBundleName, aStringBase) {
+ let monthForm = cal.calGetString(aBundleName, aStringBase + ".monthFormat") || "nominative";
+
+ if (monthForm == "nominative") {
+ // Fall back to the default name format
+ monthForm = "name";
+ }
+
+ return cal.calGetString("dateFormat", "month." + aMonthNum + "." + monthForm);
+ },
+
+ /**
+ * moves an item to another startDate
+ *
+ * @param aOldItem The Item to be modified
+ * @param aNewDate The date at which the new item is going to start
+ * @return The modified item
+ */
+ moveItem: function(aOldItem, aNewDate) {
+ let newItem = aOldItem.clone();
+ let start = (aOldItem[calGetStartDateProp(aOldItem)] ||
+ aOldItem[calGetEndDateProp(aOldItem)]).clone();
+ let isDate = start.isDate;
+ start.resetTo(aNewDate.year, aNewDate.month, aNewDate.day,
+ start.hour, start.minute, start.second,
+ start.timezone);
+ start.isDate = isDate;
+ if (newItem[calGetStartDateProp(newItem)]) {
+ newItem[calGetStartDateProp(newItem)] = start;
+ let oldDuration = aOldItem.duration;
+ if (oldDuration) {
+ let oldEnd = aOldItem[calGetEndDateProp(aOldItem)];
+ let newEnd = start.clone();
+ newEnd.addDuration(oldDuration);
+ newEnd = newEnd.getInTimezone(oldEnd.timezone);
+ newItem[calGetEndDateProp(newItem)] = newEnd;
+ }
+ } else if (newItem[calGetEndDateProp(newItem)]) {
+ newItem[calGetEndDateProp(newItem)] = start;
+ }
+ return newItem;
+ },
+
+ /**
+ * sets the 'isDate' property of an item
+ *
+ * @param aItem The Item to be modified
+ * @param aIsDate True or false indicating the new value of 'isDate'
+ * @return The modified item
+ */
+ setItemToAllDay: function(aItem, aIsDate) {
+ let start = aItem[calGetStartDateProp(aItem)];
+ let end = aItem[calGetEndDateProp(aItem)];
+ if (start || end) {
+ let item = aItem.clone();
+ if (start && (start.isDate != aIsDate)) {
+ start = start.clone();
+ start.isDate = aIsDate;
+ item[calGetStartDateProp(item)] = start;
+ }
+ if (end && (end.isDate != aIsDate)) {
+ end = end.clone();
+ end.isDate = aIsDate;
+ item[calGetEndDateProp(item)] = end;
+ }
+ return item;
+ } else {
+ return aItem;
+ }
+ },
+
+ /**
+ * checks if the mousepointer of an event resides over a XULBox during an event
+ *
+ * @param aMouseEvent The event eg. a 'mouseout' or 'mousedown' event
+ * @param aXULBox The xul element
+ * @return true or false depending on whether the mouse pointer
+ * resides over the xulelement
+ */
+ isMouseOverBox: function(aMouseEvent, aXULElement) {
+ let boxObject = aXULElement.boxObject;
+ let boxWidth = boxObject.width;
+ let boxHeight = boxObject.height;
+ let boxScreenX = boxObject.screenX;
+ let boxScreenY = boxObject.screenY;
+ let mouseX = aMouseEvent.screenX;
+ let mouseY = aMouseEvent.screenY;
+ let xIsWithin = (mouseX >= boxScreenX) &&
+ (mouseX <= (boxScreenX + boxWidth));
+ let yIsWithin = (mouseY >= boxScreenY) &&
+ (mouseY <= (boxScreenY + boxHeight));
+ return (xIsWithin && yIsWithin);
+ },
+
+ /**
+ * removes those childnodes from a node that contain a specified attribute
+ * and where the value of this attribute matches a passed value
+ * @param aParentNode The parent node that contains the child nodes in question
+ * @param aAttribute The name of the attribute
+ * @param aAttribute The value of the attribute
+ */
+ removeChildElementsByAttribute: function(aParentNode, aAttribute, aValue) {
+ let childNode = aParentNode.lastChild;
+ while (childNode) {
+ let prevChildNode = childNode.previousSibling;
+ if (!aAttribute || aAttribute === undefined) {
+ childNode.remove();
+ } else if (!aValue || aValue === undefined) {
+ childNode.remove();
+ } else if (childNode && childNode.hasAttribute(aAttribute) &&
+ childNode.getAttribute(aAttribute) == aValue) {
+ childNode.remove();
+ }
+ childNode = prevChildNode;
+ }
+ },
+
+ /**
+ * Returns the most recent calendar window in an application independent way
+ */
+ getCalendarWindow: function() {
+ return Services.wm.getMostRecentWindow("calendarMainWindow") ||
+ Services.wm.getMostRecentWindow("mail:3pane");
+ },
+
+ /**
+ * Adds an observer listening for the topic.
+ *
+ * @param func function to execute on topic
+ * @param topic topic to listen for
+ * @param oneTime whether to listen only once
+ */
+ addObserver: function(func, topic, oneTime) {
+ let observer = { // nsIObserver:
+ observe: function(subject, topic_, data) {
+ if (topic == topic_) {
+ if (oneTime) {
+ Services.obs.removeObserver(this, topic);
+ }
+ func(subject, topic, data);
+ }
+ }
+ };
+ Services.obs.addObserver(observer, topic, false /* don't hold weakly */);
+ },
+
+ /**
+ * Wraps an instance. Replaces calInstanceOf from calUtils.js
+ *
+ * @param aObj the object under consideration
+ * @param aInterface the interface to be wrapped
+ *
+ * Use this function to QueryInterface the object to a particular interface.
+ * You may only expect the return value to be wrapped, not the original passed object.
+ * For example:
+ * // BAD USAGE:
+ * if (cal.wrapInstance(foo, Ci.nsIBar)) {
+ * foo.barMethod();
+ * }
+ * // GOOD USAGE:
+ * foo = cal.wrapInstance(foo, Ci.nsIBar);
+ * if (foo) {
+ * foo.barMethod();
+ * }
+ *
+ */
+ wrapInstance: function(aObj, aInterface) {
+ if (!aObj) {
+ return null;
+ }
+
+ try {
+ return aObj.QueryInterface(aInterface);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Adds an xpcom shutdown observer.
+ *
+ * @param func function to execute
+ */
+ addShutdownObserver: function(func) {
+ cal.addObserver(func, "xpcom-shutdown", true /* one time */);
+ },
+
+ /**
+ * Due to wrapped js objects, some objects may have cyclic references.
+ * You can register properties of objects to be cleaned up on xpcom-shutdown.
+ *
+ * @param obj object
+ * @param prop property to be deleted on shutdown
+ * (if null, |object| will be deleted)
+ */
+ registerForShutdownCleanup: shutdownCleanup
+};
+
+// local to this module;
+// will be used to clean up global objects on shutdown
+// some objects have cyclic references due to wrappers
+function shutdownCleanup(obj, prop) {
+ if (!shutdownCleanup.mEntries) {
+ shutdownCleanup.mEntries = [];
+ cal.addShutdownObserver(() => {
+ for (let entry of shutdownCleanup.mEntries) {
+ if (entry.mProp) {
+ delete entry.mObj[entry.mProp];
+ } else {
+ delete entry.mObj;
+ }
+ }
+ delete shutdownCleanup.mEntries;
+ });
+ }
+ shutdownCleanup.mEntries.push({ mObj: obj, mProp: prop });
+}
+
+// local to this module;
+// will be used to generate service accessor functions
+function generateServiceAccessor(id, iface) {
+ // eslint-disable-next-line func-names
+ return function this_() {
+ if (!("mService" in this_)) {
+ this_.mService = Components.classes[id].getService(iface);
+ shutdownCleanup(this_, "mService");
+ }
+ return this_.mService;
+ };
+}
+
+// Interim import of all symbols into cal:
+// This should serve as a clean start for new code, e.g. new code could use
+// cal.createDatetime instead of plain createDatetime NOW.
+cal.loadScripts(["calUtils.js"], cal);
+// Some functions in calUtils.js refer to other in the same file, thus include
+// the code in global scope (although only visible to this module file), too:
+cal.loadScripts(["calUtils.js"], Components.utils.getGlobalForObject(cal));
diff --git a/calendar/base/modules/calViewUtils.jsm b/calendar/base/modules/calViewUtils.jsm
new file mode 100644
index 000000000..24934dc64
--- /dev/null
+++ b/calendar/base/modules/calViewUtils.jsm
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["cal"];
+cal.view = {
+ /**
+ - * Item comparator for inserting items into dayboxes.
+ - *
+ - * @param a The first item
+ - * @param b The second item
+ - * @return The usual -1, 0, 1
+ - */
+ compareItems: function(a, b) {
+ if (!a) {
+ return -1;
+ }
+ if (!b) {
+ return 1;
+ }
+
+ let aIsEvent = cal.isEvent(a);
+ let aIsTodo = cal.isToDo(a);
+
+ let bIsEvent = cal.isEvent(b);
+ let bIsTodo = cal.isToDo(b);
+
+ // sort todos before events
+ if (aIsTodo && bIsEvent) {
+ return -1;
+ }
+ if (aIsEvent && bIsTodo) {
+ return 1;
+ }
+
+ // sort items of the same type according to date-time
+ let aStartDate = a.startDate || a.entryDate || a.dueDate;
+ let bStartDate = b.startDate || b.entryDate || b.dueDate;
+ let aEndDate = a.endDate || a.dueDate || a.entryDate;
+ let bEndDate = b.endDate || b.dueDate || b.entryDate;
+ if (!aStartDate || !bStartDate) {
+ return 0;
+ }
+
+ // sort all day events before events with a duration
+ if (aStartDate.isDate && !bStartDate.isDate) {
+ return -1;
+ }
+ if (!aStartDate.isDate && bStartDate.isDate) {
+ return 1;
+ }
+
+ let cmp = aStartDate.compare(bStartDate);
+ if (cmp != 0) {
+ return cmp;
+ }
+
+ if (!aEndDate || !bEndDate) {
+ return 0;
+ }
+ cmp = aEndDate.compare(bEndDate);
+ if (cmp != 0) {
+ return cmp;
+ }
+
+ cmp = (a.title > b.title) - (a.title < b.title);
+ return cmp;
+ }
+};
diff --git a/calendar/base/modules/calXMLUtils.jsm b/calendar/base/modules/calXMLUtils.jsm
new file mode 100644
index 000000000..d026ad946
--- /dev/null
+++ b/calendar/base/modules/calXMLUtils.jsm
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** Helper functions for parsing and serializing XML */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["cal"];
+cal.xml = {} || cal.xml;
+
+/**
+ * Evaluate an XPath query for the given node. Be careful with the return value
+ * here, as it may be:
+ *
+ * - null, if there are no results
+ * - a number, string or boolean value
+ * - an array of strings or DOM elements
+ *
+ * @param aNode The context node to search from
+ * @param aExpr The XPath expression to search for
+ * @param aResolver (optional) The namespace resolver to use for the expression
+ * @param aType (optional) Force a result type, must be an XPathResult constant
+ * @return The result, see above for details.
+ */
+cal.xml.evalXPath = function(aNode, aExpr, aResolver, aType) {
+ const XPR = Components.interfaces.nsIDOMXPathResult;
+ let doc = (aNode.ownerDocument ? aNode.ownerDocument : aNode);
+ let resolver = aResolver || doc.createNSResolver(doc.documentElement);
+ let resultType = aType || XPR.ANY_TYPE;
+
+ let result = doc.evaluate(aExpr, aNode, resolver, resultType, null);
+ let returnResult, next;
+ switch (result.resultType) {
+ case XPR.NUMBER_TYPE:
+ returnResult = result.numberValue;
+ break;
+ case XPR.STRING_TYPE:
+ returnResult = result.stringValue;
+ break;
+ case XPR.BOOLEAN_TYPE:
+ returnResult = result.booleanValue;
+ break;
+ case XPR.UNORDERED_NODE_ITERATOR_TYPE:
+ case XPR.ORDERED_NODE_ITERATOR_TYPE:
+ returnResult = [];
+ while ((next = result.iterateNext())) {
+ if (next instanceof Components.interfaces.nsIDOMText) {
+ returnResult.push(next.wholeText);
+ } else if (next instanceof Components.interfaces.nsIDOMAttr) {
+ returnResult.push(next.value);
+ } else {
+ returnResult.push(next);
+ }
+ }
+ break;
+ case XPR.UNORDERED_NODE_SNAPSHOT_TYPE:
+ case XPR.ORDERED_NODE_SNAPSHOT_TYPE:
+ returnResult = [];
+ for (let i = 0; i < result.snapshotLength; i++) {
+ next = result.snapshotItem(i);
+ if (next instanceof Components.interfaces.nsIDOMText) {
+ returnResult.push(next.wholeText);
+ } else if (next instanceof Components.interfaces.nsIDOMAttr) {
+ returnResult.push(next.value);
+ } else {
+ returnResult.push(next);
+ }
+ }
+ break;
+ case XPR.ANY_UNORDERED_NODE_TYPE:
+ case XPR.FIRST_ORDERED_NODE_TYPE:
+ returnResult = result.singleNodeValue;
+ break;
+ default:
+ returnResult = null;
+ break;
+ }
+
+ if (Array.isArray(returnResult) && returnResult.length == 0) {
+ returnResult = null;
+ }
+
+ return returnResult;
+};
+
+/**
+ * Convenience function to evaluate an XPath expression and return null or the
+ * first result. Helpful if you just expect one value in a text() expression,
+ * but its possible that there will be more than one. The result may be:
+ *
+ * - null, if there are no results
+ * - A string, number, boolean or DOM Element value
+ *
+ * @param aNode The context node to search from
+ * @param aExpr The XPath expression to search for
+ * @param aResolver (optional) The namespace resolver to use for the expression
+ * @param aType (optional) Force a result type, must be an XPathResult constant
+ * @return The result, see above for details.
+ */
+cal.xml.evalXPathFirst = function(aNode, aExpr, aResolver, aType) {
+ let result = cal.xml.evalXPath(aNode, aExpr, aResolver, aType);
+
+ if (Array.isArray(result)) {
+ return result[0];
+ } else {
+ return result;
+ }
+};
+
+/**
+ * Parse the given string into a DOM tree
+ *
+ * @param str The string to parse
+ * @param docUri (optional) The document URI to use
+ * @param baseUri (optional) The base URI to use
+ * @return The parsed DOM Document
+ */
+cal.xml.parseString = function(str, docUri, baseUri) {
+ let parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Components.interfaces.nsIDOMParser);
+
+ parser.init(null, docUri, baseUri);
+ return parser.parseFromString(str, "application/xml");
+};
+
+/**
+ * Read an XML file synchronously. This method should be avoided, consider
+ * rewriting the caller to be asynchronous.
+ *
+ * @param uri The URI to read.
+ * @return The DOM Document resulting from the file.
+ */
+cal.xml.parseFile = function(uri) {
+ let req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Components.interfaces.nsIXMLHttpRequest);
+
+ req.open("GET", uri, false);
+ req.overrideMimeType("text/xml");
+ req.send(null);
+ return req.responseXML;
+};
+
+/**
+ * Serialize the DOM tree into a string.
+ *
+ * @param doc The DOM document to serialize
+ * @return The DOM document as a string.
+ */
+cal.xml.serializeDOM = function(doc) {
+ let serializer = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"]
+ .createInstance(Components.interfaces.nsIDOMSerializer);
+ return serializer.serializeToString(doc);
+};
+
+/**
+ * Escape a string for use in XML
+ *
+ * @param str The string to escape
+ * @param isAttribute If true, " and ' are also escaped
+ * @return The escaped string
+ */
+cal.xml.escapeString = function(str, isAttribute) {
+ return str.replace(/[&<>'"]/g, (chr) => {
+ switch (chr) {
+ case "&": return "&amp;";
+ case "<": return "&lt;";
+ case ">": return "&gt;";
+ case '"': return (isAttribute ? "&quot;" : chr);
+ case "'": return (isAttribute ? "&apos;" : chr);
+ default: return chr;
+ }
+ });
+};
diff --git a/calendar/base/modules/ical.js b/calendar/base/modules/ical.js
new file mode 100644
index 000000000..5fb00b852
--- /dev/null
+++ b/calendar/base/modules/ical.js
@@ -0,0 +1,9354 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This is ical.js from <https://github.com/mozilla-comm/ical.js>.
+ *
+ * If you would like to change anything in ical.js, it is required to do so
+ * upstream first.
+ *
+ * Current ical.js git revision: f4fb1b9564f990f7b718253f2251bb30f58c9a5a (v1.2.0)
+ */
+
+var EXPORTED_SYMBOLS = ["ICAL", "unwrap", "unwrapSetter", "unwrapSingle", "wrapGetter"];
+
+function wrapGetter(type, val) {
+ return val ? new type(val) : null;
+}
+
+function unwrap(type, innerFunc) {
+ return function(val) { return unwrapSetter.call(this, type, val, innerFunc); };
+}
+
+function unwrapSetter(type, val, innerFunc, thisObj) {
+ return innerFunc.call(thisObj || this, unwrapSingle(type, val));
+}
+
+function unwrapSingle(type, val) {
+ if (!val || !val.wrappedJSObject) {
+ return null;
+ } else if (val.wrappedJSObject.innerObject instanceof type) {
+ return val.wrappedJSObject.innerObject;
+ } else {
+ Components.utils.import("resource://calendar/modules/calUtils.jsm");
+ Components.utils.reportError("Unknown " + (type.icalclass || type) + " passed at " + cal.STACK(10));
+ return null;
+ }
+}
+
+// -- start ical.js --
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/* istanbul ignore next */
+/* jshint ignore:start */
+if (typeof module === 'object') {
+ // CommonJS, where exports may be different each time.
+ ICAL = module.exports;
+} else if (typeof ICAL !== 'object') {/* istanbul ignore next */
+ /** @ignore */
+ this.ICAL = {};
+}
+/* jshint ignore:end */
+
+
+/**
+ * The number of characters before iCalendar line folding should occur
+ * @type {Number}
+ * @default 75
+ */
+ICAL.foldLength = 75;
+
+
+/**
+ * The character(s) to be used for a newline. The default value is provided by
+ * rfc5545.
+ * @type {String}
+ * @default "\r\n"
+ */
+ICAL.newLineChar = '\r\n';
+
+
+/**
+ * Helper functions used in various places within ical.js
+ * @namespace
+ */
+ICAL.helpers = {
+ /**
+ * Checks if the given type is of the number type and also NaN.
+ *
+ * @param {Number} number The number to check
+ * @return {Boolean} True, if the number is strictly NaN
+ */
+ isStrictlyNaN: function(number) {
+ return typeof(number) === 'number' && isNaN(number);
+ },
+
+ /**
+ * Parses a string value that is expected to be an integer, when the valid is
+ * not an integer throws a decoration error.
+ *
+ * @param {String} string Raw string input
+ * @return {Number} Parsed integer
+ */
+ strictParseInt: function(string) {
+ var result = parseInt(string, 10);
+
+ if (ICAL.helpers.isStrictlyNaN(result)) {
+ throw new Error(
+ 'Could not extract integer from "' + string + '"'
+ );
+ }
+
+ return result;
+ },
+
+ /**
+ * Creates or returns a class instance of a given type with the initialization
+ * data if the data is not already an instance of the given type.
+ *
+ * @example
+ * var time = new ICAL.Time(...);
+ * var result = ICAL.helpers.formatClassType(time, ICAL.Time);
+ *
+ * (result instanceof ICAL.Time)
+ * // => true
+ *
+ * result = ICAL.helpers.formatClassType({}, ICAL.Time);
+ * (result isntanceof ICAL.Time)
+ * // => true
+ *
+ *
+ * @param {Object} data object initialization data
+ * @param {Object} type object type (like ICAL.Time)
+ * @return {?} An instance of the found type.
+ */
+ formatClassType: function formatClassType(data, type) {
+ if (typeof(data) === 'undefined') {
+ return undefined;
+ }
+
+ if (data instanceof type) {
+ return data;
+ }
+ return new type(data);
+ },
+
+ /**
+ * Identical to indexOf but will only match values when they are not preceded
+ * by a backslash character.
+ *
+ * @param {String} buffer String to search
+ * @param {String} search Value to look for
+ * @param {Number} pos Start position
+ * @return {Number} The position, or -1 if not found
+ */
+ unescapedIndexOf: function(buffer, search, pos) {
+ while ((pos = buffer.indexOf(search, pos)) !== -1) {
+ if (pos > 0 && buffer[pos - 1] === '\\') {
+ pos += 1;
+ } else {
+ return pos;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Find the index for insertion using binary search.
+ *
+ * @param {Array} list The list to search
+ * @param {?} seekVal The value to insert
+ * @param {function(?,?)} cmpfunc The comparison func, that can
+ * compare two seekVals
+ * @return {Number} The insert position
+ */
+ binsearchInsert: function(list, seekVal, cmpfunc) {
+ if (!list.length)
+ return 0;
+
+ var low = 0, high = list.length - 1,
+ mid, cmpval;
+
+ while (low <= high) {
+ mid = low + Math.floor((high - low) / 2);
+ cmpval = cmpfunc(seekVal, list[mid]);
+
+ if (cmpval < 0)
+ high = mid - 1;
+ else if (cmpval > 0)
+ low = mid + 1;
+ else
+ break;
+ }
+
+ if (cmpval < 0)
+ return mid; // insertion is displacing, so use mid outright.
+ else if (cmpval > 0)
+ return mid + 1;
+ else
+ return mid;
+ },
+
+ /**
+ * Convenience function for debug output
+ * @private
+ */
+ dumpn: /* istanbul ignore next */ function() {
+ if (!ICAL.debug) {
+ return;
+ }
+
+ if (typeof (console) !== 'undefined' && 'log' in console) {
+ ICAL.helpers.dumpn = function consoleDumpn(input) {
+ console.log(input);
+ };
+ } else {
+ ICAL.helpers.dumpn = function geckoDumpn(input) {
+ dump(input + '\n');
+ };
+ }
+
+ ICAL.helpers.dumpn(arguments[0]);
+ },
+
+ /**
+ * Clone the passed object or primitive. By default a shallow clone will be
+ * executed.
+ *
+ * @param {*} aSrc The thing to clone
+ * @param {Boolean=} aDeep If true, a deep clone will be performed
+ * @return {*} The copy of the thing
+ */
+ clone: function(aSrc, aDeep) {
+ if (!aSrc || typeof aSrc != "object") {
+ return aSrc;
+ } else if (aSrc instanceof Date) {
+ return new Date(aSrc.getTime());
+ } else if ("clone" in aSrc) {
+ return aSrc.clone();
+ } else if (Array.isArray(aSrc)) {
+ var arr = [];
+ for (var i = 0; i < aSrc.length; i++) {
+ arr.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]);
+ }
+ return arr;
+ } else {
+ var obj = {};
+ for (var name in aSrc) {
+ // uses prototype method to allow use of Object.create(null);
+ /* istanbul ignore else */
+ if (Object.prototype.hasOwnProperty.call(aSrc, name)) {
+ if (aDeep) {
+ obj[name] = ICAL.helpers.clone(aSrc[name], true);
+ } else {
+ obj[name] = aSrc[name];
+ }
+ }
+ }
+ return obj;
+ }
+ },
+
+ /**
+ * Performs iCalendar line folding. A line ending character is inserted and
+ * the next line begins with a whitespace.
+ *
+ * @example
+ * SUMMARY:This line will be fold
+ * ed right in the middle of a word.
+ *
+ * @param {String} aLine The line to fold
+ * @return {String} The folded line
+ */
+ foldline: function foldline(aLine) {
+ var result = "";
+ var line = aLine || "";
+
+ while (line.length) {
+ result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength);
+ line = line.substr(ICAL.foldLength);
+ }
+ return result.substr(ICAL.newLineChar.length + 1);
+ },
+
+ /**
+ * Pads the given string or number with zeros so it will have at least two
+ * characters.
+ *
+ * @param {String|Number} data The string or number to pad
+ * @return {String} The number padded as a string
+ */
+ pad2: function pad(data) {
+ if (typeof(data) !== 'string') {
+ // handle fractions.
+ if (typeof(data) === 'number') {
+ data = parseInt(data);
+ }
+ data = String(data);
+ }
+
+ var len = data.length;
+
+ switch (len) {
+ case 0:
+ return '00';
+ case 1:
+ return '0' + data;
+ default:
+ return data;
+ }
+ },
+
+ /**
+ * Truncates the given number, correctly handling negative numbers.
+ *
+ * @param {Number} number The number to truncate
+ * @return {Number} The truncated number
+ */
+ trunc: function trunc(number) {
+ return (number < 0 ? Math.ceil(number) : Math.floor(number));
+ },
+
+ /**
+ * Poor-man's cross-browser inheritance for JavaScript. Doesn't support all
+ * the features, but enough for our usage.
+ *
+ * @param {Function} base The base class constructor function.
+ * @param {Function} child The child class constructor function.
+ * @param {Object} extra Extends the prototype with extra properties
+ * and methods
+ */
+ inherits: function(base, child, extra) {
+ function F() {}
+ F.prototype = base.prototype;
+ child.prototype = new F();
+
+ if (extra) {
+ ICAL.helpers.extend(extra, child.prototype);
+ }
+ },
+
+ /**
+ * Poor-man's cross-browser object extension. Doesn't support all the
+ * features, but enough for our usage. Note that the target's properties are
+ * not overwritten with the source properties.
+ *
+ * @example
+ * var child = ICAL.helpers.extend(parent, {
+ * "bar": 123
+ * });
+ *
+ * @param {Object} source The object to extend
+ * @param {Object} target The object to extend with
+ * @return {Object} Returns the target.
+ */
+ extend: function(source, target) {
+ for (var key in source) {
+ var descr = Object.getOwnPropertyDescriptor(source, key);
+ if (descr && !Object.getOwnPropertyDescriptor(target, key)) {
+ Object.defineProperty(target, key, descr);
+ }
+ }
+ return target;
+ }
+};
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+/** @namespace ICAL */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.design = (function() {
+ 'use strict';
+
+ var FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g;
+ var TO_ICAL_NEWLINE = /\\|;|,|\n/g;
+ var FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g;
+ var TO_VCARD_NEWLINE = /\\|,|\n/g;
+
+ function createTextType(fromNewline, toNewline) {
+ var result = {
+ matches: /.*/,
+
+ fromICAL: function(aValue, structuredEscape) {
+ return replaceNewline(aValue, fromNewline, structuredEscape);
+ },
+
+ toICAL: function(aValue, structuredEscape) {
+ var regEx = toNewline;
+ if (structuredEscape)
+ regEx = new RegExp(regEx.source + '|' + structuredEscape);
+ return aValue.replace(regEx, function(str) {
+ switch (str) {
+ case "\\":
+ return "\\\\";
+ case ";":
+ return "\\;";
+ case ",":
+ return "\\,";
+ case "\n":
+ return "\\n";
+ /* istanbul ignore next */
+ default:
+ return str;
+ }
+ });
+ }
+ };
+ return result;
+ }
+
+ // default types used multiple times
+ var DEFAULT_TYPE_TEXT = { defaultType: "text" };
+ var DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," };
+ var DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" };
+ var DEFAULT_TYPE_INTEGER = { defaultType: "integer" };
+ var DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] };
+ var DEFAULT_TYPE_DATETIME = { defaultType: "date-time" };
+ var DEFAULT_TYPE_URI = { defaultType: "uri" };
+ var DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" };
+ var DEFAULT_TYPE_RECUR = { defaultType: "recur" };
+ var DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] };
+
+ function replaceNewlineReplace(string) {
+ switch (string) {
+ case "\\\\":
+ return "\\";
+ case "\\;":
+ return ";";
+ case "\\,":
+ return ",";
+ case "\\n":
+ case "\\N":
+ return "\n";
+ /* istanbul ignore next */
+ default:
+ return string;
+ }
+ }
+
+ function replaceNewline(value, newline, structuredEscape) {
+ // avoid regex when possible.
+ if (value.indexOf('\\') === -1) {
+ return value;
+ }
+ if (structuredEscape)
+ newline = new RegExp(newline.source + '|\\\\' + structuredEscape);
+ return value.replace(newline, replaceNewlineReplace);
+ }
+
+ var commonProperties = {
+ "categories": DEFAULT_TYPE_TEXT_MULTI,
+ "url": DEFAULT_TYPE_URI,
+ "version": DEFAULT_TYPE_TEXT,
+ "uid": DEFAULT_TYPE_TEXT
+ };
+
+ var commonValues = {
+ "boolean": {
+ values: ["TRUE", "FALSE"],
+
+ fromICAL: function(aValue) {
+ switch (aValue) {
+ case 'TRUE':
+ return true;
+ case 'FALSE':
+ return false;
+ default:
+ //TODO: parser warning
+ return false;
+ }
+ },
+
+ toICAL: function(aValue) {
+ if (aValue) {
+ return 'TRUE';
+ }
+ return 'FALSE';
+ }
+
+ },
+ float: {
+ matches: /^[+-]?\d+\.\d+$/,
+
+ fromICAL: function(aValue) {
+ var parsed = parseFloat(aValue);
+ if (ICAL.helpers.isStrictlyNaN(parsed)) {
+ // TODO: parser warning
+ return 0.0;
+ }
+ return parsed;
+ },
+
+ toICAL: function(aValue) {
+ return String(aValue);
+ }
+ },
+ integer: {
+ fromICAL: function(aValue) {
+ var parsed = parseInt(aValue);
+ if (ICAL.helpers.isStrictlyNaN(parsed)) {
+ return 0;
+ }
+ return parsed;
+ },
+
+ toICAL: function(aValue) {
+ return String(aValue);
+ }
+ },
+ "utc-offset": {
+ toICAL: function(aValue) {
+ if (aValue.length < 7) {
+ // no seconds
+ // -0500
+ return aValue.substr(0, 3) +
+ aValue.substr(4, 2);
+ } else {
+ // seconds
+ // -050000
+ return aValue.substr(0, 3) +
+ aValue.substr(4, 2) +
+ aValue.substr(7, 2);
+ }
+ },
+
+ fromICAL: function(aValue) {
+ if (aValue.length < 6) {
+ // no seconds
+ // -05:00
+ return aValue.substr(0, 3) + ':' +
+ aValue.substr(3, 2);
+ } else {
+ // seconds
+ // -05:00:00
+ return aValue.substr(0, 3) + ':' +
+ aValue.substr(3, 2) + ':' +
+ aValue.substr(5, 2);
+ }
+ },
+
+ decorate: function(aValue) {
+ return ICAL.UtcOffset.fromString(aValue);
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toString();
+ }
+ }
+ };
+
+ var icalParams = {
+ // Although the syntax is DQUOTE uri DQUOTE, I don't think we should
+ // enfoce anything aside from it being a valid content line.
+ //
+ // At least some params require - if multi values are used - DQUOTEs
+ // for each of its values - e.g. delegated-from="uri1","uri2"
+ // To indicate this, I introduced the new k/v pair
+ // multiValueSeparateDQuote: true
+ //
+ // "ALTREP": { ... },
+
+ // CN just wants a param-value
+ // "CN": { ... }
+
+ "cutype": {
+ values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"],
+ allowXName: true,
+ allowIanaToken: true
+ },
+
+ "delegated-from": {
+ valueType: "cal-address",
+ multiValue: ",",
+ multiValueSeparateDQuote: true
+ },
+ "delegated-to": {
+ valueType: "cal-address",
+ multiValue: ",",
+ multiValueSeparateDQuote: true
+ },
+ // "DIR": { ... }, // See ALTREP
+ "encoding": {
+ values: ["8BIT", "BASE64"]
+ },
+ // "FMTTYPE": { ... }, // See ALTREP
+ "fbtype": {
+ values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"],
+ allowXName: true,
+ allowIanaToken: true
+ },
+ // "LANGUAGE": { ... }, // See ALTREP
+ "member": {
+ valueType: "cal-address",
+ multiValue: ",",
+ multiValueSeparateDQuote: true
+ },
+ "partstat": {
+ // TODO These values are actually different per-component
+ values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE",
+ "DELEGATED", "COMPLETED", "IN-PROCESS"],
+ allowXName: true,
+ allowIanaToken: true
+ },
+ "range": {
+ values: ["THISLANDFUTURE"]
+ },
+ "related": {
+ values: ["START", "END"]
+ },
+ "reltype": {
+ values: ["PARENT", "CHILD", "SIBLING"],
+ allowXName: true,
+ allowIanaToken: true
+ },
+ "role": {
+ values: ["REQ-PARTICIPANT", "CHAIR",
+ "OPT-PARTICIPANT", "NON-PARTICIPANT"],
+ allowXName: true,
+ allowIanaToken: true
+ },
+ "rsvp": {
+ values: ["TRUE", "FALSE"]
+ },
+ "sent-by": {
+ valueType: "cal-address"
+ },
+ "tzid": {
+ matches: /^\//
+ },
+ "value": {
+ // since the value here is a 'type' lowercase is used.
+ values: ["binary", "boolean", "cal-address", "date", "date-time",
+ "duration", "float", "integer", "period", "recur", "text",
+ "time", "uri", "utc-offset"],
+ allowXName: true,
+ allowIanaToken: true
+ }
+ };
+
+ // When adding a value here, be sure to add it to the parameter types!
+ var icalValues = ICAL.helpers.extend(commonValues, {
+ text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE),
+
+ uri: {
+ // TODO
+ /* ... */
+ },
+
+ "binary": {
+ decorate: function(aString) {
+ return ICAL.Binary.fromString(aString);
+ },
+
+ undecorate: function(aBinary) {
+ return aBinary.toString();
+ }
+ },
+ "cal-address": {
+ // needs to be an uri
+ },
+ "date": {
+ decorate: function(aValue, aProp) {
+ return ICAL.Time.fromDateString(aValue, aProp);
+ },
+
+ /**
+ * undecorates a time object.
+ */
+ undecorate: function(aValue) {
+ return aValue.toString();
+ },
+
+ fromICAL: function(aValue) {
+ // from: 20120901
+ // to: 2012-09-01
+ return aValue.substr(0, 4) + '-' +
+ aValue.substr(4, 2) + '-' +
+ aValue.substr(6, 2);
+ },
+
+ toICAL: function(aValue) {
+ // from: 2012-09-01
+ // to: 20120901
+
+ if (aValue.length > 11) {
+ //TODO: serialize warning?
+ return aValue;
+ }
+
+ return aValue.substr(0, 4) +
+ aValue.substr(5, 2) +
+ aValue.substr(8, 2);
+ }
+ },
+ "date-time": {
+ fromICAL: function(aValue) {
+ // from: 20120901T130000
+ // to: 2012-09-01T13:00:00
+ var result = aValue.substr(0, 4) + '-' +
+ aValue.substr(4, 2) + '-' +
+ aValue.substr(6, 2) + 'T' +
+ aValue.substr(9, 2) + ':' +
+ aValue.substr(11, 2) + ':' +
+ aValue.substr(13, 2);
+
+ if (aValue[15] && aValue[15] === 'Z') {
+ result += 'Z';
+ }
+
+ return result;
+ },
+
+ toICAL: function(aValue) {
+ // from: 2012-09-01T13:00:00
+ // to: 20120901T130000
+
+ if (aValue.length < 19) {
+ // TODO: error
+ return aValue;
+ }
+
+ var result = aValue.substr(0, 4) +
+ aValue.substr(5, 2) +
+ // grab the (DDTHH) segment
+ aValue.substr(8, 5) +
+ // MM
+ aValue.substr(14, 2) +
+ // SS
+ aValue.substr(17, 2);
+
+ if (aValue[19] && aValue[19] === 'Z') {
+ result += 'Z';
+ }
+
+ return result;
+ },
+
+ decorate: function(aValue, aProp) {
+ return ICAL.Time.fromDateTimeString(aValue, aProp);
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toString();
+ }
+ },
+ duration: {
+ decorate: function(aValue) {
+ return ICAL.Duration.fromString(aValue);
+ },
+ undecorate: function(aValue) {
+ return aValue.toString();
+ }
+ },
+ period: {
+
+ fromICAL: function(string) {
+ var parts = string.split('/');
+ parts[0] = icalValues['date-time'].fromICAL(parts[0]);
+
+ if (!ICAL.Duration.isValueString(parts[1])) {
+ parts[1] = icalValues['date-time'].fromICAL(parts[1]);
+ }
+
+ return parts;
+ },
+
+ toICAL: function(parts) {
+ parts[0] = icalValues['date-time'].toICAL(parts[0]);
+
+ if (!ICAL.Duration.isValueString(parts[1])) {
+ parts[1] = icalValues['date-time'].toICAL(parts[1]);
+ }
+
+ return parts.join("/");
+ },
+
+ decorate: function(aValue, aProp) {
+ return ICAL.Period.fromJSON(aValue, aProp);
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toJSON();
+ }
+ },
+ recur: {
+ fromICAL: function(string) {
+ return ICAL.Recur._stringToData(string, true);
+ },
+
+ toICAL: function(data) {
+ var str = "";
+ for (var k in data) {
+ /* istanbul ignore if */
+ if (!Object.prototype.hasOwnProperty.call(data, k)) {
+ continue;
+ }
+ var val = data[k];
+ if (k == "until") {
+ if (val.length > 10) {
+ val = icalValues['date-time'].toICAL(val);
+ } else {
+ val = icalValues.date.toICAL(val);
+ }
+ } else if (k == "wkst") {
+ if (typeof val === 'number') {
+ val = ICAL.Recur.numericDayToIcalDay(val);
+ }
+ } else if (Array.isArray(val)) {
+ val = val.join(",");
+ }
+ str += k.toUpperCase() + "=" + val + ";";
+ }
+ return str.substr(0, str.length - 1);
+ },
+
+ decorate: function decorate(aValue) {
+ return ICAL.Recur.fromData(aValue);
+ },
+
+ undecorate: function(aRecur) {
+ return aRecur.toJSON();
+ }
+ },
+
+ time: {
+ fromICAL: function(aValue) {
+ // from: MMHHSS(Z)?
+ // to: HH:MM:SS(Z)?
+ if (aValue.length < 6) {
+ // TODO: parser exception?
+ return aValue;
+ }
+
+ // HH::MM::SSZ?
+ var result = aValue.substr(0, 2) + ':' +
+ aValue.substr(2, 2) + ':' +
+ aValue.substr(4, 2);
+
+ if (aValue[6] === 'Z') {
+ result += 'Z';
+ }
+
+ return result;
+ },
+
+ toICAL: function(aValue) {
+ // from: HH:MM:SS(Z)?
+ // to: MMHHSS(Z)?
+ if (aValue.length < 8) {
+ //TODO: error
+ return aValue;
+ }
+
+ var result = aValue.substr(0, 2) +
+ aValue.substr(3, 2) +
+ aValue.substr(6, 2);
+
+ if (aValue[8] === 'Z') {
+ result += 'Z';
+ }
+
+ return result;
+ }
+ }
+ });
+
+ var icalProperties = ICAL.helpers.extend(commonProperties, {
+
+ "action": DEFAULT_TYPE_TEXT,
+ "attach": { defaultType: "uri" },
+ "attendee": { defaultType: "cal-address" },
+ "calscale": DEFAULT_TYPE_TEXT,
+ "class": DEFAULT_TYPE_TEXT,
+ "comment": DEFAULT_TYPE_TEXT,
+ "completed": DEFAULT_TYPE_DATETIME,
+ "contact": DEFAULT_TYPE_TEXT,
+ "created": DEFAULT_TYPE_DATETIME,
+ "description": DEFAULT_TYPE_TEXT,
+ "dtend": DEFAULT_TYPE_DATETIME_DATE,
+ "dtstamp": DEFAULT_TYPE_DATETIME,
+ "dtstart": DEFAULT_TYPE_DATETIME_DATE,
+ "due": DEFAULT_TYPE_DATETIME_DATE,
+ "duration": { defaultType: "duration" },
+ "exdate": {
+ defaultType: "date-time",
+ allowedTypes: ["date-time", "date"],
+ multiValue: ','
+ },
+ "exrule": DEFAULT_TYPE_RECUR,
+ "freebusy": { defaultType: "period", multiValue: "," },
+ "geo": { defaultType: "float", structuredValue: ";" },
+ "last-modified": DEFAULT_TYPE_DATETIME,
+ "location": DEFAULT_TYPE_TEXT,
+ "method": DEFAULT_TYPE_TEXT,
+ "organizer": { defaultType: "cal-address" },
+ "percent-complete": DEFAULT_TYPE_INTEGER,
+ "priority": DEFAULT_TYPE_INTEGER,
+ "prodid": DEFAULT_TYPE_TEXT,
+ "related-to": DEFAULT_TYPE_TEXT,
+ "repeat": DEFAULT_TYPE_INTEGER,
+ "rdate": {
+ defaultType: "date-time",
+ allowedTypes: ["date-time", "date", "period"],
+ multiValue: ',',
+ detectType: function(string) {
+ if (string.indexOf('/') !== -1) {
+ return 'period';
+ }
+ return (string.indexOf('T') === -1) ? 'date' : 'date-time';
+ }
+ },
+ "recurrence-id": DEFAULT_TYPE_DATETIME_DATE,
+ "resources": DEFAULT_TYPE_TEXT_MULTI,
+ "request-status": DEFAULT_TYPE_TEXT_STRUCTURED,
+ "rrule": DEFAULT_TYPE_RECUR,
+ "sequence": DEFAULT_TYPE_INTEGER,
+ "status": DEFAULT_TYPE_TEXT,
+ "summary": DEFAULT_TYPE_TEXT,
+ "transp": DEFAULT_TYPE_TEXT,
+ "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] },
+ "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET,
+ "tzoffsetto": DEFAULT_TYPE_UTCOFFSET,
+ "tzurl": DEFAULT_TYPE_URI,
+ "tzid": DEFAULT_TYPE_TEXT,
+ "tzname": DEFAULT_TYPE_TEXT
+ });
+
+ // When adding a value here, be sure to add it to the parameter types!
+ var vcardValues = ICAL.helpers.extend(commonValues, {
+ text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
+ uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
+
+ date: {
+ decorate: function(aValue) {
+ return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date");
+ },
+ undecorate: function(aValue) {
+ return aValue.toString();
+ },
+ fromICAL: function(aValue) {
+ if (aValue.length == 8) {
+ return icalValues.date.fromICAL(aValue);
+ } else if (aValue[0] == '-' && aValue.length == 6) {
+ return aValue.substr(0, 4) + '-' + aValue.substr(4);
+ } else {
+ return aValue;
+ }
+ },
+ toICAL: function(aValue) {
+ if (aValue.length == 10) {
+ return icalValues.date.toICAL(aValue);
+ } else if (aValue[0] == '-' && aValue.length == 7) {
+ return aValue.substr(0, 4) + aValue.substr(5);
+ } else {
+ return aValue;
+ }
+ }
+ },
+
+ time: {
+ decorate: function(aValue) {
+ return ICAL.VCardTime.fromDateAndOrTimeString("T" + aValue, "time");
+ },
+ undecorate: function(aValue) {
+ return aValue.toString();
+ },
+ fromICAL: function(aValue) {
+ var splitzone = vcardValues.time._splitZone(aValue, true);
+ var zone = splitzone[0], value = splitzone[1];
+
+ //console.log("SPLIT: ",splitzone);
+
+ if (value.length == 6) {
+ value = value.substr(0, 2) + ':' +
+ value.substr(2, 2) + ':' +
+ value.substr(4, 2);
+ } else if (value.length == 4 && value[0] != '-') {
+ value = value.substr(0, 2) + ':' + value.substr(2, 2);
+ } else if (value.length == 5) {
+ value = value.substr(0, 3) + ':' + value.substr(3, 2);
+ }
+
+ if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) {
+ zone = zone.substr(0, 3) + ':' + zone.substr(3);
+ }
+
+ return value + zone;
+ },
+
+ toICAL: function(aValue) {
+ var splitzone = vcardValues.time._splitZone(aValue);
+ var zone = splitzone[0], value = splitzone[1];
+
+ if (value.length == 8) {
+ value = value.substr(0, 2) +
+ value.substr(3, 2) +
+ value.substr(6, 2);
+ } else if (value.length == 5 && value[0] != '-') {
+ value = value.substr(0, 2) + value.substr(3, 2);
+ } else if (value.length == 6) {
+ value = value.substr(0, 3) + value.substr(4, 2);
+ }
+
+ if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) {
+ zone = zone.substr(0, 3) + zone.substr(4);
+ }
+
+ return value + zone;
+ },
+
+ _splitZone: function(aValue, isFromIcal) {
+ var lastChar = aValue.length - 1;
+ var signChar = aValue.length - (isFromIcal ? 5 : 6);
+ var sign = aValue[signChar];
+ var zone, value;
+
+ if (aValue[lastChar] == 'Z') {
+ zone = aValue[lastChar];
+ value = aValue.substr(0, lastChar);
+ } else if (aValue.length > 6 && (sign == '-' || sign == '+')) {
+ zone = aValue.substr(signChar);
+ value = aValue.substr(0, signChar);
+ } else {
+ zone = "";
+ value = aValue;
+ }
+
+ return [zone, value];
+ }
+ },
+
+ "date-time": {
+ decorate: function(aValue) {
+ return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-time");
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toString();
+ },
+
+ fromICAL: function(aValue) {
+ return vcardValues['date-and-or-time'].fromICAL(aValue);
+ },
+
+ toICAL: function(aValue) {
+ return vcardValues['date-and-or-time'].toICAL(aValue);
+ }
+ },
+
+ "date-and-or-time": {
+ decorate: function(aValue) {
+ return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time");
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toString();
+ },
+
+ fromICAL: function(aValue) {
+ var parts = aValue.split('T');
+ return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') +
+ (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : '');
+ },
+
+ toICAL: function(aValue) {
+ var parts = aValue.split('T');
+ return vcardValues.date.toICAL(parts[0]) +
+ (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : '');
+
+ }
+ },
+ timestamp: icalValues['date-time'],
+ "language-tag": {
+ matches: /^[a-zA-Z0-9\-]+$/ // Could go with a more strict regex here
+ }
+ });
+
+ var vcardParams = {
+ "type": {
+ valueType: "text",
+ multiValue: ","
+ },
+ "value": {
+ // since the value here is a 'type' lowercase is used.
+ values: ["text", "uri", "date", "time", "date-time", "date-and-or-time",
+ "timestamp", "boolean", "integer", "float", "utc-offset",
+ "language-tag"],
+ allowXName: true,
+ allowIanaToken: true
+ }
+ };
+
+ var vcardProperties = ICAL.helpers.extend(commonProperties, {
+ "adr": { defaultType: "text", structuredValue: ";", multiValue: "," },
+ "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME,
+ "bday": DEFAULT_TYPE_DATE_ANDOR_TIME,
+ "caladruri": DEFAULT_TYPE_URI,
+ "caluri": DEFAULT_TYPE_URI,
+ "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED,
+ "email": DEFAULT_TYPE_TEXT,
+ "fburl": DEFAULT_TYPE_URI,
+ "fn": DEFAULT_TYPE_TEXT,
+ "gender": DEFAULT_TYPE_TEXT_STRUCTURED,
+ "geo": DEFAULT_TYPE_URI,
+ "impp": DEFAULT_TYPE_URI,
+ "key": DEFAULT_TYPE_URI,
+ "kind": DEFAULT_TYPE_TEXT,
+ "lang": { defaultType: "language-tag" },
+ "logo": DEFAULT_TYPE_URI,
+ "member": DEFAULT_TYPE_URI,
+ "n": { defaultType: "text", structuredValue: ";", multiValue: "," },
+ "nickname": DEFAULT_TYPE_TEXT_MULTI,
+ "note": DEFAULT_TYPE_TEXT,
+ "org": { defaultType: "text", structuredValue: ";" },
+ "photo": DEFAULT_TYPE_URI,
+ "related": DEFAULT_TYPE_URI,
+ "rev": { defaultType: "timestamp" },
+ "role": DEFAULT_TYPE_TEXT,
+ "sound": DEFAULT_TYPE_URI,
+ "source": DEFAULT_TYPE_URI,
+ "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] },
+ "title": DEFAULT_TYPE_TEXT,
+ "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] },
+ "xml": DEFAULT_TYPE_TEXT
+ });
+
+ var vcard3Values = ICAL.helpers.extend(commonValues, {
+ binary: icalValues.binary,
+ date: vcardValues.date,
+ "date-time": vcardValues["date-time"],
+ "phone-number": {
+ // TODO
+ /* ... */
+ },
+ uri: icalValues.uri,
+ text: icalValues.text,
+ time: icalValues.time,
+ vcard: icalValues.text,
+ "utc-offset": {
+ toICAL: function(aValue) {
+ return aValue.substr(0, 7);
+ },
+
+ fromICAL: function(aValue) {
+ return aValue.substr(0, 7);
+ },
+
+ decorate: function(aValue) {
+ return ICAL.UtcOffset.fromString(aValue);
+ },
+
+ undecorate: function(aValue) {
+ return aValue.toString();
+ }
+ }
+ });
+
+ var vcard3Params = {
+ "type": {
+ valueType: "text",
+ multiValue: ","
+ },
+ "value": {
+ // since the value here is a 'type' lowercase is used.
+ values: ["text", "uri", "date", "date-time", "phone-number", "time",
+ "boolean", "integer", "float", "utc-offset", "vcard", "binary"],
+ allowXName: true,
+ allowIanaToken: true
+ }
+ };
+
+ var vcard3Properties = ICAL.helpers.extend(commonProperties, {
+ fn: DEFAULT_TYPE_TEXT,
+ n: { defaultType: "text", structuredValue: ";", multiValue: "," },
+ nickname: DEFAULT_TYPE_TEXT_MULTI,
+ photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
+ bday: {
+ defaultType: "date-time",
+ allowedTypes: ["date-time", "date"],
+ detectType: function(string) {
+ return (string.indexOf('T') === -1) ? 'date' : 'date-time';
+ }
+ },
+
+ adr: { defaultType: "text", structuredValue: ";", multiValue: "," },
+ label: DEFAULT_TYPE_TEXT,
+
+ tel: { defaultType: "phone-number" },
+ email: DEFAULT_TYPE_TEXT,
+ mailer: DEFAULT_TYPE_TEXT,
+
+ tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] },
+ geo: { defaultType: "float", structuredValue: ";" },
+
+ title: DEFAULT_TYPE_TEXT,
+ role: DEFAULT_TYPE_TEXT,
+ logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
+ agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] },
+ org: DEFAULT_TYPE_TEXT_STRUCTURED,
+
+ note: DEFAULT_TYPE_TEXT_MULTI,
+ prodid: DEFAULT_TYPE_TEXT,
+ rev: {
+ defaultType: "date-time",
+ allowedTypes: ["date-time", "date"],
+ detectType: function(string) {
+ return (string.indexOf('T') === -1) ? 'date' : 'date-time';
+ }
+ },
+ "sort-string": DEFAULT_TYPE_TEXT,
+ sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
+
+ class: DEFAULT_TYPE_TEXT,
+ key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
+ });
+
+ /**
+ * iCalendar design set
+ * @type {ICAL.design.designSet}
+ */
+ var icalSet = {
+ value: icalValues,
+ param: icalParams,
+ property: icalProperties
+ };
+
+ /**
+ * vCard 4.0 design set
+ * @type {ICAL.design.designSet}
+ */
+ var vcardSet = {
+ value: vcardValues,
+ param: vcardParams,
+ property: vcardProperties
+ };
+
+ /**
+ * vCard 3.0 design set
+ * @type {ICAL.design.designSet}
+ */
+ var vcard3Set = {
+ value: vcard3Values,
+ param: vcard3Params,
+ property: vcard3Properties
+ };
+
+ /**
+ * The design data, used by the parser to determine types for properties and
+ * other metadata needed to produce correct jCard/jCal data.
+ *
+ * @alias ICAL.design
+ * @namespace
+ */
+ var design = {
+ /**
+ * A designSet describes value, parameter and property data. It is used by
+ * ther parser and stringifier in components and properties to determine they
+ * should be represented.
+ *
+ * @typedef {Object} designSet
+ * @memberOf ICAL.design
+ * @property {Object} value Definitions for value types, keys are type names
+ * @property {Object} param Definitions for params, keys are param names
+ * @property {Object} property Defintions for properties, keys are property names
+ */
+
+
+ /**
+ * The default set for new properties and components if none is specified.
+ * @type {ICAL.design.designSet}
+ */
+ defaultSet: icalSet,
+
+ /**
+ * The default type for unknown properties
+ * @type {String}
+ */
+ defaultType: 'unknown',
+
+ /**
+ * Holds the design set for known top-level components
+ *
+ * @type {Object}
+ * @property {ICAL.design.designSet} vcard vCard VCARD
+ * @property {ICAL.design.designSet} vevent iCalendar VEVENT
+ * @property {ICAL.design.designSet} vtodo iCalendar VTODO
+ * @property {ICAL.design.designSet} vjournal iCalendar VJOURNAL
+ * @property {ICAL.design.designSet} valarm iCalendar VALARM
+ * @property {ICAL.design.designSet} vtimezone iCalendar VTIMEZONE
+ * @property {ICAL.design.designSet} daylight iCalendar DAYLIGHT
+ * @property {ICAL.design.designSet} standard iCalendar STANDARD
+ *
+ * @example
+ * var propertyName = 'fn';
+ * var componentDesign = ICAL.design.components.vcard;
+ * var propertyDetails = componentDesign.property[propertyName];
+ * if (propertyDetails.defaultType == 'text') {
+ * // Yep, sure is...
+ * }
+ */
+ components: {
+ vcard: vcardSet,
+ vcard3: vcard3Set,
+ vevent: icalSet,
+ vtodo: icalSet,
+ vjournal: icalSet,
+ valarm: icalSet,
+ vtimezone: icalSet,
+ daylight: icalSet,
+ standard: icalSet
+ },
+
+
+ /**
+ * The design set for iCalendar (rfc5545/rfc7265) components.
+ * @type {ICAL.design.designSet}
+ */
+ icalendar: icalSet,
+
+ /**
+ * The design set for vCard (rfc6350/rfc7095) components.
+ * @type {ICAL.design.designSet}
+ */
+ vcard: vcardSet,
+
+ /**
+ * The design set for vCard (rfc2425/rfc2426/rfc7095) components.
+ * @type {ICAL.design.designSet}
+ */
+ vcard3: vcard3Set,
+
+ /**
+ * Gets the design set for the given component name.
+ *
+ * @param {String} componentName The name of the component
+ * @return {ICAL.design.designSet} The design set for the component
+ */
+ getDesignSet: function(componentName) {
+ var isInDesign = componentName && componentName in design.components;
+ return isInDesign ? design.components[componentName] : design.defaultSet;
+ }
+ };
+
+ return design;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * Contains various functions to convert jCal and jCard data back into
+ * iCalendar and vCard.
+ * @namespace
+ */
+ICAL.stringify = (function() {
+ 'use strict';
+
+ var LINE_ENDING = '\r\n';
+ var DEFAULT_VALUE_TYPE = 'unknown';
+
+ var design = ICAL.design;
+ var helpers = ICAL.helpers;
+
+ /**
+ * Convert a full jCal/jCard array into a iCalendar/vCard string.
+ *
+ * @function ICAL.stringify
+ * @variation function
+ * @param {Array} jCal The jCal/jCard document
+ * @return {String} The stringified iCalendar/vCard document
+ */
+ function stringify(jCal) {
+ if (typeof jCal[0] == "string") {
+ // This is a single component
+ jCal = [jCal];
+ }
+
+ var i = 0;
+ var len = jCal.length;
+ var result = '';
+
+ for (; i < len; i++) {
+ result += stringify.component(jCal[i]) + LINE_ENDING;
+ }
+
+ return result;
+ }
+
+ /**
+ * Converts an jCal component array into a ICAL string.
+ * Recursive will resolve sub-components.
+ *
+ * Exact component/property order is not saved all
+ * properties will come before subcomponents.
+ *
+ * @function ICAL.stringify.component
+ * @param {Array} component
+ * jCal/jCard fragment of a component
+ * @param {ICAL.design.designSet} designSet
+ * The design data to use for this component
+ * @return {String} The iCalendar/vCard string
+ */
+ stringify.component = function(component, designSet) {
+ var name = component[0].toUpperCase();
+ var result = 'BEGIN:' + name + LINE_ENDING;
+
+ var props = component[1];
+ var propIdx = 0;
+ var propLen = props.length;
+
+ var designSetName = component[0];
+ // rfc6350 requires that in vCard 4.0 the first component is the VERSION
+ // component with as value 4.0, note that 3.0 does not have this requirement.
+ if (designSetName === 'vcard' && component[1].length > 0 &&
+ !(component[1][0][0] === "version" && component[1][0][3] === "4.0")) {
+ designSetName = "vcard3";
+ }
+ designSet = designSet || design.getDesignSet(designSetName);
+
+ for (; propIdx < propLen; propIdx++) {
+ result += stringify.property(props[propIdx], designSet) + LINE_ENDING;
+ }
+
+ var comps = component[2];
+ var compIdx = 0;
+ var compLen = comps.length;
+
+ for (; compIdx < compLen; compIdx++) {
+ result += stringify.component(comps[compIdx], designSet) + LINE_ENDING;
+ }
+
+ result += 'END:' + name;
+ return result;
+ };
+
+ /**
+ * Converts a single jCal/jCard property to a iCalendar/vCard string.
+ *
+ * @function ICAL.stringify.property
+ * @param {Array} property
+ * jCal/jCard property array
+ * @param {ICAL.design.designSet} designSet
+ * The design data to use for this property
+ * @param {Boolean} noFold
+ * If true, the line is not folded
+ * @return {String} The iCalendar/vCard string
+ */
+ stringify.property = function(property, designSet, noFold) {
+ var name = property[0].toUpperCase();
+ var jsName = property[0];
+ var params = property[1];
+
+ var line = name;
+
+ var paramName;
+ for (paramName in params) {
+ var value = params[paramName];
+
+ /* istanbul ignore else */
+ if (params.hasOwnProperty(paramName)) {
+ var multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue;
+ if (multiValue && Array.isArray(value)) {
+ if (designSet.param[paramName].multiValueSeparateDQuote) {
+ multiValue = '"' + multiValue + '"';
+ }
+ value = value.map(stringify._rfc6868Unescape);
+ value = stringify.multiValue(value, multiValue, "unknown", null, designSet);
+ } else {
+ value = stringify._rfc6868Unescape(value);
+ }
+
+
+ line += ';' + paramName.toUpperCase();
+ line += '=' + stringify.propertyValue(value);
+ }
+ }
+
+ if (property.length === 3) {
+ // If there are no values, we must assume a blank value
+ return line + ':';
+ }
+
+ var valueType = property[2];
+
+ if (!designSet) {
+ designSet = design.defaultSet;
+ }
+
+ var propDetails;
+ var multiValue = false;
+ var structuredValue = false;
+ var isDefault = false;
+
+ if (jsName in designSet.property) {
+ propDetails = designSet.property[jsName];
+
+ if ('multiValue' in propDetails) {
+ multiValue = propDetails.multiValue;
+ }
+
+ if (('structuredValue' in propDetails) && Array.isArray(property[3])) {
+ structuredValue = propDetails.structuredValue;
+ }
+
+ if ('defaultType' in propDetails) {
+ if (valueType === propDetails.defaultType) {
+ isDefault = true;
+ }
+ } else {
+ if (valueType === DEFAULT_VALUE_TYPE) {
+ isDefault = true;
+ }
+ }
+ } else {
+ if (valueType === DEFAULT_VALUE_TYPE) {
+ isDefault = true;
+ }
+ }
+
+ // push the VALUE property if type is not the default
+ // for the current property.
+ if (!isDefault) {
+ // value will never contain ;/:/, so we don't escape it here.
+ line += ';VALUE=' + valueType.toUpperCase();
+ }
+
+ line += ':';
+
+ if (multiValue && structuredValue) {
+ line += stringify.multiValue(
+ property[3], structuredValue, valueType, multiValue, designSet, structuredValue
+ );
+ } else if (multiValue) {
+ line += stringify.multiValue(
+ property.slice(3), multiValue, valueType, null, designSet, false
+ );
+ } else if (structuredValue) {
+ line += stringify.multiValue(
+ property[3], structuredValue, valueType, null, designSet, structuredValue
+ );
+ } else {
+ line += stringify.value(property[3], valueType, designSet, false);
+ }
+
+ return noFold ? line : ICAL.helpers.foldline(line);
+ };
+
+ /**
+ * Handles escaping of property values that may contain:
+ *
+ * COLON (:), SEMICOLON (;), or COMMA (,)
+ *
+ * If any of the above are present the result is wrapped
+ * in double quotes.
+ *
+ * @function ICAL.stringify.propertyValue
+ * @param {String} value Raw property value
+ * @return {String} Given or escaped value when needed
+ */
+ stringify.propertyValue = function(value) {
+
+ if ((helpers.unescapedIndexOf(value, ',') === -1) &&
+ (helpers.unescapedIndexOf(value, ':') === -1) &&
+ (helpers.unescapedIndexOf(value, ';') === -1)) {
+
+ return value;
+ }
+
+ return '"' + value + '"';
+ };
+
+ /**
+ * Converts an array of ical values into a single
+ * string based on a type and a delimiter value (like ",").
+ *
+ * @function ICAL.stringify.multiValue
+ * @param {Array} values List of values to convert
+ * @param {String} delim Used to join the values (",", ";", ":")
+ * @param {String} type Lowecase ical value type
+ * (like boolean, date-time, etc..)
+ * @param {?String} innerMulti If set, each value will again be processed
+ * Used for structured values
+ * @param {ICAL.design.designSet} designSet
+ * The design data to use for this property
+ *
+ * @return {String} iCalendar/vCard string for value
+ */
+ stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) {
+ var result = '';
+ var len = values.length;
+ var i = 0;
+
+ for (; i < len; i++) {
+ if (innerMulti && Array.isArray(values[i])) {
+ result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue);
+ } else {
+ result += stringify.value(values[i], type, designSet, structuredValue);
+ }
+
+ if (i !== (len - 1)) {
+ result += delim;
+ }
+ }
+
+ return result;
+ };
+
+ /**
+ * Processes a single ical value runs the associated "toICAL" method from the
+ * design value type if available to convert the value.
+ *
+ * @function ICAL.stringify.value
+ * @param {String|Number} value A formatted value
+ * @param {String} type Lowercase iCalendar/vCard value type
+ * (like boolean, date-time, etc..)
+ * @return {String} iCalendar/vCard value for single value
+ */
+ stringify.value = function(value, type, designSet, structuredValue) {
+ if (type in designSet.value && 'toICAL' in designSet.value[type]) {
+ return designSet.value[type].toICAL(value, structuredValue);
+ }
+ return value;
+ };
+
+ /**
+ * Internal helper for rfc6868. Exposing this on ICAL.stringify so that
+ * hackers can disable the rfc6868 parsing if the really need to.
+ *
+ * @param {String} val The value to unescape
+ * @return {String} The escaped value
+ */
+ stringify._rfc6868Unescape = function(val) {
+ return val.replace(/[\n^"]/g, function(x) {
+ return RFC6868_REPLACE_MAP[x];
+ });
+ };
+ var RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" };
+
+ return stringify;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * Contains various functions to parse iCalendar and vCard data.
+ * @namespace
+ */
+ICAL.parse = (function() {
+ 'use strict';
+
+ var CHAR = /[^ \t]/;
+ var MULTIVALUE_DELIMITER = ',';
+ var VALUE_DELIMITER = ':';
+ var PARAM_DELIMITER = ';';
+ var PARAM_NAME_DELIMITER = '=';
+ var DEFAULT_VALUE_TYPE = 'unknown';
+ var DEFAULT_PARAM_TYPE = 'text';
+
+ var design = ICAL.design;
+ var helpers = ICAL.helpers;
+
+ /**
+ * An error that occurred during parsing.
+ *
+ * @param {String} message The error message
+ * @memberof ICAL.parse
+ * @extends {Error}
+ * @class
+ */
+ function ParserError(message) {
+ this.message = message;
+ this.name = 'ParserError';
+
+ try {
+ throw new Error();
+ } catch (e) {
+ if (e.stack) {
+ var split = e.stack.split('\n');
+ split.shift();
+ this.stack = split.join('\n');
+ }
+ }
+ }
+
+ ParserError.prototype = Error.prototype;
+
+ /**
+ * Parses iCalendar or vCard data into a raw jCal object. Consult
+ * documentation on the {@tutorial layers|layers of parsing} for more
+ * details.
+ *
+ * @function ICAL.parse
+ * @variation function
+ * @todo Fix the API to be more clear on the return type
+ * @param {String} input The string data to parse
+ * @return {Object|Object[]} A single jCal object, or an array thereof
+ */
+ function parser(input) {
+ var state = {};
+ var root = state.component = [];
+
+ state.stack = [root];
+
+ parser._eachLine(input, function(err, line) {
+ parser._handleContentLine(line, state);
+ });
+
+
+ // when there are still items on the stack
+ // throw a fatal error, a component was not closed
+ // correctly in that case.
+ if (state.stack.length > 1) {
+ throw new ParserError(
+ 'invalid ical body. component began but did not end'
+ );
+ }
+
+ state = null;
+
+ return (root.length == 1 ? root[0] : root);
+ }
+
+ /**
+ * Parse an iCalendar property value into the jCal for a single property
+ *
+ * @function ICAL.parse.property
+ * @param {String} str
+ * The iCalendar property string to parse
+ * @param {ICAL.design.designSet=} designSet
+ * The design data to use for this property
+ * @return {Object}
+ * The jCal Object containing the property
+ */
+ parser.property = function(str, designSet) {
+ var state = {
+ component: [[], []],
+ designSet: designSet || design.defaultSet
+ };
+ parser._handleContentLine(str, state);
+ return state.component[1][0];
+ };
+
+ /**
+ * Convenience method to parse a component. You can use ICAL.parse() directly
+ * instead.
+ *
+ * @function ICAL.parse.component
+ * @see ICAL.parse(function)
+ * @param {String} str The iCalendar component string to parse
+ * @return {Object} The jCal Object containing the component
+ */
+ parser.component = function(str) {
+ return parser(str);
+ };
+
+ // classes & constants
+ parser.ParserError = ParserError;
+
+ /**
+ * The state for parsing content lines from an iCalendar/vCard string.
+ *
+ * @private
+ * @memberof ICAL.parse
+ * @typedef {Object} parserState
+ * @property {ICAL.design.designSet} designSet The design set to use for parsing
+ * @property {ICAL.Component[]} stack The stack of components being processed
+ * @property {ICAL.Component} component The currently active component
+ */
+
+
+ /**
+ * Handles a single line of iCalendar/vCard, updating the state.
+ *
+ * @private
+ * @function ICAL.parse._handleContentLine
+ * @param {String} line The content line to process
+ * @param {ICAL.parse.parserState} The current state of the line parsing
+ */
+ parser._handleContentLine = function(line, state) {
+ // break up the parts of the line
+ var valuePos = line.indexOf(VALUE_DELIMITER);
+ var paramPos = line.indexOf(PARAM_DELIMITER);
+
+ var lastParamIndex;
+ var lastValuePos;
+
+ // name of property or begin/end
+ var name;
+ var value;
+ // params is only overridden if paramPos !== -1.
+ // we can't do params = params || {} later on
+ // because it sacrifices ops.
+ var params = {};
+
+ /**
+ * Different property cases
+ *
+ *
+ * 1. RRULE:FREQ=foo
+ * // FREQ= is not a param but the value
+ *
+ * 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
+ * // ROLE= is a param because : has not happened yet
+ */
+ // when the parameter delimiter is after the
+ // value delimiter then its not a parameter.
+
+ if ((paramPos !== -1 && valuePos !== -1)) {
+ // when the parameter delimiter is after the
+ // value delimiter then its not a parameter.
+ if (paramPos > valuePos) {
+ paramPos = -1;
+ }
+ }
+
+ var parsedParams;
+ if (paramPos !== -1) {
+ name = line.substring(0, paramPos).toLowerCase();
+ parsedParams = parser._parseParameters(line.substring(paramPos), 0, state.designSet);
+ if (parsedParams[2] == -1) {
+ throw new ParserError("Invalid parameters in '" + line + "'");
+ }
+ params = parsedParams[0];
+ lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
+ if ((lastValuePos =
+ line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
+ value = line.substring(lastParamIndex + lastValuePos + 1);
+ } else {
+ throw new ParserError("Missing parameter value in '" + line + "'");
+ }
+ } else if (valuePos !== -1) {
+ // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
+ name = line.substring(0, valuePos).toLowerCase();
+ value = line.substring(valuePos + 1);
+
+ if (name === 'begin') {
+ var newComponent = [value.toLowerCase(), [], []];
+ if (state.stack.length === 1) {
+ state.component.push(newComponent);
+ } else {
+ state.component[2].push(newComponent);
+ }
+ state.stack.push(state.component);
+ state.component = newComponent;
+ if (!state.designSet) {
+ state.designSet = design.getDesignSet(state.component[0]);
+ }
+ return;
+ } else if (name === 'end') {
+ state.component = state.stack.pop();
+ return;
+ }
+ // If its not begin/end, then this is a property with an empty value,
+ // which should be considered valid.
+ } else {
+ /**
+ * Invalid line.
+ * The rational to throw an error is we will
+ * never be certain that the rest of the file
+ * is sane and its unlikely that we can serialize
+ * the result correctly either.
+ */
+ throw new ParserError(
+ 'invalid line (no token ";" or ":") "' + line + '"'
+ );
+ }
+
+ var valueType;
+ var multiValue = false;
+ var structuredValue = false;
+ var propertyDetails;
+
+ if (name in state.designSet.property) {
+ propertyDetails = state.designSet.property[name];
+
+ if ('multiValue' in propertyDetails) {
+ multiValue = propertyDetails.multiValue;
+ }
+
+ if ('structuredValue' in propertyDetails) {
+ structuredValue = propertyDetails.structuredValue;
+ }
+
+ if (value && 'detectType' in propertyDetails) {
+ valueType = propertyDetails.detectType(value);
+ }
+ }
+
+ // attempt to determine value
+ if (!valueType) {
+ if (!('value' in params)) {
+ if (propertyDetails) {
+ valueType = propertyDetails.defaultType;
+ } else {
+ valueType = DEFAULT_VALUE_TYPE;
+ }
+ } else {
+ // possible to avoid this?
+ valueType = params.value.toLowerCase();
+ }
+ }
+
+ delete params.value;
+
+ /**
+ * Note on `var result` juggling:
+ *
+ * I observed that building the array in pieces has adverse
+ * effects on performance, so where possible we inline the creation.
+ * Its a little ugly but resulted in ~2000 additional ops/sec.
+ */
+
+ var result;
+ if (multiValue && structuredValue) {
+ value = parser._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue);
+ result = [name, params, valueType, value];
+ } else if (multiValue) {
+ result = [name, params, valueType];
+ parser._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false);
+ } else if (structuredValue) {
+ value = parser._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue);
+ result = [name, params, valueType, value];
+ } else {
+ value = parser._parseValue(value, valueType, state.designSet, false);
+ result = [name, params, valueType, value];
+ }
+ // rfc6350 requires that in vCard 4.0 the first component is the VERSION
+ // component with as value 4.0, note that 3.0 does not have this requirement.
+ if (state.component[0] === 'vcard' && state.component[1].length === 0 &&
+ !(name === 'version' && value === '4.0')) {
+ state.designSet = design.getDesignSet("vcard3");
+ }
+ state.component[1].push(result);
+ };
+
+ /**
+ * Parse a value from the raw value into the jCard/jCal value.
+ *
+ * @private
+ * @function ICAL.parse._parseValue
+ * @param {String} value Original value
+ * @param {String} type Type of value
+ * @param {Object} designSet The design data to use for this value
+ * @return {Object} varies on type
+ */
+ parser._parseValue = function(value, type, designSet, structuredValue) {
+ if (type in designSet.value && 'fromICAL' in designSet.value[type]) {
+ return designSet.value[type].fromICAL(value, structuredValue);
+ }
+ return value;
+ };
+
+ /**
+ * Parse parameters from a string to object.
+ *
+ * @function ICAL.parse._parseParameters
+ * @private
+ * @param {String} line A single unfolded line
+ * @param {Numeric} start Position to start looking for properties
+ * @param {Object} designSet The design data to use for this property
+ * @return {Object} key/value pairs
+ */
+ parser._parseParameters = function(line, start, designSet) {
+ var lastParam = start;
+ var pos = 0;
+ var delim = PARAM_NAME_DELIMITER;
+ var result = {};
+ var name, lcname;
+ var value, valuePos = -1;
+ var type, multiValue, mvdelim;
+
+ // find the next '=' sign
+ // use lastParam and pos to find name
+ // check if " is used if so get value from "->"
+ // then increment pos to find next ;
+
+ while ((pos !== false) &&
+ (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) {
+
+ name = line.substr(lastParam + 1, pos - lastParam - 1);
+ if (name.length == 0) {
+ throw new ParserError("Empty parameter name in '" + line + "'");
+ }
+ lcname = name.toLowerCase();
+
+ if (lcname in designSet.param && designSet.param[lcname].valueType) {
+ type = designSet.param[lcname].valueType;
+ } else {
+ type = DEFAULT_PARAM_TYPE;
+ }
+
+ if (lcname in designSet.param) {
+ multiValue = designSet.param[lcname].multiValue;
+ if (designSet.param[lcname].multiValueSeparateDQuote) {
+ mvdelim = parser._rfc6868Escape('"' + multiValue + '"');
+ }
+ }
+
+ var nextChar = line[pos + 1];
+ if (nextChar === '"') {
+ valuePos = pos + 2;
+ pos = helpers.unescapedIndexOf(line, '"', valuePos);
+ if (multiValue && pos != -1) {
+ var extendedValue = true;
+ while (extendedValue) {
+ if (line[pos + 1] == multiValue && line[pos + 2] == '"') {
+ pos = helpers.unescapedIndexOf(line, '"', pos + 3);
+ } else {
+ extendedValue = false;
+ }
+ }
+ }
+ if (pos === -1) {
+ throw new ParserError(
+ 'invalid line (no matching double quote) "' + line + '"'
+ );
+ }
+ value = line.substr(valuePos, pos - valuePos);
+ lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
+ if (lastParam === -1) {
+ pos = false;
+ }
+ } else {
+ valuePos = pos + 1;
+
+ // move to next ";"
+ var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos);
+ var propValuePos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos);
+ if (propValuePos !== -1 && nextPos > propValuePos) {
+ // this is a delimiter in the property value, let's stop here
+ nextPos = propValuePos;
+ pos = false;
+ } else if (nextPos === -1) {
+ // no ";"
+ if (propValuePos === -1) {
+ nextPos = line.length;
+ } else {
+ nextPos = propValuePos;
+ }
+ pos = false;
+ } else {
+ lastParam = nextPos;
+ pos = nextPos;
+ }
+
+ value = line.substr(valuePos, nextPos - valuePos);
+ }
+
+ value = parser._rfc6868Escape(value);
+ if (multiValue) {
+ var delimiter = mvdelim || multiValue;
+ result[lcname] = parser._parseMultiValue(value, delimiter, type, [], null, designSet);
+ } else {
+ result[lcname] = parser._parseValue(value, type, designSet);
+ }
+ }
+ return [result, value, valuePos];
+ };
+
+ /**
+ * Internal helper for rfc6868. Exposing this on ICAL.parse so that
+ * hackers can disable the rfc6868 parsing if the really need to.
+ *
+ * @function ICAL.parse._rfc6868Escape
+ * @param {String} val The value to escape
+ * @return {String} The escaped value
+ */
+ parser._rfc6868Escape = function(val) {
+ return val.replace(/\^['n^]/g, function(x) {
+ return RFC6868_REPLACE_MAP[x];
+ });
+ };
+ var RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" };
+
+ /**
+ * Parse a multi value string. This function is used either for parsing
+ * actual multi-value property's values, or for handling parameter values. It
+ * can be used for both multi-value properties and structured value properties.
+ *
+ * @private
+ * @function ICAL.parse._parseMultiValue
+ * @param {String} buffer The buffer containing the full value
+ * @param {String} delim The multi-value delimiter
+ * @param {String} type The value type to be parsed
+ * @param {Array.<?>} result The array to append results to, varies on value type
+ * @param {String} innerMulti The inner delimiter to split each value with
+ * @param {ICAL.design.designSet} designSet The design data for this value
+ * @return {?|Array.<?>} Either an array of results, or the first result
+ */
+ parser._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) {
+ var pos = 0;
+ var lastPos = 0;
+ var value;
+ if (delim.length === 0) {
+ return buffer;
+ }
+
+ // split each piece
+ while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
+ value = buffer.substr(lastPos, pos - lastPos);
+ if (innerMulti) {
+ value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
+ } else {
+ value = parser._parseValue(value, type, designSet, structuredValue);
+ }
+ result.push(value);
+ lastPos = pos + delim.length;
+ }
+
+ // on the last piece take the rest of string
+ value = buffer.substr(lastPos);
+ if (innerMulti) {
+ value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
+ } else {
+ value = parser._parseValue(value, type, designSet, structuredValue);
+ }
+ result.push(value);
+
+ return result.length == 1 ? result[0] : result;
+ };
+
+ /**
+ * Process a complete buffer of iCalendar/vCard data line by line, correctly
+ * unfolding content. Each line will be processed with the given callback
+ *
+ * @private
+ * @function ICAL.parse._eachLine
+ * @param {String} buffer The buffer to process
+ * @param {function(?String, String)} callback The callback for each line
+ */
+ parser._eachLine = function(buffer, callback) {
+ var len = buffer.length;
+ var lastPos = buffer.search(CHAR);
+ var pos = lastPos;
+ var line;
+ var firstChar;
+
+ var newlineOffset;
+
+ do {
+ pos = buffer.indexOf('\n', lastPos) + 1;
+
+ if (pos > 1 && buffer[pos - 2] === '\r') {
+ newlineOffset = 2;
+ } else {
+ newlineOffset = 1;
+ }
+
+ if (pos === 0) {
+ pos = len;
+ newlineOffset = 0;
+ }
+
+ firstChar = buffer[lastPos];
+
+ if (firstChar === ' ' || firstChar === '\t') {
+ // add to line
+ line += buffer.substr(
+ lastPos + 1,
+ pos - lastPos - (newlineOffset + 1)
+ );
+ } else {
+ if (line)
+ callback(null, line);
+ // push line
+ line = buffer.substr(
+ lastPos,
+ pos - lastPos - newlineOffset
+ );
+ }
+
+ lastPos = pos;
+ } while (pos !== len);
+
+ // extra ending line
+ line = line.trim();
+
+ if (line.length)
+ callback(null, line);
+ };
+
+ return parser;
+
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.Component = (function() {
+ 'use strict';
+
+ var PROPERTY_INDEX = 1;
+ var COMPONENT_INDEX = 2;
+ var NAME_INDEX = 0;
+
+ /**
+ * @classdesc
+ * Wraps a jCal component, adding convenience methods to add, remove and
+ * update subcomponents and properties.
+ *
+ * @class
+ * @alias ICAL.Component
+ * @param {Array|String} jCal Raw jCal component data OR name of new
+ * component
+ * @param {ICAL.Component} parent Parent component to associate
+ */
+ function Component(jCal, parent) {
+ if (typeof(jCal) === 'string') {
+ // jCal spec (name, properties, components)
+ jCal = [jCal, [], []];
+ }
+
+ // mostly for legacy reasons.
+ this.jCal = jCal;
+
+ this.parent = parent || null;
+ }
+
+ Component.prototype = {
+ /**
+ * Hydrated properties are inserted into the _properties array at the same
+ * position as in the jCal array, so its possible the array contains
+ * undefined values for unhydrdated properties. To avoid iterating the
+ * array when checking if all properties have been hydrated, we save the
+ * count here.
+ *
+ * @type {Number}
+ * @private
+ */
+ _hydratedPropertyCount: 0,
+
+ /**
+ * The same count as for _hydratedPropertyCount, but for subcomponents
+ *
+ * @type {Number}
+ * @private
+ */
+ _hydratedComponentCount: 0,
+
+ /**
+ * The name of this component
+ * @readonly
+ */
+ get name() {
+ return this.jCal[NAME_INDEX];
+ },
+
+ /**
+ * The design set for this component, e.g. icalendar vs vcard
+ *
+ * @type {ICAL.design.designSet}
+ * @private
+ */
+ get _designSet() {
+ var parentDesign = this.parent && this.parent._designSet;
+ return parentDesign || ICAL.design.getDesignSet(this.name);
+ },
+
+ _hydrateComponent: function(index) {
+ if (!this._components) {
+ this._components = [];
+ this._hydratedComponentCount = 0;
+ }
+
+ if (this._components[index]) {
+ return this._components[index];
+ }
+
+ var comp = new Component(
+ this.jCal[COMPONENT_INDEX][index],
+ this
+ );
+
+ this._hydratedComponentCount++;
+ return (this._components[index] = comp);
+ },
+
+ _hydrateProperty: function(index) {
+ if (!this._properties) {
+ this._properties = [];
+ this._hydratedPropertyCount = 0;
+ }
+
+ if (this._properties[index]) {
+ return this._properties[index];
+ }
+
+ var prop = new ICAL.Property(
+ this.jCal[PROPERTY_INDEX][index],
+ this
+ );
+
+ this._hydratedPropertyCount++;
+ return (this._properties[index] = prop);
+ },
+
+ /**
+ * Finds first sub component, optionally filtered by name.
+ *
+ * @param {String=} name Optional name to filter by
+ * @return {?ICAL.Component} The found subcomponent
+ */
+ getFirstSubcomponent: function(name) {
+ if (name) {
+ var i = 0;
+ var comps = this.jCal[COMPONENT_INDEX];
+ var len = comps.length;
+
+ for (; i < len; i++) {
+ if (comps[i][NAME_INDEX] === name) {
+ var result = this._hydrateComponent(i);
+ return result;
+ }
+ }
+ } else {
+ if (this.jCal[COMPONENT_INDEX].length) {
+ return this._hydrateComponent(0);
+ }
+ }
+
+ // ensure we return a value (strict mode)
+ return null;
+ },
+
+ /**
+ * Finds all sub components, optionally filtering by name.
+ *
+ * @param {String=} name Optional name to filter by
+ * @return {ICAL.Component[]} The found sub components
+ */
+ getAllSubcomponents: function(name) {
+ var jCalLen = this.jCal[COMPONENT_INDEX].length;
+ var i = 0;
+
+ if (name) {
+ var comps = this.jCal[COMPONENT_INDEX];
+ var result = [];
+
+ for (; i < jCalLen; i++) {
+ if (name === comps[i][NAME_INDEX]) {
+ result.push(
+ this._hydrateComponent(i)
+ );
+ }
+ }
+ return result;
+ } else {
+ if (!this._components ||
+ (this._hydratedComponentCount !== jCalLen)) {
+ for (; i < jCalLen; i++) {
+ this._hydrateComponent(i);
+ }
+ }
+
+ return this._components || [];
+ }
+ },
+
+ /**
+ * Returns true when a named property exists.
+ *
+ * @param {String} name The property name
+ * @return {Boolean} True, when property is found
+ */
+ hasProperty: function(name) {
+ var props = this.jCal[PROPERTY_INDEX];
+ var len = props.length;
+
+ var i = 0;
+ for (; i < len; i++) {
+ // 0 is property name
+ if (props[i][NAME_INDEX] === name) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Finds the first property, optionally with the given name.
+ *
+ * @param {String=} name Lowercase property name
+ * @return {?ICAL.Property} The found property
+ */
+ getFirstProperty: function(name) {
+ if (name) {
+ var i = 0;
+ var props = this.jCal[PROPERTY_INDEX];
+ var len = props.length;
+
+ for (; i < len; i++) {
+ if (props[i][NAME_INDEX] === name) {
+ var result = this._hydrateProperty(i);
+ return result;
+ }
+ }
+ } else {
+ if (this.jCal[PROPERTY_INDEX].length) {
+ return this._hydrateProperty(0);
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns first property's value, if available.
+ *
+ * @param {String=} name Lowercase property name
+ * @return {?String} The found property value.
+ */
+ getFirstPropertyValue: function(name) {
+ var prop = this.getFirstProperty(name);
+ if (prop) {
+ return prop.getFirstValue();
+ }
+
+ return null;
+ },
+
+ /**
+ * Get all properties in the component, optionally filtered by name.
+ *
+ * @param {String=} name Lowercase property name
+ * @return {ICAL.Property[]} List of properties
+ */
+ getAllProperties: function(name) {
+ var jCalLen = this.jCal[PROPERTY_INDEX].length;
+ var i = 0;
+
+ if (name) {
+ var props = this.jCal[PROPERTY_INDEX];
+ var result = [];
+
+ for (; i < jCalLen; i++) {
+ if (name === props[i][NAME_INDEX]) {
+ result.push(
+ this._hydrateProperty(i)
+ );
+ }
+ }
+ return result;
+ } else {
+ if (!this._properties ||
+ (this._hydratedPropertyCount !== jCalLen)) {
+ for (; i < jCalLen; i++) {
+ this._hydrateProperty(i);
+ }
+ }
+
+ return this._properties || [];
+ }
+ },
+
+ _removeObjectByIndex: function(jCalIndex, cache, index) {
+ cache = cache || [];
+ // remove cached version
+ if (cache[index]) {
+ var obj = cache[index];
+ if ("parent" in obj) {
+ obj.parent = null;
+ }
+ }
+
+ cache.splice(index, 1);
+
+ // remove it from the jCal
+ this.jCal[jCalIndex].splice(index, 1);
+ },
+
+ _removeObject: function(jCalIndex, cache, nameOrObject) {
+ var i = 0;
+ var objects = this.jCal[jCalIndex];
+ var len = objects.length;
+ var cached = this[cache];
+
+ if (typeof(nameOrObject) === 'string') {
+ for (; i < len; i++) {
+ if (objects[i][NAME_INDEX] === nameOrObject) {
+ this._removeObjectByIndex(jCalIndex, cached, i);
+ return true;
+ }
+ }
+ } else if (cached) {
+ for (; i < len; i++) {
+ if (cached[i] && cached[i] === nameOrObject) {
+ this._removeObjectByIndex(jCalIndex, cached, i);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ _removeAllObjects: function(jCalIndex, cache, name) {
+ var cached = this[cache];
+
+ // Unfortunately we have to run through all children to reset their
+ // parent property.
+ var objects = this.jCal[jCalIndex];
+ var i = objects.length - 1;
+
+ // descending search required because splice
+ // is used and will effect the indices.
+ for (; i >= 0; i--) {
+ if (!name || objects[i][NAME_INDEX] === name) {
+ this._removeObjectByIndex(jCalIndex, cached, i);
+ }
+ }
+ },
+
+ /**
+ * Adds a single sub component.
+ *
+ * @param {ICAL.Component} component The component to add
+ * @return {ICAL.Component} The passed in component
+ */
+ addSubcomponent: function(component) {
+ if (!this._components) {
+ this._components = [];
+ this._hydratedComponentCount = 0;
+ }
+
+ if (component.parent) {
+ component.parent.removeSubcomponent(component);
+ }
+
+ var idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
+ this._components[idx - 1] = component;
+ this._hydratedComponentCount++;
+ component.parent = this;
+ return component;
+ },
+
+ /**
+ * Removes a single component by name or the instance of a specific
+ * component.
+ *
+ * @param {ICAL.Component|String} nameOrComp Name of component, or component
+ * @return {Boolean} True when comp is removed
+ */
+ removeSubcomponent: function(nameOrComp) {
+ var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
+ if (removed) {
+ this._hydratedComponentCount--;
+ }
+ return removed;
+ },
+
+ /**
+ * Removes all components or (if given) all components by a particular
+ * name.
+ *
+ * @param {String=} name Lowercase component name
+ */
+ removeAllSubcomponents: function(name) {
+ var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
+ this._hydratedComponentCount = 0;
+ return removed;
+ },
+
+ /**
+ * Adds an {@link ICAL.Property} to the component.
+ *
+ * @param {ICAL.Property} property The property to add
+ * @return {ICAL.Property} The passed in property
+ */
+ addProperty: function(property) {
+ if (!(property instanceof ICAL.Property)) {
+ throw new TypeError('must instance of ICAL.Property');
+ }
+
+ if (!this._properties) {
+ this._properties = [];
+ this._hydratedPropertyCount = 0;
+ }
+
+ if (property.parent) {
+ property.parent.removeProperty(property);
+ }
+
+ var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
+ this._properties[idx - 1] = property;
+ this._hydratedPropertyCount++;
+ property.parent = this;
+ return property;
+ },
+
+ /**
+ * Helper method to add a property with a value to the component.
+ *
+ * @param {String} name Property name to add
+ * @param {String|Number|Object} value Property value
+ * @return {ICAL.Property} The created property
+ */
+ addPropertyWithValue: function(name, value) {
+ var prop = new ICAL.Property(name);
+ prop.setValue(value);
+
+ this.addProperty(prop);
+
+ return prop;
+ },
+
+ /**
+ * Helper method that will update or create a property of the given name
+ * and sets its value. If multiple properties with the given name exist,
+ * only the first is updated.
+ *
+ * @param {String} name Property name to update
+ * @param {String|Number|Object} value Property value
+ * @return {ICAL.Property} The created property
+ */
+ updatePropertyWithValue: function(name, value) {
+ var prop = this.getFirstProperty(name);
+
+ if (prop) {
+ prop.setValue(value);
+ } else {
+ prop = this.addPropertyWithValue(name, value);
+ }
+
+ return prop;
+ },
+
+ /**
+ * Removes a single property by name or the instance of the specific
+ * property.
+ *
+ * @param {String|ICAL.Property} nameOrProp Property name or instance to remove
+ * @return {Boolean} True, when deleted
+ */
+ removeProperty: function(nameOrProp) {
+ var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
+ if (removed) {
+ this._hydratedPropertyCount--;
+ }
+ return removed;
+ },
+
+ /**
+ * Removes all properties associated with this component, optionally
+ * filtered by name.
+ *
+ * @param {String=} name Lowercase property name
+ * @return {Boolean} True, when deleted
+ */
+ removeAllProperties: function(name) {
+ var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
+ this._hydratedPropertyCount = 0;
+ return removed;
+ },
+
+ /**
+ * Returns the Object representation of this component. The returned object
+ * is a live jCal object and should be cloned if modified.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return this.jCal;
+ },
+
+ /**
+ * The string representation of this component.
+ * @return {String}
+ */
+ toString: function() {
+ return ICAL.stringify.component(
+ this.jCal, this._designSet
+ );
+ }
+ };
+
+ /**
+ * Create an {@link ICAL.Component} by parsing the passed iCalendar string.
+ *
+ * @param {String} str The iCalendar string to parse
+ */
+ Component.fromString = function(str) {
+ return new Component(ICAL.parse.component(str));
+ };
+
+ return Component;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.Property = (function() {
+ 'use strict';
+
+ var NAME_INDEX = 0;
+ var PROP_INDEX = 1;
+ var TYPE_INDEX = 2;
+ var VALUE_INDEX = 3;
+
+ var design = ICAL.design;
+
+ /**
+ * @classdesc
+ * Provides a layer on top of the raw jCal object for manipulating a single
+ * property, with its parameters and value.
+ *
+ * @description
+ * Its important to note that mutations done in the wrapper
+ * directly mutate the jCal object used to initialize.
+ *
+ * Can also be used to create new properties by passing
+ * the name of the property (as a String).
+ *
+ * @class
+ * @alias ICAL.Property
+ * @param {Array|String} jCal Raw jCal representation OR
+ * the new name of the property
+ *
+ * @param {ICAL.Component=} parent Parent component
+ */
+ function Property(jCal, parent) {
+ this._parent = parent || null;
+
+ if (typeof(jCal) === 'string') {
+ // We are creating the property by name and need to detect the type
+ this.jCal = [jCal, {}, design.defaultType];
+ this.jCal[TYPE_INDEX] = this.getDefaultType();
+ } else {
+ this.jCal = jCal;
+ }
+ this._updateType();
+ }
+
+ Property.prototype = {
+
+ /**
+ * The value type for this property
+ * @readonly
+ * @type {String}
+ */
+ get type() {
+ return this.jCal[TYPE_INDEX];
+ },
+
+ /**
+ * The name of this property, in lowercase.
+ * @readonly
+ * @type {String}
+ */
+ get name() {
+ return this.jCal[NAME_INDEX];
+ },
+
+ /**
+ * The parent component for this property.
+ * @type {ICAL.Component}
+ */
+ get parent() {
+ return this._parent;
+ },
+
+ set parent(p) {
+ // Before setting the parent, check if the design set has changed. If it
+ // has, we later need to update the type if it was unknown before.
+ var designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet);
+
+ this._parent = p;
+
+ if (this.type == design.defaultType && designSetChanged) {
+ this.jCal[TYPE_INDEX] = this.getDefaultType();
+ this._updateType();
+ }
+
+ return p;
+ },
+
+ /**
+ * The design set for this property, e.g. icalendar vs vcard
+ *
+ * @type {ICAL.design.designSet}
+ * @private
+ */
+ get _designSet() {
+ return this.parent ? this.parent._designSet : design.defaultSet;
+ },
+
+ /**
+ * Updates the type metadata from the current jCal type and design set.
+ *
+ * @private
+ */
+ _updateType: function() {
+ var designSet = this._designSet;
+
+ if (this.type in designSet.value) {
+ var designType = designSet.value[this.type];
+
+ if ('decorate' in designSet.value[this.type]) {
+ this.isDecorated = true;
+ } else {
+ this.isDecorated = false;
+ }
+
+ if (this.name in designSet.property) {
+ this.isMultiValue = ('multiValue' in designSet.property[this.name]);
+ this.isStructuredValue = ('structuredValue' in designSet.property[this.name]);
+ }
+ }
+ },
+
+ /**
+ * Hydrate a single value. The act of hydrating means turning the raw jCal
+ * value into a potentially wrapped object, for example {@link ICAL.Time}.
+ *
+ * @private
+ * @param {Number} index The index of the value to hydrate
+ * @return {Object} The decorated value.
+ */
+ _hydrateValue: function(index) {
+ if (this._values && this._values[index]) {
+ return this._values[index];
+ }
+
+ // for the case where there is no value.
+ if (this.jCal.length <= (VALUE_INDEX + index)) {
+ return null;
+ }
+
+ if (this.isDecorated) {
+ if (!this._values) {
+ this._values = [];
+ }
+ return (this._values[index] = this._decorate(
+ this.jCal[VALUE_INDEX + index]
+ ));
+ } else {
+ return this.jCal[VALUE_INDEX + index];
+ }
+ },
+
+ /**
+ * Decorate a single value, returning its wrapped object. This is used by
+ * the hydrate function to actually wrap the value.
+ *
+ * @private
+ * @param {?} value The value to decorate
+ * @return {Object} The decorated value
+ */
+ _decorate: function(value) {
+ return this._designSet.value[this.type].decorate(value, this);
+ },
+
+ /**
+ * Undecorate a single value, returning its raw jCal data.
+ *
+ * @private
+ * @param {Object} value The value to undecorate
+ * @return {?} The undecorated value
+ */
+ _undecorate: function(value) {
+ return this._designSet.value[this.type].undecorate(value, this);
+ },
+
+ /**
+ * Sets the value at the given index while also hydrating it. The passed
+ * value can either be a decorated or undecorated value.
+ *
+ * @private
+ * @param {?} value The value to set
+ * @param {Number} index The index to set it at
+ */
+ _setDecoratedValue: function(value, index) {
+ if (!this._values) {
+ this._values = [];
+ }
+
+ if (typeof(value) === 'object' && 'icaltype' in value) {
+ // decorated value
+ this.jCal[VALUE_INDEX + index] = this._undecorate(value);
+ this._values[index] = value;
+ } else {
+ // undecorated value
+ this.jCal[VALUE_INDEX + index] = value;
+ this._values[index] = this._decorate(value);
+ }
+ },
+
+ /**
+ * Gets a parameter on the property.
+ *
+ * @param {String} name Property name (lowercase)
+ * @return {Array|String} Property value
+ */
+ getParameter: function(name) {
+ if (name in this.jCal[PROP_INDEX]) {
+ return this.jCal[PROP_INDEX][name];
+ } else {
+ return undefined;
+ }
+ },
+
+ /**
+ * Sets a parameter on the property.
+ *
+ * @param {String} name The parameter name
+ * @param {Array|String} value The parameter value
+ */
+ setParameter: function(name, value) {
+ var lcname = name.toLowerCase();
+ if (typeof value === "string" &&
+ lcname in this._designSet.param &&
+ 'multiValue' in this._designSet.param[lcname]) {
+ value = [value];
+ }
+ this.jCal[PROP_INDEX][name] = value;
+ },
+
+ /**
+ * Removes a parameter
+ *
+ * @param {String} name The parameter name
+ */
+ removeParameter: function(name) {
+ delete this.jCal[PROP_INDEX][name];
+ },
+
+ /**
+ * Get the default type based on this property's name.
+ *
+ * @return {String} The default type for this property
+ */
+ getDefaultType: function() {
+ var name = this.jCal[NAME_INDEX];
+ var designSet = this._designSet;
+
+ if (name in designSet.property) {
+ var details = designSet.property[name];
+ if ('defaultType' in details) {
+ return details.defaultType;
+ }
+ }
+ return design.defaultType;
+ },
+
+ /**
+ * Sets type of property and clears out any existing values of the current
+ * type.
+ *
+ * @param {String} type New iCAL type (see design.*.values)
+ */
+ resetType: function(type) {
+ this.removeAllValues();
+ this.jCal[TYPE_INDEX] = type;
+ this._updateType();
+ },
+
+ /**
+ * Finds the first property value.
+ *
+ * @return {String} First property value
+ */
+ getFirstValue: function() {
+ return this._hydrateValue(0);
+ },
+
+ /**
+ * Gets all values on the property.
+ *
+ * NOTE: this creates an array during each call.
+ *
+ * @return {Array} List of values
+ */
+ getValues: function() {
+ var len = this.jCal.length - VALUE_INDEX;
+
+ if (len < 1) {
+ // its possible for a property to have no value.
+ return [];
+ }
+
+ var i = 0;
+ var result = [];
+
+ for (; i < len; i++) {
+ result[i] = this._hydrateValue(i);
+ }
+
+ return result;
+ },
+
+ /**
+ * Removes all values from this property
+ */
+ removeAllValues: function() {
+ if (this._values) {
+ this._values.length = 0;
+ }
+ this.jCal.length = 3;
+ },
+
+ /**
+ * Sets the values of the property. Will overwrite the existing values.
+ * This can only be used for multi-value properties.
+ *
+ * @param {Array} values An array of values
+ */
+ setValues: function(values) {
+ if (!this.isMultiValue) {
+ throw new Error(
+ this.name + ': does not not support mulitValue.\n' +
+ 'override isMultiValue'
+ );
+ }
+
+ var len = values.length;
+ var i = 0;
+ this.removeAllValues();
+
+ if (len > 0 &&
+ typeof(values[0]) === 'object' &&
+ 'icaltype' in values[0]) {
+ this.resetType(values[0].icaltype);
+ }
+
+ if (this.isDecorated) {
+ for (; i < len; i++) {
+ this._setDecoratedValue(values[i], i);
+ }
+ } else {
+ for (; i < len; i++) {
+ this.jCal[VALUE_INDEX + i] = values[i];
+ }
+ }
+ },
+
+ /**
+ * Sets the current value of the property. If this is a multi-value
+ * property, all other values will be removed.
+ *
+ * @param {String|Object} value New property value.
+ */
+ setValue: function(value) {
+ this.removeAllValues();
+ if (typeof(value) === 'object' && 'icaltype' in value) {
+ this.resetType(value.icaltype);
+ }
+
+ if (this.isDecorated) {
+ this._setDecoratedValue(value, 0);
+ } else {
+ this.jCal[VALUE_INDEX] = value;
+ }
+ },
+
+ /**
+ * Returns the Object representation of this component. The returned object
+ * is a live jCal object and should be cloned if modified.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return this.jCal;
+ },
+
+ /**
+ * The string representation of this component.
+ * @return {String}
+ */
+ toICALString: function() {
+ return ICAL.stringify.property(
+ this.jCal, this._designSet, true
+ );
+ }
+ };
+
+ /**
+ * Create an {@link ICAL.Property} by parsing the passed iCalendar string.
+ *
+ * @param {String} str The iCalendar string to parse
+ * @param {ICAL.design.designSet=} designSet The design data to use for this property
+ * @return {ICAL.Property} The created iCalendar property
+ */
+ Property.fromString = function(str, designSet) {
+ return new Property(ICAL.parse.property(str, designSet));
+ };
+
+ return Property;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.UtcOffset = (function() {
+
+ /**
+ * @classdesc
+ * This class represents the "duration" value type, with various calculation
+ * and manipulation methods.
+ *
+ * @class
+ * @alias ICAL.UtcOffset
+ * @param {Object} aData An object with members of the utc offset
+ * @param {Number=} aData.hours The hours for the utc offset
+ * @param {Number=} aData.minutes The minutes in the utc offset
+ * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
+ */
+ function UtcOffset(aData) {
+ this.fromData(aData);
+ }
+
+ UtcOffset.prototype = {
+
+ /**
+ * The hours in the utc-offset
+ * @type {Number}
+ */
+ hours: 0,
+
+ /**
+ * The minutes in the utc-offset
+ * @type {Number}
+ */
+ minutes: 0,
+
+ /**
+ * The sign of the utc offset, 1 for positive offset, -1 for negative
+ * offsets.
+ * @type {Number}
+ */
+ factor: 1,
+
+ /**
+ * The type name, to be used in the jCal object.
+ * @constant
+ * @type {String}
+ * @default "utc-offset"
+ */
+ icaltype: "utc-offset",
+
+ /**
+ * Returns a clone of the utc offset object.
+ *
+ * @return {ICAL.UtcOffset} The cloned object
+ */
+ clone: function() {
+ return ICAL.UtcOffset.fromSeconds(this.toSeconds());
+ },
+
+ /**
+ * Sets up the current instance using members from the passed data object.
+ *
+ * @param {Object} aData An object with members of the utc offset
+ * @param {Number=} aData.hours The hours for the utc offset
+ * @param {Number=} aData.minutes The minutes in the utc offset
+ * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
+ */
+ fromData: function(aData) {
+ if (aData) {
+ for (var key in aData) {
+ /* istanbul ignore else */
+ if (aData.hasOwnProperty(key)) {
+ this[key] = aData[key];
+ }
+ }
+ }
+ this._normalize();
+ },
+
+ /**
+ * Sets up the current instance from the given seconds value. The seconds
+ * value is truncated to the minute. Offsets are wrapped when the world
+ * ends, the hour after UTC+14:00 is UTC-12:00.
+ *
+ * @param {Number} aSeconds The seconds to convert into an offset
+ */
+ fromSeconds: function(aSeconds) {
+ var secs = Math.abs(aSeconds);
+
+ this.factor = aSeconds < 0 ? -1 : 1;
+ this.hours = ICAL.helpers.trunc(secs / 3600);
+
+ secs -= (this.hours * 3600);
+ this.minutes = ICAL.helpers.trunc(secs / 60);
+ return this;
+ },
+
+ /**
+ * Convert the current offset to a value in seconds
+ *
+ * @return {Number} The offset in seconds
+ */
+ toSeconds: function() {
+ return this.factor * (60 * this.minutes + 3600 * this.hours);
+ },
+
+ /**
+ * Compare this utc offset with another one.
+ *
+ * @param {ICAL.UtcOffset} other The other offset to compare with
+ * @return {Number} -1, 0 or 1 for less/equal/greater
+ */
+ compare: function icaltime_compare(other) {
+ var a = this.toSeconds();
+ var b = other.toSeconds();
+ return (a > b) - (b > a);
+ },
+
+ _normalize: function() {
+ // Range: 97200 seconds (with 1 hour inbetween)
+ var secs = this.toSeconds();
+ var factor = this.factor;
+ while (secs < -43200) { // = UTC-12:00
+ secs += 97200;
+ }
+ while (secs > 50400) { // = UTC+14:00
+ secs -= 97200;
+ }
+
+ this.fromSeconds(secs);
+
+ // Avoid changing the factor when on zero seconds
+ if (secs == 0) {
+ this.factor = factor;
+ }
+ },
+
+ /**
+ * The iCalendar string representation of this utc-offset.
+ * @return {String}
+ */
+ toICALString: function() {
+ return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString());
+ },
+
+ /**
+ * The string representation of this utc-offset.
+ * @return {String}
+ */
+ toString: function toString() {
+ return (this.factor == 1 ? "+" : "-") +
+ ICAL.helpers.pad2(this.hours) + ':' +
+ ICAL.helpers.pad2(this.minutes);
+ }
+ };
+
+ /**
+ * Creates a new {@link ICAL.UtcOffset} instance from the passed string.
+ *
+ * @param {String} aString The string to parse
+ * @return {ICAL.Duration} The created utc-offset instance
+ */
+ UtcOffset.fromString = function(aString) {
+ // -05:00
+ var options = {};
+ //TODO: support seconds per rfc5545 ?
+ options.factor = (aString[0] === '+') ? 1 : -1;
+ options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2));
+ options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2));
+
+ return new ICAL.UtcOffset(options);
+ };
+
+ /**
+ * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds
+ * value.
+ *
+ * @param {Number} aSeconds The number of seconds to convert
+ */
+ UtcOffset.fromSeconds = function(aSeconds) {
+ var instance = new UtcOffset();
+ instance.fromSeconds(aSeconds);
+ return instance;
+ };
+
+ return UtcOffset;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.Binary = (function() {
+
+ /**
+ * @classdesc
+ * Represents the BINARY value type, which contains extra methods for
+ * encoding and decoding.
+ *
+ * @class
+ * @alias ICAL.Binary
+ * @param {String} aValue The binary data for this value
+ */
+ function Binary(aValue) {
+ this.value = aValue;
+ }
+
+ Binary.prototype = {
+ /**
+ * The type name, to be used in the jCal object.
+ * @default "binary"
+ * @constant
+ */
+ icaltype: "binary",
+
+ /**
+ * Base64 decode the current value
+ *
+ * @return {String} The base64-decoded value
+ */
+ decodeValue: function decodeValue() {
+ return this._b64_decode(this.value);
+ },
+
+ /**
+ * Encodes the passed parameter with base64 and sets the internal
+ * value to the result.
+ *
+ * @param {String} aValue The raw binary value to encode
+ */
+ setEncodedValue: function setEncodedValue(aValue) {
+ this.value = this._b64_encode(aValue);
+ },
+
+ _b64_encode: function base64_encode(data) {
+ // http://kevin.vanzonneveld.net
+ // + original by: Tyler Akins (http://rumkin.com)
+ // + improved by: Bayron Guevara
+ // + improved by: Thunder.m
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + bugfixed by: Pellentesque Malesuada
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + improved by: Rafał Kukawski (http://kukawski.pl)
+ // * example 1: base64_encode('Kevin van Zonneveld');
+ // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
+ // mozilla has this native
+ // - but breaks in 2.0.0.12!
+ //if (typeof this.window['atob'] == 'function') {
+ // return atob(data);
+ //}
+ var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+ "abcdefghijklmnopqrstuvwxyz0123456789+/=";
+ var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
+ ac = 0,
+ enc = "",
+ tmp_arr = [];
+
+ if (!data) {
+ return data;
+ }
+
+ do { // pack three octets into four hexets
+ o1 = data.charCodeAt(i++);
+ o2 = data.charCodeAt(i++);
+ o3 = data.charCodeAt(i++);
+
+ bits = o1 << 16 | o2 << 8 | o3;
+
+ h1 = bits >> 18 & 0x3f;
+ h2 = bits >> 12 & 0x3f;
+ h3 = bits >> 6 & 0x3f;
+ h4 = bits & 0x3f;
+
+ // use hexets to index into b64, and append result to encoded string
+ tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
+ } while (i < data.length);
+
+ enc = tmp_arr.join('');
+
+ var r = data.length % 3;
+
+ return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
+
+ },
+
+ _b64_decode: function base64_decode(data) {
+ // http://kevin.vanzonneveld.net
+ // + original by: Tyler Akins (http://rumkin.com)
+ // + improved by: Thunder.m
+ // + input by: Aman Gupta
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + bugfixed by: Onno Marsman
+ // + bugfixed by: Pellentesque Malesuada
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + input by: Brett Zamir (http://brett-zamir.me)
+ // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
+ // * returns 1: 'Kevin van Zonneveld'
+ // mozilla has this native
+ // - but breaks in 2.0.0.12!
+ //if (typeof this.window['btoa'] == 'function') {
+ // return btoa(data);
+ //}
+ var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+ "abcdefghijklmnopqrstuvwxyz0123456789+/=";
+ var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
+ ac = 0,
+ dec = "",
+ tmp_arr = [];
+
+ if (!data) {
+ return data;
+ }
+
+ data += '';
+
+ do { // unpack four hexets into three octets using index points in b64
+ h1 = b64.indexOf(data.charAt(i++));
+ h2 = b64.indexOf(data.charAt(i++));
+ h3 = b64.indexOf(data.charAt(i++));
+ h4 = b64.indexOf(data.charAt(i++));
+
+ bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
+
+ o1 = bits >> 16 & 0xff;
+ o2 = bits >> 8 & 0xff;
+ o3 = bits & 0xff;
+
+ if (h3 == 64) {
+ tmp_arr[ac++] = String.fromCharCode(o1);
+ } else if (h4 == 64) {
+ tmp_arr[ac++] = String.fromCharCode(o1, o2);
+ } else {
+ tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
+ }
+ } while (i < data.length);
+
+ dec = tmp_arr.join('');
+
+ return dec;
+ },
+
+ /**
+ * The string representation of this value
+ * @return {String}
+ */
+ toString: function() {
+ return this.value;
+ }
+ };
+
+ /**
+ * Creates a binary value from the given string.
+ *
+ * @param {String} aString The binary value string
+ * @return {ICAL.Binary} The binary value instance
+ */
+ Binary.fromString = function(aString) {
+ return new Binary(aString);
+ };
+
+ return Binary;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+
+(function() {
+ /**
+ * @classdesc
+ * This class represents the "period" value type, with various calculation
+ * and manipulation methods.
+ *
+ * @description
+ * The passed data object cannot contain both and end date and a duration.
+ *
+ * @class
+ * @param {Object} aData An object with members of the period
+ * @param {ICAL.Time=} aData.start The start of the period
+ * @param {ICAL.Time=} aData.end The end of the period
+ * @param {ICAL.Duration=} aData.duration The duration of the period
+ */
+ ICAL.Period = function icalperiod(aData) {
+ this.wrappedJSObject = this;
+
+ if (aData && 'start' in aData) {
+ if (aData.start && !(aData.start instanceof ICAL.Time)) {
+ throw new TypeError('.start must be an instance of ICAL.Time');
+ }
+ this.start = aData.start;
+ }
+
+ if (aData && aData.end && aData.duration) {
+ throw new Error('cannot accept both end and duration');
+ }
+
+ if (aData && 'end' in aData) {
+ if (aData.end && !(aData.end instanceof ICAL.Time)) {
+ throw new TypeError('.end must be an instance of ICAL.Time');
+ }
+ this.end = aData.end;
+ }
+
+ if (aData && 'duration' in aData) {
+ if (aData.duration && !(aData.duration instanceof ICAL.Duration)) {
+ throw new TypeError('.duration must be an instance of ICAL.Duration');
+ }
+ this.duration = aData.duration;
+ }
+ };
+
+ ICAL.Period.prototype = {
+
+ /**
+ * The start of the period
+ * @type {ICAL.Time}
+ */
+ start: null,
+
+ /**
+ * The end of the period
+ * @type {ICAL.Time}
+ */
+ end: null,
+
+ /**
+ * The duration of the period
+ * @type {ICAL.Duration}
+ */
+ duration: null,
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "icalperiod"
+ */
+ icalclass: "icalperiod",
+
+ /**
+ * The type name, to be used in the jCal object.
+ * @constant
+ * @type {String}
+ * @default "period"
+ */
+ icaltype: "period",
+
+ /**
+ * Returns a clone of the duration object.
+ *
+ * @return {ICAL.Period} The cloned object
+ */
+ clone: function() {
+ return ICAL.Period.fromData({
+ start: this.start ? this.start.clone() : null,
+ end: this.end ? this.end.clone() : null,
+ duration: this.duration ? this.duration.clone() : null
+ });
+ },
+
+ /**
+ * Calculates the duration of the period, either directly or by subtracting
+ * start from end date.
+ *
+ * @return {ICAL.Duration} The calculated duration
+ */
+ getDuration: function duration() {
+ if (this.duration) {
+ return this.duration;
+ } else {
+ return this.end.subtractDate(this.start);
+ }
+ },
+
+ /**
+ * Calculates the end date of the period, either directly or by adding
+ * duration to start date.
+ *
+ * @return {ICAL.Time} The calculated end date
+ */
+ getEnd: function() {
+ if (this.end) {
+ return this.end;
+ } else {
+ var end = this.start.clone();
+ end.addDuration(this.duration);
+ return end;
+ }
+ },
+
+ /**
+ * The string representation of this period.
+ * @return {String}
+ */
+ toString: function toString() {
+ return this.start + "/" + (this.end || this.duration);
+ },
+
+ /**
+ * The jCal representation of this period type.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return [this.start.toString(), (this.end || this.duration).toString()];
+ },
+
+ /**
+ * The iCalendar string representation of this period.
+ * @return {String}
+ */
+ toICALString: function() {
+ return this.start.toICALString() + "/" +
+ (this.end || this.duration).toICALString();
+ }
+ };
+
+ /**
+ * Creates a new {@link ICAL.Period} instance from the passed string.
+ *
+ * @param {String} str The string to parse
+ * @param {ICAL.Property} prop The property this period will be on
+ * @return {ICAL.Period} The created period instance
+ */
+ ICAL.Period.fromString = function fromString(str, prop) {
+ var parts = str.split('/');
+
+ if (parts.length !== 2) {
+ throw new Error(
+ 'Invalid string value: "' + str + '" must contain a "/" char.'
+ );
+ }
+
+ var options = {
+ start: ICAL.Time.fromDateTimeString(parts[0], prop)
+ };
+
+ var end = parts[1];
+
+ if (ICAL.Duration.isValueString(end)) {
+ options.duration = ICAL.Duration.fromString(end);
+ } else {
+ options.end = ICAL.Time.fromDateTimeString(end, prop);
+ }
+
+ return new ICAL.Period(options);
+ };
+
+ /**
+ * Creates a new {@link ICAL.Period} instance from the given data object.
+ * The passed data object cannot contain both and end date and a duration.
+ *
+ * @param {Object} aData An object with members of the period
+ * @param {ICAL.Time=} aData.start The start of the period
+ * @param {ICAL.Time=} aData.end The end of the period
+ * @param {ICAL.Duration=} aData.duration The duration of the period
+ * @return {ICAL.Period} The period instance
+ */
+ ICAL.Period.fromData = function fromData(aData) {
+ return new ICAL.Period(aData);
+ };
+
+ /**
+ * Returns a new period instance from the given jCal data array. The first
+ * member is always the start date string, the second member is either a
+ * duration or end date string.
+ *
+ * @param {Array<String,String>} aData The jCal data array
+ * @param {ICAL.Property} aProp The property this jCal data is on
+ * @return {ICAL.Period} The period instance
+ */
+ ICAL.Period.fromJSON = function(aData, aProp) {
+ if (ICAL.Duration.isValueString(aData[1])) {
+ return ICAL.Period.fromData({
+ start: ICAL.Time.fromDateTimeString(aData[0], aProp),
+ duration: ICAL.Duration.fromString(aData[1])
+ });
+ } else {
+ return ICAL.Period.fromData({
+ start: ICAL.Time.fromDateTimeString(aData[0], aProp),
+ end: ICAL.Time.fromDateTimeString(aData[1], aProp)
+ });
+ }
+ };
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+
+(function() {
+ var DURATION_LETTERS = /([PDWHMTS]{1,1})/;
+
+ /**
+ * @classdesc
+ * This class represents the "duration" value type, with various calculation
+ * and manipulation methods.
+ *
+ * @class
+ * @alias ICAL.Duration
+ * @param {Object} data An object with members of the duration
+ * @param {Number} data.weeks Duration in weeks
+ * @param {Number} data.days Duration in days
+ * @param {Number} data.hours Duration in hours
+ * @param {Number} data.minutes Duration in minutes
+ * @param {Number} data.seconds Duration in seconds
+ * @param {Boolean} data.isNegative If true, the duration is negative
+ */
+ ICAL.Duration = function icalduration(data) {
+ this.wrappedJSObject = this;
+ this.fromData(data);
+ };
+
+ ICAL.Duration.prototype = {
+ /**
+ * The weeks in this duration
+ * @type {Number}
+ * @default 0
+ */
+ weeks: 0,
+
+ /**
+ * The days in this duration
+ * @type {Number}
+ * @default 0
+ */
+ days: 0,
+
+ /**
+ * The days in this duration
+ * @type {Number}
+ * @default 0
+ */
+ hours: 0,
+
+ /**
+ * The minutes in this duration
+ * @type {Number}
+ * @default 0
+ */
+ minutes: 0,
+
+ /**
+ * The seconds in this duration
+ * @type {Number}
+ * @default 0
+ */
+ seconds: 0,
+
+ /**
+ * The seconds in this duration
+ * @type {Boolean}
+ * @default false
+ */
+ isNegative: false,
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "icalduration"
+ */
+ icalclass: "icalduration",
+
+ /**
+ * The type name, to be used in the jCal object.
+ * @constant
+ * @type {String}
+ * @default "duration"
+ */
+ icaltype: "duration",
+
+ /**
+ * Returns a clone of the duration object.
+ *
+ * @return {ICAL.Duration} The cloned object
+ */
+ clone: function clone() {
+ return ICAL.Duration.fromData(this);
+ },
+
+ /**
+ * The duration value expressed as a number of seconds.
+ *
+ * @return {Number} The duration value in seconds
+ */
+ toSeconds: function toSeconds() {
+ var seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
+ 86400 * this.days + 7 * 86400 * this.weeks;
+ return (this.isNegative ? -seconds : seconds);
+ },
+
+ /**
+ * Reads the passed seconds value into this duration object. Afterwards,
+ * members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up
+ * accordingly.
+ *
+ * @param {Number} aSeconds The duration value in seconds
+ * @return {ICAL.Duration} Returns this instance
+ */
+ fromSeconds: function fromSeconds(aSeconds) {
+ var secs = Math.abs(aSeconds);
+
+ this.isNegative = (aSeconds < 0);
+ this.days = ICAL.helpers.trunc(secs / 86400);
+
+ // If we have a flat number of weeks, use them.
+ if (this.days % 7 == 0) {
+ this.weeks = this.days / 7;
+ this.days = 0;
+ } else {
+ this.weeks = 0;
+ }
+
+ secs -= (this.days + 7 * this.weeks) * 86400;
+
+ this.hours = ICAL.helpers.trunc(secs / 3600);
+ secs -= this.hours * 3600;
+
+ this.minutes = ICAL.helpers.trunc(secs / 60);
+ secs -= this.minutes * 60;
+
+ this.seconds = secs;
+ return this;
+ },
+
+ /**
+ * Sets up the current instance using members from the passed data object.
+ *
+ * @param {Object} aData An object with members of the duration
+ * @param {Number} aData.weeks Duration in weeks
+ * @param {Number} aData.days Duration in days
+ * @param {Number} aData.hours Duration in hours
+ * @param {Number} aData.minutes Duration in minutes
+ * @param {Number} aData.seconds Duration in seconds
+ * @param {Boolean} aData.isNegative If true, the duration is negative
+ */
+ fromData: function fromData(aData) {
+ var propsToCopy = ["weeks", "days", "hours",
+ "minutes", "seconds", "isNegative"];
+ for (var key in propsToCopy) {
+ /* istanbul ignore if */
+ if (!propsToCopy.hasOwnProperty(key)) {
+ continue;
+ }
+ var prop = propsToCopy[key];
+ if (aData && prop in aData) {
+ this[prop] = aData[prop];
+ } else {
+ this[prop] = 0;
+ }
+ }
+ },
+
+ /**
+ * Resets the duration instance to the default values, i.e. PT0S
+ */
+ reset: function reset() {
+ this.isNegative = false;
+ this.weeks = 0;
+ this.days = 0;
+ this.hours = 0;
+ this.minutes = 0;
+ this.seconds = 0;
+ },
+
+ /**
+ * Compares the duration instance with another one.
+ *
+ * @param {ICAL.Duration} aOther The instance to compare with
+ * @return {Number} -1, 0 or 1 for less/equal/greater
+ */
+ compare: function compare(aOther) {
+ var thisSeconds = this.toSeconds();
+ var otherSeconds = aOther.toSeconds();
+ return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
+ },
+
+ /**
+ * Normalizes the duration instance. For example, a duration with a value
+ * of 61 seconds will be normalized to 1 minute and 1 second.
+ */
+ normalize: function normalize() {
+ this.fromSeconds(this.toSeconds());
+ },
+
+ /**
+ * The string representation of this duration.
+ * @return {String}
+ */
+ toString: function toString() {
+ if (this.toSeconds() == 0) {
+ return "PT0S";
+ } else {
+ var str = "";
+ if (this.isNegative) str += "-";
+ str += "P";
+ if (this.weeks) str += this.weeks + "W";
+ if (this.days) str += this.days + "D";
+
+ if (this.hours || this.minutes || this.seconds) {
+ str += "T";
+ if (this.hours) str += this.hours + "H";
+ if (this.minutes) str += this.minutes + "M";
+ if (this.seconds) str += this.seconds + "S";
+ }
+ return str;
+ }
+ },
+
+ /**
+ * The iCalendar string representation of this duration.
+ * @return {String}
+ */
+ toICALString: function() {
+ return this.toString();
+ }
+ };
+
+ /**
+ * Returns a new ICAL.Duration instance from the passed seconds value.
+ *
+ * @param {Number} aSeconds The seconds to create the instance from
+ * @return {ICAL.Duration} The newly created duration instance
+ */
+ ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) {
+ return (new ICAL.Duration()).fromSeconds(aSeconds);
+ };
+
+ /**
+ * Internal helper function to handle a chunk of a duration.
+ *
+ * @param {String} letter type of duration chunk
+ * @param {String} number numeric value or -/+
+ * @param {Object} dict target to assign values to
+ */
+ function parseDurationChunk(letter, number, object) {
+ var type;
+ switch (letter) {
+ case 'P':
+ if (number && number === '-') {
+ object.isNegative = true;
+ } else {
+ object.isNegative = false;
+ }
+ // period
+ break;
+ case 'D':
+ type = 'days';
+ break;
+ case 'W':
+ type = 'weeks';
+ break;
+ case 'H':
+ type = 'hours';
+ break;
+ case 'M':
+ type = 'minutes';
+ break;
+ case 'S':
+ type = 'seconds';
+ break;
+ default:
+ // Not a valid chunk
+ return 0;
+ }
+
+ if (type) {
+ if (!number && number !== 0) {
+ throw new Error(
+ 'invalid duration value: Missing number before "' + letter + '"'
+ );
+ }
+ var num = parseInt(number, 10);
+ if (ICAL.helpers.isStrictlyNaN(num)) {
+ throw new Error(
+ 'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
+ );
+ }
+ object[type] = num;
+ }
+
+ return 1;
+ }
+
+ /**
+ * Checks if the given string is an iCalendar duration value.
+ *
+ * @param {String} value The raw ical value
+ * @return {Boolean} True, if the given value is of the
+ * duration ical type
+ */
+ ICAL.Duration.isValueString = function(string) {
+ return (string[0] === 'P' || string[1] === 'P');
+ };
+
+ /**
+ * Creates a new {@link ICAL.Duration} instance from the passed string.
+ *
+ * @param {String} aStr The string to parse
+ * @return {ICAL.Duration} The created duration instance
+ */
+ ICAL.Duration.fromString = function icalduration_from_string(aStr) {
+ var pos = 0;
+ var dict = Object.create(null);
+ var chunks = 0;
+
+ while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
+ var type = aStr[pos];
+ var numeric = aStr.substr(0, pos);
+ aStr = aStr.substr(pos + 1);
+
+ chunks += parseDurationChunk(type, numeric, dict);
+ }
+
+ if (chunks < 2) {
+ // There must be at least a chunk with "P" and some unit chunk
+ throw new Error(
+ 'invalid duration value: Not enough duration components in "' + aStr + '"'
+ );
+ }
+
+ return new ICAL.Duration(dict);
+ };
+
+ /**
+ * Creates a new ICAL.Duration instance from the given data object.
+ *
+ * @param {Object} aData An object with members of the duration
+ * @param {Number} aData.weeks Duration in weeks
+ * @param {Number} aData.days Duration in days
+ * @param {Number} aData.hours Duration in hours
+ * @param {Number} aData.minutes Duration in minutes
+ * @param {Number} aData.seconds Duration in seconds
+ * @param {Boolean} aData.isNegative If true, the duration is negative
+ * @return {ICAL.Duration} The createad duration instance
+ */
+ ICAL.Duration.fromData = function icalduration_from_data(aData) {
+ return new ICAL.Duration(aData);
+ };
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2012 */
+
+
+
+(function() {
+ var OPTIONS = ["tzid", "location", "tznames",
+ "latitude", "longitude"];
+
+ /**
+ * @classdesc
+ * Timezone representation, created by passing in a tzid and component.
+ *
+ * @example
+ * var vcalendar;
+ * var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
+ * var tzid = timezoneComp.getFirstPropertyValue('tzid');
+ *
+ * var timezone = new ICAL.Timezone({
+ * component: timezoneComp,
+ * tzid
+ * });
+ *
+ * @class
+ * @param {ICAL.Component|Object} data options for class
+ * @param {String|ICAL.Component} data.component
+ * If data is a simple object, then this member can be set to either a
+ * string containing the component data, or an already parsed
+ * ICAL.Component
+ * @param {String} data.tzid The timezone identifier
+ * @param {String} data.location The timezone locationw
+ * @param {String} data.tznames An alternative string representation of the
+ * timezone
+ * @param {Number} data.latitude The latitude of the timezone
+ * @param {Number} data.longitude The longitude of the timezone
+ */
+ ICAL.Timezone = function icaltimezone(data) {
+ this.wrappedJSObject = this;
+ this.fromData(data);
+ };
+
+ ICAL.Timezone.prototype = {
+
+ /**
+ * Timezone identifier
+ * @type {String}
+ */
+ tzid: "",
+
+ /**
+ * Timezone location
+ * @type {String}
+ */
+ location: "",
+
+ /**
+ * Alternative timezone name, for the string representation
+ * @type {String}
+ */
+ tznames: "",
+
+ /**
+ * The primary latitude for the timezone.
+ * @type {Number}
+ */
+ latitude: 0.0,
+
+ /**
+ * The primary longitude for the timezone.
+ * @type {Number}
+ */
+ longitude: 0.0,
+
+ /**
+ * The vtimezone component for this timezone.
+ * @type {ICAL.Component}
+ */
+ component: null,
+
+ /**
+ * The year this timezone has been expanded to. All timezone transition
+ * dates until this year are known and can be used for calculation
+ *
+ * @private
+ * @type {Number}
+ */
+ expandedUntilYear: 0,
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "icaltimezone"
+ */
+ icalclass: "icaltimezone",
+
+ /**
+ * Sets up the current instance using members from the passed data object.
+ *
+ * @param {ICAL.Component|Object} aData options for class
+ * @param {String|ICAL.Component} aData.component
+ * If aData is a simple object, then this member can be set to either a
+ * string containing the component data, or an already parsed
+ * ICAL.Component
+ * @param {String} aData.tzid The timezone identifier
+ * @param {String} aData.location The timezone locationw
+ * @param {String} aData.tznames An alternative string representation of the
+ * timezone
+ * @param {Number} aData.latitude The latitude of the timezone
+ * @param {Number} aData.longitude The longitude of the timezone
+ */
+ fromData: function fromData(aData) {
+ this.expandedUntilYear = 0;
+ this.changes = [];
+
+ if (aData instanceof ICAL.Component) {
+ // Either a component is passed directly
+ this.component = aData;
+ } else {
+ // Otherwise the component may be in the data object
+ if (aData && "component" in aData) {
+ if (typeof aData.component == "string") {
+ // If a string was passed, parse it as a component
+ var jCal = ICAL.parse(aData.component);
+ this.component = new ICAL.Component(jCal);
+ } else if (aData.component instanceof ICAL.Component) {
+ // If it was a component already, then just set it
+ this.component = aData.component;
+ } else {
+ // Otherwise just null out the component
+ this.component = null;
+ }
+ }
+
+ // Copy remaining passed properties
+ for (var key in OPTIONS) {
+ /* istanbul ignore else */
+ if (OPTIONS.hasOwnProperty(key)) {
+ var prop = OPTIONS[key];
+ if (aData && prop in aData) {
+ this[prop] = aData[prop];
+ }
+ }
+ }
+ }
+
+ // If we have a component but no TZID, attempt to get it from the
+ // component's properties.
+ if (this.component instanceof ICAL.Component && !this.tzid) {
+ this.tzid = this.component.getFirstPropertyValue('tzid');
+ }
+
+ return this;
+ },
+
+ /**
+ * Finds the utcOffset the given time would occur in this timezone.
+ *
+ * @param {ICAL.Time} tt The time to check for
+ * @return {Number} utc offset in seconds
+ */
+ utcOffset: function utcOffset(tt) {
+ if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) {
+ return 0;
+ }
+
+ this._ensureCoverage(tt.year);
+
+ if (!this.changes.length) {
+ return 0;
+ }
+
+ var tt_change = {
+ year: tt.year,
+ month: tt.month,
+ day: tt.day,
+ hour: tt.hour,
+ minute: tt.minute,
+ second: tt.second
+ };
+
+ var change_num = this._findNearbyChange(tt_change);
+ var change_num_to_use = -1;
+ var step = 1;
+
+ // TODO: replace with bin search?
+ for (;;) {
+ var change = ICAL.helpers.clone(this.changes[change_num], true);
+ if (change.utcOffset < change.prevUtcOffset) {
+ ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
+ } else {
+ ICAL.Timezone.adjust_change(change, 0, 0, 0,
+ change.prevUtcOffset);
+ }
+
+ var cmp = ICAL.Timezone._compare_change_fn(tt_change, change);
+
+ if (cmp >= 0) {
+ change_num_to_use = change_num;
+ } else {
+ step = -1;
+ }
+
+ if (step == -1 && change_num_to_use != -1) {
+ break;
+ }
+
+ change_num += step;
+
+ if (change_num < 0) {
+ return 0;
+ }
+
+ if (change_num >= this.changes.length) {
+ break;
+ }
+ }
+
+ var zone_change = this.changes[change_num_to_use];
+ var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
+
+ if (utcOffset_change < 0 && change_num_to_use > 0) {
+ var tmp_change = ICAL.helpers.clone(zone_change, true);
+ ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0,
+ tmp_change.prevUtcOffset);
+
+ if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
+ var prev_zone_change = this.changes[change_num_to_use - 1];
+
+ var want_daylight = false; // TODO
+
+ if (zone_change.is_daylight != want_daylight &&
+ prev_zone_change.is_daylight == want_daylight) {
+ zone_change = prev_zone_change;
+ }
+ }
+ }
+
+ // TODO return is_daylight?
+ return zone_change.utcOffset;
+ },
+
+ _findNearbyChange: function icaltimezone_find_nearby_change(change) {
+ // find the closest match
+ var idx = ICAL.helpers.binsearchInsert(
+ this.changes,
+ change,
+ ICAL.Timezone._compare_change_fn
+ );
+
+ if (idx >= this.changes.length) {
+ return this.changes.length - 1;
+ }
+
+ return idx;
+ },
+
+ _ensureCoverage: function(aYear) {
+ if (ICAL.Timezone._minimumExpansionYear == -1) {
+ var today = ICAL.Time.now();
+ ICAL.Timezone._minimumExpansionYear = today.year;
+ }
+
+ var changesEndYear = aYear;
+ if (changesEndYear < ICAL.Timezone._minimumExpansionYear) {
+ changesEndYear = ICAL.Timezone._minimumExpansionYear;
+ }
+
+ changesEndYear += ICAL.Timezone.EXTRA_COVERAGE;
+
+ if (changesEndYear > ICAL.Timezone.MAX_YEAR) {
+ changesEndYear = ICAL.Timezone.MAX_YEAR;
+ }
+
+ if (!this.changes.length || this.expandedUntilYear < aYear) {
+ var subcomps = this.component.getAllSubcomponents();
+ var compLen = subcomps.length;
+ var compIdx = 0;
+
+ for (; compIdx < compLen; compIdx++) {
+ this._expandComponent(
+ subcomps[compIdx], changesEndYear, this.changes
+ );
+ }
+
+ this.changes.sort(ICAL.Timezone._compare_change_fn);
+ this.expandedUntilYear = changesEndYear;
+ }
+ },
+
+ _expandComponent: function(aComponent, aYear, changes) {
+ if (!aComponent.hasProperty("dtstart") ||
+ !aComponent.hasProperty("tzoffsetto") ||
+ !aComponent.hasProperty("tzoffsetfrom")) {
+ return null;
+ }
+
+ var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
+ var change;
+
+ function convert_tzoffset(offset) {
+ return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
+ }
+
+ function init_changes() {
+ var changebase = {};
+ changebase.is_daylight = (aComponent.name == "daylight");
+ changebase.utcOffset = convert_tzoffset(
+ aComponent.getFirstProperty("tzoffsetto").getFirstValue()
+ );
+
+ changebase.prevUtcOffset = convert_tzoffset(
+ aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
+ );
+
+ return changebase;
+ }
+
+ if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
+ change = init_changes();
+ change.year = dtstart.year;
+ change.month = dtstart.month;
+ change.day = dtstart.day;
+ change.hour = dtstart.hour;
+ change.minute = dtstart.minute;
+ change.second = dtstart.second;
+
+ ICAL.Timezone.adjust_change(change, 0, 0, 0,
+ -change.prevUtcOffset);
+ changes.push(change);
+ } else {
+ var props = aComponent.getAllProperties("rdate");
+ for (var rdatekey in props) {
+ /* istanbul ignore if */
+ if (!props.hasOwnProperty(rdatekey)) {
+ continue;
+ }
+ var rdate = props[rdatekey];
+ var time = rdate.getFirstValue();
+ change = init_changes();
+
+ change.year = time.year;
+ change.month = time.month;
+ change.day = time.day;
+
+ if (time.isDate) {
+ change.hour = dtstart.hour;
+ change.minute = dtstart.minute;
+ change.second = dtstart.second;
+
+ if (dtstart.zone != ICAL.Timezone.utcTimezone) {
+ ICAL.Timezone.adjust_change(change, 0, 0, 0,
+ -change.prevUtcOffset);
+ }
+ } else {
+ change.hour = time.hour;
+ change.minute = time.minute;
+ change.second = time.second;
+
+ if (time.zone != ICAL.Timezone.utcTimezone) {
+ ICAL.Timezone.adjust_change(change, 0, 0, 0,
+ -change.prevUtcOffset);
+ }
+ }
+
+ changes.push(change);
+ }
+
+ var rrule = aComponent.getFirstProperty("rrule");
+
+ if (rrule) {
+ rrule = rrule.getFirstValue();
+ change = init_changes();
+
+ if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) {
+ rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
+ rrule.until.zone = ICAL.Timezone.localTimezone;
+ }
+
+ var iterator = rrule.iterator(dtstart);
+
+ var occ;
+ while ((occ = iterator.next())) {
+ change = init_changes();
+ if (occ.year > aYear || !occ) {
+ break;
+ }
+
+ change.year = occ.year;
+ change.month = occ.month;
+ change.day = occ.day;
+ change.hour = occ.hour;
+ change.minute = occ.minute;
+ change.second = occ.second;
+ change.isDate = occ.isDate;
+
+ ICAL.Timezone.adjust_change(change, 0, 0, 0,
+ -change.prevUtcOffset);
+ changes.push(change);
+ }
+ }
+ }
+
+ return changes;
+ },
+
+ /**
+ * The string representation of this timezone.
+ * @return {String}
+ */
+ toString: function toString() {
+ return (this.tznames ? this.tznames : this.tzid);
+ }
+ };
+
+ ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) {
+ if (a.year < b.year) return -1;
+ else if (a.year > b.year) return 1;
+
+ if (a.month < b.month) return -1;
+ else if (a.month > b.month) return 1;
+
+ if (a.day < b.day) return -1;
+ else if (a.day > b.day) return 1;
+
+ if (a.hour < b.hour) return -1;
+ else if (a.hour > b.hour) return 1;
+
+ if (a.minute < b.minute) return -1;
+ else if (a.minute > b.minute) return 1;
+
+ if (a.second < b.second) return -1;
+ else if (a.second > b.second) return 1;
+
+ return 0;
+ };
+
+ /**
+ * Convert the date/time from one zone to the next.
+ *
+ * @param {ICAL.Time} tt The time to convert
+ * @param {ICAL.Timezone} from_zone The source zone to convert from
+ * @param {ICAL.Timezone} to_zone The target zone to conver to
+ * @return {ICAL.Time} The converted date/time object
+ */
+ ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) {
+ if (tt.isDate ||
+ from_zone.tzid == to_zone.tzid ||
+ from_zone == ICAL.Timezone.localTimezone ||
+ to_zone == ICAL.Timezone.localTimezone) {
+ tt.zone = to_zone;
+ return tt;
+ }
+
+ var utcOffset = from_zone.utcOffset(tt);
+ tt.adjust(0, 0, 0, - utcOffset);
+
+ utcOffset = to_zone.utcOffset(tt);
+ tt.adjust(0, 0, 0, utcOffset);
+
+ return null;
+ };
+
+ /**
+ * Creates a new ICAL.Timezone instance from the passed data object.
+ *
+ * @param {ICAL.Component|Object} aData options for class
+ * @param {String|ICAL.Component} aData.component
+ * If aData is a simple object, then this member can be set to either a
+ * string containing the component data, or an already parsed
+ * ICAL.Component
+ * @param {String} aData.tzid The timezone identifier
+ * @param {String} aData.location The timezone locationw
+ * @param {String} aData.tznames An alternative string representation of the
+ * timezone
+ * @param {Number} aData.latitude The latitude of the timezone
+ * @param {Number} aData.longitude The longitude of the timezone
+ */
+ ICAL.Timezone.fromData = function icaltimezone_fromData(aData) {
+ var tt = new ICAL.Timezone();
+ return tt.fromData(aData);
+ };
+
+ /**
+ * The instance describing the UTC timezone
+ * @type {ICAL.Timezone}
+ * @constant
+ * @instance
+ */
+ ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({
+ tzid: "UTC"
+ });
+
+ /**
+ * The instance describing the local timezone
+ * @type {ICAL.Timezone}
+ * @constant
+ * @instance
+ */
+ ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({
+ tzid: "floating"
+ });
+
+ /**
+ * Adjust a timezone change object.
+ * @private
+ * @param {Object} change The timezone change object
+ * @param {Number} days The extra amount of days
+ * @param {Number} hours The extra amount of hours
+ * @param {Number} minutes The extra amount of minutes
+ * @param {Number} seconds The extra amount of seconds
+ */
+ ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) {
+ return ICAL.Time.prototype.adjust.call(
+ change,
+ days,
+ hours,
+ minutes,
+ seconds,
+ change
+ );
+ };
+
+ ICAL.Timezone._minimumExpansionYear = -1;
+ ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull?
+ ICAL.Timezone.EXTRA_COVERAGE = 5;
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.TimezoneService = (function() {
+ var zones;
+
+ /**
+ * @classdesc
+ * Singleton class to contain timezones. Right now its all manual registry in
+ * the future we may use this class to download timezone information or handle
+ * loading pre-expanded timezones.
+ *
+ * @namespace
+ * @alias ICAL.TimezoneService
+ */
+ var TimezoneService = {
+ reset: function() {
+ zones = Object.create(null);
+ var utc = ICAL.Timezone.utcTimezone;
+
+ zones.Z = utc;
+ zones.UTC = utc;
+ zones.GMT = utc;
+ },
+
+ /**
+ * Checks if timezone id has been registered.
+ *
+ * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
+ * @return {Boolean} False, when not present
+ */
+ has: function(tzid) {
+ return !!zones[tzid];
+ },
+
+ /**
+ * Returns a timezone by its tzid if present.
+ *
+ * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
+ * @return {?ICAL.Timezone} The timezone, or null if not found
+ */
+ get: function(tzid) {
+ return zones[tzid];
+ },
+
+ /**
+ * Registers a timezone object or component.
+ *
+ * @param {String=} name
+ * The name of the timezone. Defaults to the component's TZID if not
+ * passed.
+ * @param {ICAL.Component|ICAL.Timezone} zone
+ * The initialized zone or vtimezone.
+ */
+ register: function(name, timezone) {
+ if (name instanceof ICAL.Component) {
+ if (name.name === 'vtimezone') {
+ timezone = new ICAL.Timezone(name);
+ name = timezone.tzid;
+ }
+ }
+
+ if (timezone instanceof ICAL.Timezone) {
+ zones[name] = timezone;
+ } else {
+ throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component');
+ }
+ },
+
+ /**
+ * Removes a timezone by its tzid from the list.
+ *
+ * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
+ * @return {?ICAL.Timezone} The removed timezone, or null if not registered
+ */
+ remove: function(tzid) {
+ return (delete zones[tzid]);
+ }
+ };
+
+ // initialize defaults
+ TimezoneService.reset();
+
+ return TimezoneService;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+
+(function() {
+
+ /**
+ * @classdesc
+ * iCalendar Time representation (similar to JS Date object). Fully
+ * independent of system (OS) timezone / time. Unlike JS Date, the month
+ * January is 1, not zero.
+ *
+ * @example
+ * var time = new ICAL.Time({
+ * year: 2012,
+ * month: 10,
+ * day: 11
+ * minute: 0,
+ * second: 0,
+ * isDate: false
+ * });
+ *
+ *
+ * @alias ICAL.Time
+ * @class
+ * @param {Object} data Time initialization
+ * @param {Number=} data.year The year for this date
+ * @param {Number=} data.month The month for this date
+ * @param {Number=} data.day The day for this date
+ * @param {Number=} data.hour The hour for this date
+ * @param {Number=} data.minute The minute for this date
+ * @param {Number=} data.second The second for this date
+ * @param {Boolean=} data.isDate If true, the instance represents a date (as
+ * opposed to a date-time)
+ * @param {ICAL.Timezone} zone timezone this position occurs in
+ */
+ ICAL.Time = function icaltime(data, zone) {
+ this.wrappedJSObject = this;
+ var time = this._time = Object.create(null);
+
+ /* time defaults */
+ time.year = 0;
+ time.month = 1;
+ time.day = 1;
+ time.hour = 0;
+ time.minute = 0;
+ time.second = 0;
+ time.isDate = false;
+
+ this.fromData(data, zone);
+ };
+
+ ICAL.Time._dowCache = {};
+ ICAL.Time._wnCache = {};
+
+ ICAL.Time.prototype = {
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "icaltime"
+ */
+ icalclass: "icaltime",
+ _cachedUnixTime: null,
+
+ /**
+ * The type name, to be used in the jCal object. This value may change and
+ * is strictly defined by the {@link ICAL.Time#isDate isDate} member.
+ * @readonly
+ * @type {String}
+ * @default "date-time"
+ */
+ get icaltype() {
+ return this.isDate ? 'date' : 'date-time';
+ },
+
+ /**
+ * The timezone for this time.
+ * @type {ICAL.Timezone}
+ */
+ zone: null,
+
+ /**
+ * Internal uses to indicate that a change has been made and the next read
+ * operation must attempt to normalize the value (for example changing the
+ * day to 33).
+ *
+ * @type {Boolean}
+ * @private
+ */
+ _pendingNormalization: false,
+
+ /**
+ * Returns a clone of the time object.
+ *
+ * @return {ICAL.Time} The cloned object
+ */
+ clone: function() {
+ return new ICAL.Time(this._time, this.zone);
+ },
+
+ /**
+ * Reset the time instance to epoch time
+ */
+ reset: function icaltime_reset() {
+ this.fromData(ICAL.Time.epochTime);
+ this.zone = ICAL.Timezone.utcTimezone;
+ },
+
+ /**
+ * Reset the time instance to the given date/time values.
+ *
+ * @param {Number} year The year to set
+ * @param {Number} month The month to set
+ * @param {Number} day The day to set
+ * @param {Number} hour The hour to set
+ * @param {Number} minute The minute to set
+ * @param {Number} second The second to set
+ * @param {ICAL.Timezone} timezone The timezone to set
+ */
+ resetTo: function icaltime_resetTo(year, month, day,
+ hour, minute, second, timezone) {
+ this.fromData({
+ year: year,
+ month: month,
+ day: day,
+ hour: hour,
+ minute: minute,
+ second: second,
+ zone: timezone
+ });
+ },
+
+ /**
+ * Set up the current instance from the Javascript date value.
+ *
+ * @param {?Date} aDate The Javascript Date to read, or null to reset
+ * @param {Boolean} useUTC If true, the UTC values of the date will be used
+ */
+ fromJSDate: function icaltime_fromJSDate(aDate, useUTC) {
+ if (!aDate) {
+ this.reset();
+ } else {
+ if (useUTC) {
+ this.zone = ICAL.Timezone.utcTimezone;
+ this.year = aDate.getUTCFullYear();
+ this.month = aDate.getUTCMonth() + 1;
+ this.day = aDate.getUTCDate();
+ this.hour = aDate.getUTCHours();
+ this.minute = aDate.getUTCMinutes();
+ this.second = aDate.getUTCSeconds();
+ } else {
+ this.zone = ICAL.Timezone.localTimezone;
+ this.year = aDate.getFullYear();
+ this.month = aDate.getMonth() + 1;
+ this.day = aDate.getDate();
+ this.hour = aDate.getHours();
+ this.minute = aDate.getMinutes();
+ this.second = aDate.getSeconds();
+ }
+ }
+ this._cachedUnixTime = null;
+ return this;
+ },
+
+ /**
+ * Sets up the current instance using members from the passed data object.
+ *
+ * @param {Object} aData Time initialization
+ * @param {Number=} aData.year The year for this date
+ * @param {Number=} aData.month The month for this date
+ * @param {Number=} aData.day The day for this date
+ * @param {Number=} aData.hour The hour for this date
+ * @param {Number=} aData.minute The minute for this date
+ * @param {Number=} aData.second The second for this date
+ * @param {Boolean=} aData.isDate If true, the instance represents a date
+ * (as opposed to a date-time)
+ * @param {ICAL.Timezone=} aZone Timezone this position occurs in
+ */
+ fromData: function fromData(aData, aZone) {
+ if (aData) {
+ for (var key in aData) {
+ /* istanbul ignore else */
+ if (Object.prototype.hasOwnProperty.call(aData, key)) {
+ // ical type cannot be set
+ if (key === 'icaltype') continue;
+ this[key] = aData[key];
+ }
+ }
+ }
+
+ if (aZone) {
+ this.zone = aZone;
+ }
+
+ if (aData && !("isDate" in aData)) {
+ this.isDate = !("hour" in aData);
+ } else if (aData && ("isDate" in aData)) {
+ this.isDate = aData.isDate;
+ }
+
+ if (aData && "timezone" in aData) {
+ var zone = ICAL.TimezoneService.get(
+ aData.timezone
+ );
+
+ this.zone = zone || ICAL.Timezone.localTimezone;
+ }
+
+ if (aData && "zone" in aData) {
+ this.zone = aData.zone;
+ }
+
+ if (!this.zone) {
+ this.zone = ICAL.Timezone.localTimezone;
+ }
+
+ this._cachedUnixTime = null;
+ return this;
+ },
+
+ /**
+ * Calculate the day of week.
+ * @return {ICAL.Time.weekDay}
+ */
+ dayOfWeek: function icaltime_dayOfWeek() {
+ var dowCacheKey = (this.year << 9) + (this.month << 5) + this.day;
+ if (dowCacheKey in ICAL.Time._dowCache) {
+ return ICAL.Time._dowCache[dowCacheKey];
+ }
+
+ // Using Zeller's algorithm
+ var q = this.day;
+ var m = this.month + (this.month < 3 ? 12 : 0);
+ var Y = this.year - (this.month < 3 ? 1 : 0);
+
+ var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4));
+ /* istanbul ignore else */
+ if (true /* gregorian */) {
+ h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400);
+ } else {
+ h += 5;
+ }
+
+ // Normalize to 1 = sunday
+ h = ((h + 6) % 7) + 1;
+ ICAL.Time._dowCache[dowCacheKey] = h;
+ return h;
+ },
+
+ /**
+ * Calculate the day of year.
+ * @return {Number}
+ */
+ dayOfYear: function dayOfYear() {
+ var is_leap = (ICAL.Time.isLeapYear(this.year) ? 1 : 0);
+ var diypm = ICAL.Time.daysInYearPassedMonth;
+ return diypm[is_leap][this.month - 1] + this.day;
+ },
+
+ /**
+ * Returns a copy of the current date/time, rewound to the start of the
+ * week. The resulting ICAL.Time instance is of icaltype date, even if this
+ * is a date-time.
+ *
+ * @param {ICAL.Time.weekDay=} aWeekStart
+ * The week start weekday, defaults to SUNDAY
+ * @return {ICAL.Time} The start of the week (cloned)
+ */
+ startOfWeek: function startOfWeek(aWeekStart) {
+ var firstDow = aWeekStart || ICAL.Time.SUNDAY;
+ var result = this.clone();
+ result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7);
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * Returns a copy of the current date/time, shifted to the end of the week.
+ * The resulting ICAL.Time instance is of icaltype date, even if this is a
+ * date-time.
+ *
+ * @param {ICAL.Time.weekDay=} aWeekStart
+ * The week start weekday, defaults to SUNDAY
+ * @return {ICAL.Time} The end of the week (cloned)
+ */
+ endOfWeek: function endOfWeek(aWeekStart) {
+ var firstDow = aWeekStart || ICAL.Time.SUNDAY;
+ var result = this.clone();
+ result.day += (7 - this.dayOfWeek() + firstDow - ICAL.Time.SUNDAY) % 7;
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * Returns a copy of the current date/time, rewound to the start of the
+ * month. The resulting ICAL.Time instance is of icaltype date, even if
+ * this is a date-time.
+ *
+ * @return {ICAL.Time} The start of the month (cloned)
+ */
+ startOfMonth: function startOfMonth() {
+ var result = this.clone();
+ result.day = 1;
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * Returns a copy of the current date/time, shifted to the end of the
+ * month. The resulting ICAL.Time instance is of icaltype date, even if
+ * this is a date-time.
+ *
+ * @return {ICAL.Time} The end of the month (cloned)
+ */
+ endOfMonth: function endOfMonth() {
+ var result = this.clone();
+ result.day = ICAL.Time.daysInMonth(result.month, result.year);
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * Returns a copy of the current date/time, rewound to the start of the
+ * year. The resulting ICAL.Time instance is of icaltype date, even if
+ * this is a date-time.
+ *
+ * @return {ICAL.Time} The start of the year (cloned)
+ */
+ startOfYear: function startOfYear() {
+ var result = this.clone();
+ result.day = 1;
+ result.month = 1;
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * Returns a copy of the current date/time, shifted to the end of the
+ * year. The resulting ICAL.Time instance is of icaltype date, even if
+ * this is a date-time.
+ *
+ * @return {ICAL.Time} The end of the year (cloned)
+ */
+ endOfYear: function endOfYear() {
+ var result = this.clone();
+ result.day = 31;
+ result.month = 12;
+ result.isDate = true;
+ result.hour = 0;
+ result.minute = 0;
+ result.second = 0;
+ return result;
+ },
+
+ /**
+ * First calculates the start of the week, then returns the day of year for
+ * this date. If the day falls into the previous year, the day is zero or negative.
+ *
+ * @param {ICAL.Time.weekDay=} aFirstDayOfWeek
+ * The week start weekday, defaults to SUNDAY
+ * @return {Number} The calculated day of year
+ */
+ startDoyWeek: function startDoyWeek(aFirstDayOfWeek) {
+ var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY;
+ var delta = this.dayOfWeek() - firstDow;
+ if (delta < 0) delta += 7;
+ return this.dayOfYear() - delta;
+ },
+
+ /**
+ * Get the dominical letter for the current year. Letters range from A - G
+ * for common years, and AG to GF for leap years.
+ *
+ * @param {Number} yr The year to retrieve the letter for
+ * @return {String} The dominical letter.
+ */
+ getDominicalLetter: function() {
+ return ICAL.Time.getDominicalLetter(this.year);
+ },
+
+ /**
+ * Finds the nthWeekDay relative to the current month (not day). The
+ * returned value is a day relative the month that this month belongs to so
+ * 1 would indicate the first of the month and 40 would indicate a day in
+ * the following month.
+ *
+ * @param {Number} aDayOfWeek Day of the week see the day name constants
+ * @param {Number} aPos Nth occurrence of a given week day values
+ * of 1 and 0 both indicate the first weekday of that type. aPos may
+ * be either positive or negative
+ *
+ * @return {Number} numeric value indicating a day relative
+ * to the current month of this time object
+ */
+ nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) {
+ var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year);
+ var weekday;
+ var pos = aPos;
+
+ var start = 0;
+
+ var otherDay = this.clone();
+
+ if (pos >= 0) {
+ otherDay.day = 1;
+
+ // because 0 means no position has been given
+ // 1 and 0 indicate the same day.
+ if (pos != 0) {
+ // remove the extra numeric value
+ pos--;
+ }
+
+ // set current start offset to current day.
+ start = otherDay.day;
+
+ // find the current day of week
+ var startDow = otherDay.dayOfWeek();
+
+ // calculate the difference between current
+ // day of the week and desired day of the week
+ var offset = aDayOfWeek - startDow;
+
+
+ // if the offset goes into the past
+ // week we add 7 so its goes into the next
+ // week. We only want to go forward in time here.
+ if (offset < 0)
+ // this is really important otherwise we would
+ // end up with dates from in the past.
+ offset += 7;
+
+ // add offset to start so start is the same
+ // day of the week as the desired day of week.
+ start += offset;
+
+ // because we are going to add (and multiply)
+ // the numeric value of the day we subtract it
+ // from the start position so not to add it twice.
+ start -= aDayOfWeek;
+
+ // set week day
+ weekday = aDayOfWeek;
+ } else {
+
+ // then we set it to the last day in the current month
+ otherDay.day = daysInMonth;
+
+ // find the ends weekday
+ var endDow = otherDay.dayOfWeek();
+
+ pos++;
+
+ weekday = (endDow - aDayOfWeek);
+
+ if (weekday < 0) {
+ weekday += 7;
+ }
+
+ weekday = daysInMonth - weekday;
+ }
+
+ weekday += pos * 7;
+
+ return start + weekday;
+ },
+
+ /**
+ * Checks if current time is the nth weekday, relative to the current
+ * month. Will always return false when rule resolves outside of current
+ * month.
+ *
+ * @param {ICAL.Time.weekDay} aDayOfWeek Day of week to check
+ * @param {Number} aPos Relative position
+ * @return {Boolean} True, if its the nth weekday
+ */
+ isNthWeekDay: function(aDayOfWeek, aPos) {
+ var dow = this.dayOfWeek();
+
+ if (aPos === 0 && dow === aDayOfWeek) {
+ return true;
+ }
+
+ // get pos
+ var day = this.nthWeekDay(aDayOfWeek, aPos);
+
+ if (day === this.day) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Calculates the ISO 8601 week number. The first week of a year is the
+ * week that contains the first Thursday. The year can have 53 weeks, if
+ * January 1st is a Friday.
+ *
+ * Note there are regions where the first week of the year is the one that
+ * starts on January 1st, which may offset the week number. Also, if a
+ * different week start is specified, this will also affect the week
+ * number.
+ *
+ * @see ICAL.Time.weekOneStarts
+ * @param {ICAL.Time.weekDay} aWeekStart The weekday the week starts with
+ * @return {Number} The ISO week number
+ */
+ weekNumber: function weekNumber(aWeekStart) {
+ var wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart;
+ if (wnCacheKey in ICAL.Time._wnCache) {
+ return ICAL.Time._wnCache[wnCacheKey];
+ }
+ // This function courtesty of Julian Bucknall, published under the MIT license
+ // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html
+ // plus some fixes to be able to use different week starts.
+ var week1;
+
+ var dt = this.clone();
+ dt.isDate = true;
+ var isoyear = this.year;
+
+ if (dt.month == 12 && dt.day > 25) {
+ week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart);
+ if (dt.compare(week1) < 0) {
+ week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
+ } else {
+ isoyear++;
+ }
+ } else {
+ week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
+ if (dt.compare(week1) < 0) {
+ week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart);
+ }
+ }
+
+ var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400);
+ var answer = ICAL.helpers.trunc(daysBetween / 7) + 1;
+ ICAL.Time._wnCache[wnCacheKey] = answer;
+ return answer;
+ },
+
+ /**
+ * Adds the duration to the current time. The instance is modified in
+ * place.
+ *
+ * @param {ICAL.Duration} aDuration The duration to add
+ */
+ addDuration: function icaltime_add(aDuration) {
+ var mult = (aDuration.isNegative ? -1 : 1);
+
+ // because of the duration optimizations it is much
+ // more efficient to grab all the values up front
+ // then set them directly (which will avoid a normalization call).
+ // So we don't actually normalize until we need it.
+ var second = this.second;
+ var minute = this.minute;
+ var hour = this.hour;
+ var day = this.day;
+
+ second += mult * aDuration.seconds;
+ minute += mult * aDuration.minutes;
+ hour += mult * aDuration.hours;
+ day += mult * aDuration.days;
+ day += mult * 7 * aDuration.weeks;
+
+ this.second = second;
+ this.minute = minute;
+ this.hour = hour;
+ this.day = day;
+
+ this._cachedUnixTime = null;
+ },
+
+ /**
+ * Subtract the date details (_excluding_ timezone). Useful for finding
+ * the relative difference between two time objects excluding their
+ * timezone differences.
+ *
+ * @param {ICAL.Time} aDate The date to substract
+ * @return {ICAL.Duration} The difference as a duration
+ */
+ subtractDate: function icaltime_subtract(aDate) {
+ var unixTime = this.toUnixTime() + this.utcOffset();
+ var other = aDate.toUnixTime() + aDate.utcOffset();
+ return ICAL.Duration.fromSeconds(unixTime - other);
+ },
+
+ /**
+ * Subtract the date details, taking timezones into account.
+ *
+ * @param {ICAL.Time} aDate The date to subtract
+ * @return {ICAL.Duration} The difference in duration
+ */
+ subtractDateTz: function icaltime_subtract_abs(aDate) {
+ var unixTime = this.toUnixTime();
+ var other = aDate.toUnixTime();
+ return ICAL.Duration.fromSeconds(unixTime - other);
+ },
+
+ /**
+ * Compares the ICAL.Time instance with another one.
+ *
+ * @param {ICAL.Duration} aOther The instance to compare with
+ * @return {Number} -1, 0 or 1 for less/equal/greater
+ */
+ compare: function icaltime_compare(other) {
+ var a = this.toUnixTime();
+ var b = other.toUnixTime();
+
+ if (a > b) return 1;
+ if (b > a) return -1;
+ return 0;
+ },
+
+ /**
+ * Compares only the date part of this instance with another one.
+ *
+ * @param {ICAL.Duration} other The instance to compare with
+ * @param {ICAL.Timezone} tz The timezone to compare in
+ * @return {Number} -1, 0 or 1 for less/equal/greater
+ */
+ compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) {
+ function cmp(attr) {
+ return ICAL.Time._cmp_attr(a, b, attr);
+ }
+ var a = this.convertToZone(tz);
+ var b = other.convertToZone(tz);
+ var rc = 0;
+
+ if ((rc = cmp("year")) != 0) return rc;
+ if ((rc = cmp("month")) != 0) return rc;
+ if ((rc = cmp("day")) != 0) return rc;
+
+ return rc;
+ },
+
+ /**
+ * Convert the instance into another timzone. The returned ICAL.Time
+ * instance is always a copy.
+ *
+ * @param {ICAL.Timezone} zone The zone to convert to
+ * @return {ICAL.Time} The copy, converted to the zone
+ */
+ convertToZone: function convertToZone(zone) {
+ var copy = this.clone();
+ var zone_equals = (this.zone.tzid == zone.tzid);
+
+ if (!this.isDate && !zone_equals) {
+ ICAL.Timezone.convert_time(copy, this.zone, zone);
+ }
+
+ copy.zone = zone;
+ return copy;
+ },
+
+ /**
+ * Calculates the UTC offset of the current date/time in the timezone it is
+ * in.
+ *
+ * @return {Number} UTC offset in seconds
+ */
+ utcOffset: function utc_offset() {
+ if (this.zone == ICAL.Timezone.localTimezone ||
+ this.zone == ICAL.Timezone.utcTimezone) {
+ return 0;
+ } else {
+ return this.zone.utcOffset(this);
+ }
+ },
+
+ /**
+ * Returns an RFC 5545 compliant ical representation of this object.
+ *
+ * @return {String} ical date/date-time
+ */
+ toICALString: function() {
+ var string = this.toString();
+
+ if (string.length > 10) {
+ return ICAL.design.icalendar.value['date-time'].toICAL(string);
+ } else {
+ return ICAL.design.icalendar.value.date.toICAL(string);
+ }
+ },
+
+ /**
+ * The string representation of this date/time, in jCal form
+ * (including : and - separators).
+ * @return {String}
+ */
+ toString: function toString() {
+ var result = this.year + '-' +
+ ICAL.helpers.pad2(this.month) + '-' +
+ ICAL.helpers.pad2(this.day);
+
+ if (!this.isDate) {
+ result += 'T' + ICAL.helpers.pad2(this.hour) + ':' +
+ ICAL.helpers.pad2(this.minute) + ':' +
+ ICAL.helpers.pad2(this.second);
+
+ if (this.zone === ICAL.Timezone.utcTimezone) {
+ result += 'Z';
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Converts the current instance to a Javascript date
+ * @return {Date}
+ */
+ toJSDate: function toJSDate() {
+ if (this.zone == ICAL.Timezone.localTimezone) {
+ if (this.isDate) {
+ return new Date(this.year, this.month - 1, this.day);
+ } else {
+ return new Date(this.year, this.month - 1, this.day,
+ this.hour, this.minute, this.second, 0);
+ }
+ } else {
+ return new Date(this.toUnixTime() * 1000);
+ }
+ },
+
+ _normalize: function icaltime_normalize() {
+ var isDate = this._time.isDate;
+ if (this._time.isDate) {
+ this._time.hour = 0;
+ this._time.minute = 0;
+ this._time.second = 0;
+ }
+ this.adjust(0, 0, 0, 0);
+
+ return this;
+ },
+
+ /**
+ * Adjust the date/time by the given offset
+ *
+ * @param {Number} aExtraDays The extra amount of days
+ * @param {Number} aExtraHours The extra amount of hours
+ * @param {Number} aExtraMinutes The extra amount of minutes
+ * @param {Number} aExtraSeconds The extra amount of seconds
+ * @param {Number=} aTime The time to adjust, defaults to the
+ * current instance.
+ */
+ adjust: function icaltime_adjust(aExtraDays, aExtraHours,
+ aExtraMinutes, aExtraSeconds, aTime) {
+
+ var minutesOverflow, hoursOverflow,
+ daysOverflow = 0, yearsOverflow = 0;
+
+ var second, minute, hour, day;
+ var daysInMonth;
+
+ var time = aTime || this._time;
+
+ if (!time.isDate) {
+ second = time.second + aExtraSeconds;
+ time.second = second % 60;
+ minutesOverflow = ICAL.helpers.trunc(second / 60);
+ if (time.second < 0) {
+ time.second += 60;
+ minutesOverflow--;
+ }
+
+ minute = time.minute + aExtraMinutes + minutesOverflow;
+ time.minute = minute % 60;
+ hoursOverflow = ICAL.helpers.trunc(minute / 60);
+ if (time.minute < 0) {
+ time.minute += 60;
+ hoursOverflow--;
+ }
+
+ hour = time.hour + aExtraHours + hoursOverflow;
+
+ time.hour = hour % 24;
+ daysOverflow = ICAL.helpers.trunc(hour / 24);
+ if (time.hour < 0) {
+ time.hour += 24;
+ daysOverflow--;
+ }
+ }
+
+
+ // Adjust month and year first, because we need to know what month the day
+ // is in before adjusting it.
+ if (time.month > 12) {
+ yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12);
+ } else if (time.month < 1) {
+ yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1;
+ }
+
+ time.year += yearsOverflow;
+ time.month -= 12 * yearsOverflow;
+
+ // Now take care of the days (and adjust month if needed)
+ day = time.day + aExtraDays + daysOverflow;
+
+ if (day > 0) {
+ for (;;) {
+ daysInMonth = ICAL.Time.daysInMonth(time.month, time.year);
+ if (day <= daysInMonth) {
+ break;
+ }
+
+ time.month++;
+ if (time.month > 12) {
+ time.year++;
+ time.month = 1;
+ }
+
+ day -= daysInMonth;
+ }
+ } else {
+ while (day <= 0) {
+ if (time.month == 1) {
+ time.year--;
+ time.month = 12;
+ } else {
+ time.month--;
+ }
+
+ day += ICAL.Time.daysInMonth(time.month, time.year);
+ }
+ }
+
+ time.day = day;
+
+ this._cachedUnixTime = null;
+ return this;
+ },
+
+ /**
+ * Sets up the current instance from unix time, the number of seconds since
+ * January 1st, 1970.
+ *
+ * @param {Number} seconds The seconds to set up with
+ */
+ fromUnixTime: function fromUnixTime(seconds) {
+ this.zone = ICAL.Timezone.utcTimezone;
+ var epoch = ICAL.Time.epochTime.clone();
+ epoch.adjust(0, 0, 0, seconds);
+
+ this.year = epoch.year;
+ this.month = epoch.month;
+ this.day = epoch.day;
+ this.hour = epoch.hour;
+ this.minute = epoch.minute;
+ this.second = Math.floor(epoch.second);
+
+ this._cachedUnixTime = null;
+ },
+
+ /**
+ * Converts the current instance to seconds since January 1st 1970.
+ *
+ * @return {Number} Seconds since 1970
+ */
+ toUnixTime: function toUnixTime() {
+ if (this._cachedUnixTime !== null) {
+ return this._cachedUnixTime;
+ }
+ var offset = this.utcOffset();
+
+ // we use the offset trick to ensure
+ // that we are getting the actual UTC time
+ var ms = Date.UTC(
+ this.year,
+ this.month - 1,
+ this.day,
+ this.hour,
+ this.minute,
+ this.second - offset
+ );
+
+ // seconds
+ this._cachedUnixTime = ms / 1000;
+ return this._cachedUnixTime;
+ },
+
+ /**
+ * Converts time to into Object which can be serialized then re-created
+ * using the constructor.
+ *
+ * @example
+ * // toJSON will automatically be called
+ * var json = JSON.stringify(mytime);
+ *
+ * var deserialized = JSON.parse(json);
+ *
+ * var time = new ICAL.Time(deserialized);
+ *
+ * @return {Object}
+ */
+ toJSON: function() {
+ var copy = [
+ 'year',
+ 'month',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ 'isDate'
+ ];
+
+ var result = Object.create(null);
+
+ var i = 0;
+ var len = copy.length;
+ var prop;
+
+ for (; i < len; i++) {
+ prop = copy[i];
+ result[prop] = this[prop];
+ }
+
+ if (this.zone) {
+ result.timezone = this.zone.tzid;
+ }
+
+ return result;
+ }
+
+ };
+
+ (function setupNormalizeAttributes() {
+ // This needs to run before any instances are created!
+ function defineAttr(attr) {
+ Object.defineProperty(ICAL.Time.prototype, attr, {
+ get: function getTimeAttr() {
+ if (this._pendingNormalization) {
+ this._normalize();
+ this._pendingNormalization = false;
+ }
+
+ return this._time[attr];
+ },
+ set: function setTimeAttr(val) {
+ this._cachedUnixTime = null;
+ this._pendingNormalization = true;
+ this._time[attr] = val;
+
+ return val;
+ }
+ });
+
+ }
+
+ /* istanbul ignore else */
+ if ("defineProperty" in Object) {
+ defineAttr("year");
+ defineAttr("month");
+ defineAttr("day");
+ defineAttr("hour");
+ defineAttr("minute");
+ defineAttr("second");
+ defineAttr("isDate");
+ }
+ })();
+
+ /**
+ * Returns the days in the given month
+ *
+ * @param {Number} month The month to check
+ * @param {Number} year The year to check
+ * @return {Number} The number of days in the month
+ */
+ ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) {
+ var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ var days = 30;
+
+ if (month < 1 || month > 12) return days;
+
+ days = _daysInMonth[month];
+
+ if (month == 2) {
+ days += ICAL.Time.isLeapYear(year);
+ }
+
+ return days;
+ };
+
+ /**
+ * Checks if the year is a leap year
+ *
+ * @param {Number} year The year to check
+ * @return {Boolean} True, if the year is a leap year
+ */
+ ICAL.Time.isLeapYear = function isLeapYear(year) {
+ if (year <= 1752) {
+ return ((year % 4) == 0);
+ } else {
+ return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
+ }
+ };
+
+ /**
+ * Create a new ICAL.Time from the day of year and year. The date is returned
+ * in floating timezone.
+ *
+ * @param {Number} aDayOfYear The day of year
+ * @param {Number} aYear The year to create the instance in
+ * @return {ICAL.Time} The created instance with the calculated date
+ */
+ ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) {
+ var year = aYear;
+ var doy = aDayOfYear;
+ var tt = new ICAL.Time();
+ tt.auto_normalize = false;
+ var is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
+
+ if (doy < 1) {
+ year--;
+ is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
+ doy += ICAL.Time.daysInYearPassedMonth[is_leap][12];
+ return ICAL.Time.fromDayOfYear(doy, year);
+ } else if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][12]) {
+ is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
+ doy -= ICAL.Time.daysInYearPassedMonth[is_leap][12];
+ year++;
+ return ICAL.Time.fromDayOfYear(doy, year);
+ }
+
+ tt.year = year;
+ tt.isDate = true;
+
+ for (var month = 11; month >= 0; month--) {
+ if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][month]) {
+ tt.month = month + 1;
+ tt.day = doy - ICAL.Time.daysInYearPassedMonth[is_leap][month];
+ break;
+ }
+ }
+
+ tt.auto_normalize = true;
+ return tt;
+ };
+
+ /**
+ * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
+ *
+ * @deprecated Use {@link ICAL.Time.fromDateString} instead
+ * @param {String} str The string to create from
+ * @return {ICAL.Time} The date/time instance
+ */
+ ICAL.Time.fromStringv2 = function fromString(str) {
+ return new ICAL.Time({
+ year: parseInt(str.substr(0, 4), 10),
+ month: parseInt(str.substr(5, 2), 10),
+ day: parseInt(str.substr(8, 2), 10),
+ isDate: true
+ });
+ };
+
+ /**
+ * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
+ *
+ * @param {String} aValue The string to create from
+ * @return {ICAL.Time} The date/time instance
+ */
+ ICAL.Time.fromDateString = function(aValue) {
+ // Dates should have no timezone.
+ // Google likes to sometimes specify Z on dates
+ // we specifically ignore that to avoid issues.
+
+ // YYYY-MM-DD
+ // 2012-10-10
+ return new ICAL.Time({
+ year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
+ month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
+ day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
+ isDate: true
+ });
+ };
+
+ /**
+ * Returns a new ICAL.Time instance from a date-time string, e.g
+ * 2015-01-02T03:04:05. If a property is specified, the timezone is set up
+ * from the property's TZID parameter.
+ *
+ * @param {String} aValue The string to create from
+ * @param {ICAL.Property=} prop The property the date belongs to
+ * @return {ICAL.Time} The date/time instance
+ */
+ ICAL.Time.fromDateTimeString = function(aValue, prop) {
+ if (aValue.length < 19) {
+ throw new Error(
+ 'invalid date-time value: "' + aValue + '"'
+ );
+ }
+
+ var zone;
+
+ if (aValue[19] && aValue[19] === 'Z') {
+ zone = 'Z';
+ } else if (prop) {
+ zone = prop.getParameter('tzid');
+ }
+
+ // 2012-10-10T10:10:10(Z)?
+ var time = new ICAL.Time({
+ year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
+ month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
+ day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
+ hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)),
+ minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)),
+ second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)),
+ timezone: zone
+ });
+
+ return time;
+ };
+
+ /**
+ * Returns a new ICAL.Time instance from a date or date-time string,
+ *
+ * @param {String} aValue The string to create from
+ * @return {ICAL.Time} The date/time instance
+ */
+ ICAL.Time.fromString = function fromString(aValue) {
+ if (aValue.length > 10) {
+ return ICAL.Time.fromDateTimeString(aValue);
+ } else {
+ return ICAL.Time.fromDateString(aValue);
+ }
+ };
+
+ /**
+ * Creates a new ICAL.Time instance from the given Javascript Date.
+ *
+ * @param {?Date} aDate The Javascript Date to read, or null to reset
+ * @param {Boolean} useUTC If true, the UTC values of the date will be used
+ */
+ ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) {
+ var tt = new ICAL.Time();
+ return tt.fromJSDate(aDate, useUTC);
+ };
+
+ /**
+ * Creates a new ICAL.Time instance from the the passed data object.
+ *
+ * @param {Object} aData Time initialization
+ * @param {Number=} aData.year The year for this date
+ * @param {Number=} aData.month The month for this date
+ * @param {Number=} aData.day The day for this date
+ * @param {Number=} aData.hour The hour for this date
+ * @param {Number=} aData.minute The minute for this date
+ * @param {Number=} aData.second The second for this date
+ * @param {Boolean=} aData.isDate If true, the instance represents a date
+ * (as opposed to a date-time)
+ * @param {ICAL.Timezone=} aZone Timezone this position occurs in
+ */
+ ICAL.Time.fromData = function fromData(aData, aZone) {
+ var t = new ICAL.Time();
+ return t.fromData(aData, aZone);
+ };
+
+ /**
+ * Creates a new ICAL.Time instance from the current moment.
+ * @return {ICAL.Time}
+ */
+ ICAL.Time.now = function icaltime_now() {
+ return ICAL.Time.fromJSDate(new Date(), false);
+ };
+
+ /**
+ * Returns the date on which ISO week number 1 starts.
+ *
+ * @see ICAL.Time#weekNumber
+ * @param {Number} aYear The year to search in
+ * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday, used for calculation.
+ * @return {ICAL.Time} The date on which week number 1 starts
+ */
+ ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) {
+ var t = ICAL.Time.fromData({
+ year: aYear,
+ month: 1,
+ day: 1,
+ isDate: true
+ });
+
+ var dow = t.dayOfWeek();
+ var wkst = aWeekStart || ICAL.Time.DEFAULT_WEEK_START;
+ if (dow > ICAL.Time.THURSDAY) {
+ t.day += 7;
+ }
+ if (wkst > ICAL.Time.THURSDAY) {
+ t.day -= 7;
+ }
+
+ t.day -= dow - wkst;
+
+ return t;
+ };
+
+ /**
+ * Get the dominical letter for the given year. Letters range from A - G for
+ * common years, and AG to GF for leap years.
+ *
+ * @param {Number} yr The year to retrieve the letter for
+ * @return {String} The dominical letter.
+ */
+ ICAL.Time.getDominicalLetter = function(yr) {
+ var LTRS = "GFEDCBA";
+ var dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7;
+ var isLeap = ICAL.Time.isLeapYear(yr);
+ if (isLeap) {
+ return LTRS[(dom + 6) % 7] + LTRS[dom];
+ } else {
+ return LTRS[dom];
+ }
+ };
+
+ /**
+ * January 1st, 1970 as an ICAL.Time.
+ * @type {ICAL.Time}
+ * @constant
+ * @instance
+ */
+ ICAL.Time.epochTime = ICAL.Time.fromData({
+ year: 1970,
+ month: 1,
+ day: 1,
+ hour: 0,
+ minute: 0,
+ second: 0,
+ isDate: false,
+ timezone: "Z"
+ });
+
+ ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) {
+ if (a[attr] > b[attr]) return 1;
+ if (a[attr] < b[attr]) return -1;
+ return 0;
+ };
+
+ /**
+ * The days that have passed in the year after a given month. The array has
+ * two members, one being an array of passed days for non-leap years, the
+ * other analog for leap years.
+ * @example
+ * var isLeapYear = ICAL.Time.isLeapYear(year);
+ * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month];
+ * @type {Array.<Array.<Number>>}
+ */
+ ICAL.Time.daysInYearPassedMonth = [
+ [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
+ [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
+ ];
+
+ /**
+ * The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via
+ * ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ...
+ *
+ * @typedef {Number} weekDay
+ * @memberof ICAL.Time
+ */
+
+ ICAL.Time.SUNDAY = 1;
+ ICAL.Time.MONDAY = 2;
+ ICAL.Time.TUESDAY = 3;
+ ICAL.Time.WEDNESDAY = 4;
+ ICAL.Time.THURSDAY = 5;
+ ICAL.Time.FRIDAY = 6;
+ ICAL.Time.SATURDAY = 7;
+
+ /**
+ * The default weekday for the WKST part.
+ * @constant
+ * @default ICAL.Time.MONDAY
+ */
+ ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY;
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2015 */
+
+
+
+(function() {
+
+ /**
+ * Describes a vCard time, which has slight differences to the ICAL.Time.
+ * Properties can be null if not specified, for example for dates with
+ * reduced accuracy or truncation.
+ *
+ * Note that currently not all methods are correctly re-implemented for
+ * VCardTime. For example, comparison will have undefined results when some
+ * members are null.
+ *
+ * Also, normalization is not yet implemented for this class!
+ *
+ * @alias ICAL.VCardTime
+ * @class
+ * @extends {ICAL.Time}
+ * @param {Object} data The data for the time instance
+ * @param {Number=} data.year The year for this date
+ * @param {Number=} data.month The month for this date
+ * @param {Number=} data.day The day for this date
+ * @param {Number=} data.hour The hour for this date
+ * @param {Number=} data.minute The minute for this date
+ * @param {Number=} data.second The second for this date
+ * @param {ICAL.Timezone|ICAL.UtcOffset} zone The timezone to use
+ * @param {String} icaltype The type for this date/time object
+ */
+ ICAL.VCardTime = function(data, zone, icaltype) {
+ this.wrappedJSObject = this;
+ var time = this._time = Object.create(null);
+
+ time.year = null;
+ time.month = null;
+ time.day = null;
+ time.hour = null;
+ time.minute = null;
+ time.second = null;
+
+ this.icaltype = icaltype || "date-and-or-time";
+
+ this.fromData(data, zone);
+ };
+ ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ {
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "vcardtime"
+ */
+ icalclass: "vcardtime",
+
+ /**
+ * The type name, to be used in the jCal object.
+ * @type {String}
+ * @default "date-and-or-time"
+ */
+ icaltype: "date-and-or-time",
+
+ /**
+ * The timezone. This can either be floating, UTC, or an instance of
+ * ICAL.UtcOffset.
+ * @type {ICAL.Timezone|ICAL.UtcOFfset}
+ */
+ zone: null,
+
+ /**
+ * Returns a clone of the vcard date/time object.
+ *
+ * @return {ICAL.VCardTime} The cloned object
+ */
+ clone: function() {
+ return new ICAL.VCardTime(this._time, this.zone, this.icaltype);
+ },
+
+ _normalize: function() {
+ return this;
+ },
+
+ /**
+ * @inheritdoc
+ */
+ utcOffset: function() {
+ if (this.zone instanceof ICAL.UtcOffset) {
+ return this.zone.toSeconds();
+ } else {
+ return ICAL.Time.prototype.utcOffset.apply(this, arguments);
+ }
+ },
+
+ /**
+ * Returns an RFC 6350 compliant representation of this object.
+ *
+ * @return {String} vcard date/time string
+ */
+ toICALString: function() {
+ return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString());
+ },
+
+ /**
+ * The string representation of this date/time, in jCard form
+ * (including : and - separators).
+ * @return {String}
+ */
+ toString: function toString() {
+ var p2 = ICAL.helpers.pad2;
+ var y = this.year, m = this.month, d = this.day;
+ var h = this.hour, mm = this.minute, s = this.second;
+
+ var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null;
+ var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null;
+
+ var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) +
+ (hasMonth ? p2(m) : '') +
+ (hasDay ? '-' + p2(d) : '');
+ var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') +
+ (hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') +
+ (hasMinute && hasSecond ? ':' : '') +
+ (hasSecond ? p2(s) : '');
+
+ var zone;
+ if (this.zone === ICAL.Timezone.utcTimezone) {
+ zone = 'Z';
+ } else if (this.zone instanceof ICAL.UtcOffset) {
+ zone = this.zone.toString();
+ } else if (this.zone === ICAL.Timezone.localTimezone) {
+ zone = '';
+ } else if (this.zone instanceof ICAL.Timezone) {
+ var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this));
+ zone = offset.toString();
+ } else {
+ zone = '';
+ }
+
+ switch (this.icaltype) {
+ case "time":
+ return timepart + zone;
+ case "date-and-or-time":
+ case "date-time":
+ return datepart + (timepart == '--' ? '' : 'T' + timepart + zone);
+ case "date":
+ return datepart;
+ }
+ return null;
+ }
+ });
+
+ /**
+ * Returns a new ICAL.VCardTime instance from a date and/or time string.
+ *
+ * @param {String} aValue The string to create from
+ * @param {String} aIcalType The type for this instance, e.g. date-and-or-time
+ * @return {ICAL.VCardTime} The date/time instance
+ */
+ ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) {
+ function part(v, s, e) {
+ return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null;
+ }
+ var parts = aValue.split('T');
+ var dt = parts[0], tmz = parts[1];
+ var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : [];
+ var zone = splitzone[0], tm = splitzone[1];
+
+ var stoi = ICAL.helpers.strictParseInt;
+ var dtlen = dt ? dt.length : 0;
+ var tmlen = tm ? tm.length : 0;
+
+ var hasDashDate = dt && dt[0] == '-' && dt[1] == '-';
+ var hasDashTime = tm && tm[0] == '-';
+
+ var o = {
+ year: hasDashDate ? null : part(dt, 0, 4),
+ month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null,
+ day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null,
+
+ hour: hasDashTime ? null : part(tm, 0, 2),
+ minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null,
+ second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null
+ };
+
+ if (zone == 'Z') {
+ zone = ICAL.Timezone.utcTimezone;
+ } else if (zone && zone[3] == ':') {
+ zone = ICAL.UtcOffset.fromString(zone);
+ } else {
+ zone = null;
+ }
+
+ return new ICAL.VCardTime(o, zone, aIcalType);
+ };
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+
+(function() {
+ var DOW_MAP = {
+ SU: ICAL.Time.SUNDAY,
+ MO: ICAL.Time.MONDAY,
+ TU: ICAL.Time.TUESDAY,
+ WE: ICAL.Time.WEDNESDAY,
+ TH: ICAL.Time.THURSDAY,
+ FR: ICAL.Time.FRIDAY,
+ SA: ICAL.Time.SATURDAY
+ };
+
+ var REVERSE_DOW_MAP = {};
+ for (var key in DOW_MAP) {
+ /* istanbul ignore else */
+ if (DOW_MAP.hasOwnProperty(key)) {
+ REVERSE_DOW_MAP[DOW_MAP[key]] = key;
+ }
+ }
+
+ var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
+ "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO",
+ "BYMONTH", "BYSETPOS"];
+
+ /**
+ * @classdesc
+ * This class represents the "recur" value type, with various calculation
+ * and manipulation methods.
+ *
+ * @class
+ * @alias ICAL.Recur
+ * @param {Object} data An object with members of the recurrence
+ * @param {ICAL.Recur.frequencyValues} freq The frequency value
+ * @param {Number=} data.interval The INTERVAL value
+ * @param {ICAL.Time.weekDay=} data.wkst The week start value
+ * @param {ICAL.Time=} data.until The end of the recurrence set
+ * @param {Number=} data.count The number of occurrences
+ * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
+ * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
+ * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
+ * @param {Array.<String>=} data.byday The BYDAY values
+ * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
+ * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
+ * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
+ * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
+ * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
+ */
+ ICAL.Recur = function icalrecur(data) {
+ this.wrappedJSObject = this;
+ this.parts = {};
+
+ if (data && typeof(data) === 'object') {
+ this.fromData(data);
+ }
+ };
+
+ ICAL.Recur.prototype = {
+ /**
+ * An object holding the BY-parts of the recurrence rule
+ * @type {Object}
+ */
+ parts: null,
+
+ /**
+ * The interval value for the recurrence rule.
+ * @type {Number}
+ */
+ interval: 1,
+
+ /**
+ * The week start day
+ *
+ * @type {ICAL.Time.weekDay}
+ * @default ICAL.Time.MONDAY
+ */
+ wkst: ICAL.Time.MONDAY,
+
+ /**
+ * The end of the recurrence
+ * @type {?ICAL.Time}
+ */
+ until: null,
+
+ /**
+ * The maximum number of occurrences
+ * @type {?Number}
+ */
+ count: null,
+
+ /**
+ * The frequency value.
+ * @type {ICAL.Recur.frequencyValues}
+ */
+ freq: null,
+
+ /**
+ * The class identifier.
+ * @constant
+ * @type {String}
+ * @default "icalrecur"
+ */
+ icalclass: "icalrecur",
+
+ /**
+ * The type name, to be used in the jCal object.
+ * @constant
+ * @type {String}
+ * @default "recur"
+ */
+ icaltype: "recur",
+
+ /**
+ * Create a new iterator for this recurrence rule. The passed start date
+ * must be the start date of the event, not the start of the range to
+ * search in.
+ *
+ * @example
+ * var recur = comp.getFirstPropertyValue('rrule');
+ * var dtstart = comp.getFirstPropertyValue('dtstart');
+ * var iter = recur.iterator(dtstart);
+ * for (var next = iter.next(); next; next = iter.next()) {
+ * if (next.compare(rangeStart) < 0) {
+ * continue;
+ * }
+ * console.log(next.toString());
+ * }
+ *
+ * @param {ICAL.Time} aStart The item's start date
+ * @return {ICAL.RecurIterator} The recurrence iterator
+ */
+ iterator: function(aStart) {
+ return new ICAL.RecurIterator({
+ rule: this,
+ dtstart: aStart
+ });
+ },
+
+ /**
+ * Returns a clone of the recurrence object.
+ *
+ * @return {ICAL.Recur} The cloned object
+ */
+ clone: function clone() {
+ return new ICAL.Recur(this.toJSON());
+ },
+
+ /**
+ * Checks if the current rule is finite, i.e. has a count or until part.
+ *
+ * @return {Boolean} True, if the rule is finite
+ */
+ isFinite: function isfinite() {
+ return !!(this.count || this.until);
+ },
+
+ /**
+ * Checks if the current rule has a count part, and not limited by an until
+ * part.
+ *
+ * @return {Boolean} True, if the rule is by count
+ */
+ isByCount: function isbycount() {
+ return !!(this.count && !this.until);
+ },
+
+ /**
+ * Adds a component (part) to the recurrence rule. This is not a component
+ * in the sense of {@link ICAL.Component}, but a part of the recurrence
+ * rule, i.e. BYMONTH.
+ *
+ * @param {String} aType The name of the component part
+ * @param {Array|String} aValue The component value
+ */
+ addComponent: function addPart(aType, aValue) {
+ var ucname = aType.toUpperCase();
+ if (ucname in this.parts) {
+ this.parts[ucname].push(aValue);
+ } else {
+ this.parts[ucname] = [aValue];
+ }
+ },
+
+ /**
+ * Sets the component value for the given by-part.
+ *
+ * @param {String} aType The component part name
+ * @param {Array} aValues The component values
+ */
+ setComponent: function setComponent(aType, aValues) {
+ this.parts[aType.toUpperCase()] = aValues.slice();
+ },
+
+ /**
+ * Gets (a copy) of the requested component value.
+ *
+ * @param {String} aType The component part name
+ * @return {Array} The component part value
+ */
+ getComponent: function getComponent(aType) {
+ var ucname = aType.toUpperCase();
+ return (ucname in this.parts ? this.parts[ucname].slice() : []);
+ },
+
+ /**
+ * Retrieves the next occurrence after the given recurrence id. See the
+ * guide on {@tutorial terminology} for more details.
+ *
+ * NOTE: Currently, this method iterates all occurrences from the start
+ * date. It should not be called in a loop for performance reasons. If you
+ * would like to get more than one occurrence, you can iterate the
+ * occurrences manually, see the example on the
+ * {@link ICAL.Recur#iterator iterator} method.
+ *
+ * @param {ICAL.Time} aStartTime The start of the event series
+ * @param {ICAL.Time} aRecurrenceId The date of the last occurrence
+ * @return {ICAL.Time} The next occurrence after
+ */
+ getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) {
+ var iter = this.iterator(aStartTime);
+ var next, cdt;
+
+ do {
+ next = iter.next();
+ } while (next && next.compare(aRecurrenceId) <= 0);
+
+ if (next && aRecurrenceId.zone) {
+ next.zone = aRecurrenceId.zone;
+ }
+
+ return next;
+ },
+
+ /**
+ * Sets up the current instance using members from the passed data object.
+ *
+ * @param {Object} data An object with members of the recurrence
+ * @param {ICAL.Recur.frequencyValues} freq The frequency value
+ * @param {Number=} data.interval The INTERVAL value
+ * @param {ICAL.Time.weekDay=} data.wkst The week start value
+ * @param {ICAL.Time=} data.until The end of the recurrence set
+ * @param {Number=} data.count The number of occurrences
+ * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
+ * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
+ * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
+ * @param {Array.<String>=} data.byday The BYDAY values
+ * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
+ * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
+ * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
+ * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
+ * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
+ */
+ fromData: function(data) {
+ for (var key in data) {
+ var uckey = key.toUpperCase();
+
+ if (uckey in partDesign) {
+ if (Array.isArray(data[key])) {
+ this.parts[uckey] = data[key];
+ } else {
+ this.parts[uckey] = [data[key]];
+ }
+ } else {
+ this[key] = data[key];
+ }
+ }
+
+ if (this.wkst && typeof this.wkst != "number") {
+ this.wkst = ICAL.Recur.icalDayToNumericDay(this.wkst);
+ }
+
+ if (this.until && !(this.until instanceof ICAL.Time)) {
+ this.until = ICAL.Time.fromString(this.until);
+ }
+ },
+
+ /**
+ * The jCal representation of this recurrence type.
+ * @return {Object}
+ */
+ toJSON: function() {
+ var res = Object.create(null);
+ res.freq = this.freq;
+
+ if (this.count) {
+ res.count = this.count;
+ }
+
+ if (this.interval > 1) {
+ res.interval = this.interval;
+ }
+
+ for (var k in this.parts) {
+ /* istanbul ignore if */
+ if (!this.parts.hasOwnProperty(k)) {
+ continue;
+ }
+ var kparts = this.parts[k];
+ if (Array.isArray(kparts) && kparts.length == 1) {
+ res[k.toLowerCase()] = kparts[0];
+ } else {
+ res[k.toLowerCase()] = ICAL.helpers.clone(this.parts[k]);
+ }
+ }
+
+ if (this.until) {
+ res.until = this.until.toString();
+ }
+ if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
+ res.wkst = ICAL.Recur.numericDayToIcalDay(this.wkst);
+ }
+ return res;
+ },
+
+ /**
+ * The string representation of this recurrence rule.
+ * @return {String}
+ */
+ toString: function icalrecur_toString() {
+ // TODO retain order
+ var str = "FREQ=" + this.freq;
+ if (this.count) {
+ str += ";COUNT=" + this.count;
+ }
+ if (this.interval > 1) {
+ str += ";INTERVAL=" + this.interval;
+ }
+ for (var k in this.parts) {
+ /* istanbul ignore else */
+ if (this.parts.hasOwnProperty(k)) {
+ str += ";" + k + "=" + this.parts[k];
+ }
+ }
+ if (this.until) {
+ str += ';UNTIL=' + this.until.toString();
+ }
+ if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
+ str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst);
+ }
+ return str;
+ }
+ };
+
+ function parseNumericValue(type, min, max, value) {
+ var result = value;
+
+ if (value[0] === '+') {
+ result = value.substr(1);
+ }
+
+ result = ICAL.helpers.strictParseInt(result);
+
+ if (min !== undefined && value < min) {
+ throw new Error(
+ type + ': invalid value "' + value + '" must be > ' + min
+ );
+ }
+
+ if (max !== undefined && value > max) {
+ throw new Error(
+ type + ': invalid value "' + value + '" must be < ' + min
+ );
+ }
+
+ return result;
+ }
+
+ /**
+ * Convert an ical representation of a day (SU, MO, etc..)
+ * into a numeric value of that day.
+ *
+ * @param {String} string The iCalendar day name
+ * @return {Number} Numeric value of given day
+ */
+ ICAL.Recur.icalDayToNumericDay = function toNumericDay(string) {
+ //XXX: this is here so we can deal
+ // with possibly invalid string values.
+
+ return DOW_MAP[string];
+ };
+
+ /**
+ * Convert a numeric day value into its ical representation (SU, MO, etc..)
+ *
+ * @param {Number} num Numeric value of given day
+ * @return {String} The ICAL day value, e.g SU,MO,...
+ */
+ ICAL.Recur.numericDayToIcalDay = function toIcalDay(num) {
+ //XXX: this is here so we can deal with possibly invalid number values.
+ // Also, this allows consistent mapping between day numbers and day
+ // names for external users.
+ return REVERSE_DOW_MAP[num];
+ };
+
+ var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/;
+ var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/;
+
+ /**
+ * Possible frequency values for the FREQ part
+ * (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY)
+ *
+ * @typedef {String} frequencyValues
+ * @memberof ICAL.Recur
+ */
+
+ var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY',
+ 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
+
+ var optionDesign = {
+ FREQ: function(value, dict, fmtIcal) {
+ // yes this is actually equal or faster then regex.
+ // upside here is we can enumerate the valid values.
+ if (ALLOWED_FREQ.indexOf(value) !== -1) {
+ dict.freq = value;
+ } else {
+ throw new Error(
+ 'invalid frequency "' + value + '" expected: "' +
+ ALLOWED_FREQ.join(', ') + '"'
+ );
+ }
+ },
+
+ COUNT: function(value, dict, fmtIcal) {
+ dict.count = ICAL.helpers.strictParseInt(value);
+ },
+
+ INTERVAL: function(value, dict, fmtIcal) {
+ dict.interval = ICAL.helpers.strictParseInt(value);
+ if (dict.interval < 1) {
+ // 0 or negative values are not allowed, some engines seem to generate
+ // it though. Assume 1 instead.
+ dict.interval = 1;
+ }
+ },
+
+ UNTIL: function(value, dict, fmtIcal) {
+ if (fmtIcal) {
+ if (value.length > 10) {
+ dict.until = ICAL.design.icalendar.value['date-time'].fromICAL(value);
+ } else {
+ dict.until = ICAL.design.icalendar.value.date.fromICAL(value);
+ }
+ } else {
+ dict.until = ICAL.Time.fromString(value);
+ }
+ },
+
+ WKST: function(value, dict, fmtIcal) {
+ if (VALID_DAY_NAMES.test(value)) {
+ dict.wkst = ICAL.Recur.icalDayToNumericDay(value);
+ } else {
+ throw new Error('invalid WKST value "' + value + '"');
+ }
+ }
+ };
+
+ var partDesign = {
+ BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60),
+ BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59),
+ BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23),
+ BYDAY: function(value) {
+ if (VALID_BYDAY_PART.test(value)) {
+ return value;
+ } else {
+ throw new Error('invalid BYDAY value "' + value + '"');
+ }
+ },
+ BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31),
+ BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366),
+ BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53),
+ BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12),
+ BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366)
+ };
+
+
+ /**
+ * Creates a new {@link ICAL.Recur} instance from the passed string.
+ *
+ * @param {String} string The string to parse
+ * @return {ICAL.Recur} The created recurrence instance
+ */
+ ICAL.Recur.fromString = function(string) {
+ var data = ICAL.Recur._stringToData(string, false);
+ return new ICAL.Recur(data);
+ };
+
+ /**
+ * Creates a new {@link ICAL.Recur} instance using members from the passed
+ * data object.
+ *
+ * @param {Object} aData An object with members of the recurrence
+ * @param {ICAL.Recur.frequencyValues} freq The frequency value
+ * @param {Number=} aData.interval The INTERVAL value
+ * @param {ICAL.Time.weekDay=} aData.wkst The week start value
+ * @param {ICAL.Time=} aData.until The end of the recurrence set
+ * @param {Number=} aData.count The number of occurrences
+ * @param {Array.<Number>=} aData.bysecond The seconds for the BYSECOND part
+ * @param {Array.<Number>=} aData.byminute The minutes for the BYMINUTE part
+ * @param {Array.<Number>=} aData.byhour The hours for the BYHOUR part
+ * @param {Array.<String>=} aData.byday The BYDAY values
+ * @param {Array.<Number>=} aData.bymonthday The days for the BYMONTHDAY part
+ * @param {Array.<Number>=} aData.byyearday The days for the BYYEARDAY part
+ * @param {Array.<Number>=} aData.byweekno The weeks for the BYWEEKNO part
+ * @param {Array.<Number>=} aData.bymonth The month for the BYMONTH part
+ * @param {Array.<Number>=} aData.bysetpos The positionals for the BYSETPOS part
+ */
+ ICAL.Recur.fromData = function(aData) {
+ return new ICAL.Recur(aData);
+ };
+
+ /**
+ * Converts a recurrence string to a data object, suitable for the fromData
+ * method.
+ *
+ * @param {String} string The string to parse
+ * @param {Boolean} fmtIcal If true, the string is considered to be an
+ * iCalendar string
+ * @return {ICAL.Recur} The recurrence instance
+ */
+ ICAL.Recur._stringToData = function(string, fmtIcal) {
+ var dict = Object.create(null);
+
+ // split is slower in FF but fast enough.
+ // v8 however this is faster then manual split?
+ var values = string.split(';');
+ var len = values.length;
+
+ for (var i = 0; i < len; i++) {
+ var parts = values[i].split('=');
+ var ucname = parts[0].toUpperCase();
+ var lcname = parts[0].toLowerCase();
+ var name = (fmtIcal ? lcname : ucname);
+ var value = parts[1];
+
+ if (ucname in partDesign) {
+ var partArr = value.split(',');
+ var partArrIdx = 0;
+ var partArrLen = partArr.length;
+
+ for (; partArrIdx < partArrLen; partArrIdx++) {
+ partArr[partArrIdx] = partDesign[ucname](partArr[partArrIdx]);
+ }
+ dict[name] = (partArr.length == 1 ? partArr[0] : partArr);
+ } else if (ucname in optionDesign) {
+ optionDesign[ucname](value, dict, fmtIcal);
+ } else {
+ // Don't swallow unknown values. Just set them as they are.
+ dict[lcname] = value;
+ }
+ }
+
+ return dict;
+ };
+})();
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.RecurIterator = (function() {
+
+ /**
+ * @classdesc
+ * An iterator for a single recurrence rule. This class usually doesn't have
+ * to be instanciated directly, the convenience method
+ * {@link ICAL.Recur#iterator} can be used.
+ *
+ * @description
+ * The options object may contain additional members when resuming iteration from a previous run
+ *
+ * @description
+ * The options object may contain additional members when resuming iteration
+ * from a previous run.
+ *
+ * @class
+ * @alias ICAL.RecurIterator
+ * @param {Object} options The iterator options
+ * @param {ICAL.Recur} options.rule The rule to iterate.
+ * @param {ICAL.Time} options.dtstart The start date of the event.
+ * @param {Boolean=} options.initialized When true, assume that options are
+ * from a previously constructed iterator. Initialization will not be
+ * repeated.
+ */
+ function icalrecur_iterator(options) {
+ this.fromData(options);
+ }
+
+ icalrecur_iterator.prototype = {
+
+ /**
+ * True when iteration is finished.
+ * @type {Boolean}
+ */
+ completed: false,
+
+ /**
+ * The rule that is being iterated
+ * @type {ICAL.Recur}
+ */
+ rule: null,
+
+ /**
+ * The start date of the event being iterated.
+ * @type {ICAL.Time}
+ */
+ dtstart: null,
+
+ /**
+ * The last occurrence that was returned from the
+ * {@link ICAL.RecurIterator#next} method.
+ * @type {ICAL.Time}
+ */
+ last: null,
+
+ /**
+ * The sequence number from the occurrence
+ * @type {Number}
+ */
+ occurrence_number: 0,
+
+ /**
+ * The indices used for the {@link ICAL.RecurIterator#by_data} object.
+ * @type {Object}
+ * @private
+ */
+ by_indices: null,
+
+ /**
+ * If true, the iterator has already been initialized
+ * @type {Boolean}
+ * @private
+ */
+ initialized: false,
+
+ /**
+ * The initializd by-data.
+ * @type {Object}
+ * @private
+ */
+ by_data: null,
+
+ /**
+ * The expanded yeardays
+ * @type {Array}
+ * @private
+ */
+ days: null,
+
+ /**
+ * The index in the {@link ICAL.RecurIterator#days} array.
+ * @type {Number}
+ * @private
+ */
+ days_index: 0,
+
+ /**
+ * Initialize the recurrence iterator from the passed data object. This
+ * method is usually not called directly, you can initialize the iterator
+ * through the constructor.
+ *
+ * @param {Object} options The iterator options
+ * @param {ICAL.Recur} options.rule The rule to iterate.
+ * @param {ICAL.Time} options.dtstart The start date of the event.
+ * @param {Boolean=} options.initialized When true, assume that options are
+ * from a previously constructed iterator. Initialization will not be
+ * repeated.
+ */
+ fromData: function(options) {
+ this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur);
+
+ if (!this.rule) {
+ throw new Error('iterator requires a (ICAL.Recur) rule');
+ }
+
+ this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
+
+ if (!this.dtstart) {
+ throw new Error('iterator requires a (ICAL.Time) dtstart');
+ }
+
+ if (options.by_data) {
+ this.by_data = options.by_data;
+ } else {
+ this.by_data = ICAL.helpers.clone(this.rule.parts, true);
+ }
+
+ if (options.occurrence_number)
+ this.occurrence_number = options.occurrence_number;
+
+ this.days = options.days || [];
+ if (options.last) {
+ this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time);
+ }
+
+ this.by_indices = options.by_indices;
+
+ if (!this.by_indices) {
+ this.by_indices = {
+ "BYSECOND": 0,
+ "BYMINUTE": 0,
+ "BYHOUR": 0,
+ "BYDAY": 0,
+ "BYMONTH": 0,
+ "BYWEEKNO": 0,
+ "BYMONTHDAY": 0
+ };
+ }
+
+ this.initialized = options.initialized || false;
+
+ if (!this.initialized) {
+ this.init();
+ }
+ },
+
+ /**
+ * Intialize the iterator
+ * @private
+ */
+ init: function icalrecur_iterator_init() {
+ this.initialized = true;
+ this.last = this.dtstart.clone();
+ var parts = this.by_data;
+
+ if ("BYDAY" in parts) {
+ // libical does this earlier when the rule is loaded, but we postpone to
+ // now so we can preserve the original order.
+ this.sort_byday_rules(parts.BYDAY, this.rule.wkst);
+ }
+
+ // If the BYYEARDAY appares, no other date rule part may appear
+ if ("BYYEARDAY" in parts) {
+ if ("BYMONTH" in parts || "BYWEEKNO" in parts ||
+ "BYMONTHDAY" in parts || "BYDAY" in parts) {
+ throw new Error("Invalid BYYEARDAY rule");
+ }
+ }
+
+ // BYWEEKNO and BYMONTHDAY rule parts may not both appear
+ if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) {
+ throw new Error("BYWEEKNO does not fit to BYMONTHDAY");
+ }
+
+ // For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor
+ // BYWEEKNO may appear.
+ if (this.rule.freq == "MONTHLY" &&
+ ("BYYEARDAY" in parts || "BYWEEKNO" in parts)) {
+ throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear");
+ }
+
+ // For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor
+ // BYYEARDAY may appear.
+ if (this.rule.freq == "WEEKLY" &&
+ ("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) {
+ throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear");
+ }
+
+ // BYYEARDAY may only appear in YEARLY rules
+ if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) {
+ throw new Error("BYYEARDAY may only appear in YEARLY rules");
+ }
+
+ this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second);
+ this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute);
+ this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour);
+ this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day);
+ this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month);
+
+ if (this.rule.freq == "WEEKLY") {
+ if ("BYDAY" in parts) {
+ var bydayParts = this.ruleDayOfWeek(parts.BYDAY[0]);
+ var pos = bydayParts[0];
+ var dow = bydayParts[1];
+ var wkdy = dow - this.last.dayOfWeek();
+ if ((this.last.dayOfWeek() < dow && wkdy >= 0) || wkdy < 0) {
+ // Initial time is after first day of BYDAY data
+ this.last.day += wkdy;
+ }
+ } else {
+ var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek());
+ parts.BYDAY = [dayName];
+ }
+ }
+
+ if (this.rule.freq == "YEARLY") {
+ for (;;) {
+ this.expand_year_days(this.last.year);
+ if (this.days.length > 0) {
+ break;
+ }
+ this.increment_year(this.rule.interval);
+ }
+
+ this._nextByYearDay();
+ }
+
+ if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) {
+ var tempLast = null;
+ var initLast = this.last.clone();
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+
+ // Check every weekday in BYDAY with relative dow and pos.
+ for (var i in this.by_data.BYDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYDAY.hasOwnProperty(i)) {
+ continue;
+ }
+ this.last = initLast.clone();
+ var bydayParts = this.ruleDayOfWeek(this.by_data.BYDAY[i]);
+ var pos = bydayParts[0];
+ var dow = bydayParts[1];
+ var dayOfMonth = this.last.nthWeekDay(dow, pos);
+
+ // If |pos| >= 6, the byday is invalid for a monthly rule.
+ if (pos >= 6 || pos <= -6) {
+ throw new Error("Malformed values in BYDAY part");
+ }
+
+ // If a Byday with pos=+/-5 is not in the current month it
+ // must be searched in the next months.
+ if (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
+ // Skip if we have already found a "last" in this month.
+ if (tempLast && tempLast.month == initLast.month) {
+ continue;
+ }
+ while (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
+ this.increment_month();
+ daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ dayOfMonth = this.last.nthWeekDay(dow, pos);
+ }
+ }
+
+ this.last.day = dayOfMonth;
+ if (!tempLast || this.last.compare(tempLast) < 0) {
+ tempLast = this.last.clone();
+ }
+ }
+ this.last = tempLast.clone();
+
+ //XXX: This feels like a hack, but we need to initialize
+ // the BYMONTHDAY case correctly and byDayAndMonthDay handles
+ // this case. It accepts a special flag which will avoid incrementing
+ // the initial value without the flag days that match the start time
+ // would be missed.
+ if (this.has_by_data('BYMONTHDAY')) {
+ this._byDayAndMonthDay(true);
+ }
+
+ if (this.last.day > daysInMonth || this.last.day == 0) {
+ throw new Error("Malformed values in BYDAY part");
+ }
+
+ } else if (this.has_by_data("BYMONTHDAY")) {
+ if (this.last.day < 0) {
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ this.last.day = daysInMonth + this.last.day + 1;
+ }
+ }
+
+ },
+
+ /**
+ * Retrieve the next occurrence from the iterator.
+ * @return {ICAL.Time}
+ */
+ next: function icalrecur_iterator_next() {
+ var before = (this.last ? this.last.clone() : null);
+
+ if ((this.rule.count && this.occurrence_number >= this.rule.count) ||
+ (this.rule.until && this.last.compare(this.rule.until) > 0)) {
+
+ //XXX: right now this is just a flag and has no impact
+ // we can simplify the above case to check for completed later.
+ this.completed = true;
+
+ return null;
+ }
+
+ if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) {
+ // First of all, give the instance that was initialized
+ this.occurrence_number++;
+ return this.last;
+ }
+
+
+ var valid;
+ do {
+ valid = 1;
+
+ switch (this.rule.freq) {
+ case "SECONDLY":
+ this.next_second();
+ break;
+ case "MINUTELY":
+ this.next_minute();
+ break;
+ case "HOURLY":
+ this.next_hour();
+ break;
+ case "DAILY":
+ this.next_day();
+ break;
+ case "WEEKLY":
+ this.next_week();
+ break;
+ case "MONTHLY":
+ valid = this.next_month();
+ break;
+ case "YEARLY":
+ this.next_year();
+ break;
+
+ default:
+ return null;
+ }
+ } while (!this.check_contracting_rules() ||
+ this.last.compare(this.dtstart) < 0 ||
+ !valid);
+
+ // TODO is this valid?
+ if (this.last.compare(before) == 0) {
+ throw new Error("Same occurrence found twice, protecting " +
+ "you from death by recursion");
+ }
+
+ if (this.rule.until && this.last.compare(this.rule.until) > 0) {
+ this.completed = true;
+ return null;
+ } else {
+ this.occurrence_number++;
+ return this.last;
+ }
+ },
+
+ next_second: function next_second() {
+ return this.next_generic("BYSECOND", "SECONDLY", "second", "minute");
+ },
+
+ increment_second: function increment_second(inc) {
+ return this.increment_generic(inc, "second", 60, "minute");
+ },
+
+ next_minute: function next_minute() {
+ return this.next_generic("BYMINUTE", "MINUTELY",
+ "minute", "hour", "next_second");
+ },
+
+ increment_minute: function increment_minute(inc) {
+ return this.increment_generic(inc, "minute", 60, "hour");
+ },
+
+ next_hour: function next_hour() {
+ return this.next_generic("BYHOUR", "HOURLY", "hour",
+ "monthday", "next_minute");
+ },
+
+ increment_hour: function increment_hour(inc) {
+ this.increment_generic(inc, "hour", 24, "monthday");
+ },
+
+ next_day: function next_day() {
+ var has_by_day = ("BYDAY" in this.by_data);
+ var this_freq = (this.rule.freq == "DAILY");
+
+ if (this.next_hour() == 0) {
+ return 0;
+ }
+
+ if (this_freq) {
+ this.increment_monthday(this.rule.interval);
+ } else {
+ this.increment_monthday(1);
+ }
+
+ return 0;
+ },
+
+ next_week: function next_week() {
+ var end_of_data = 0;
+
+ if (this.next_weekday_by_week() == 0) {
+ return end_of_data;
+ }
+
+ if (this.has_by_data("BYWEEKNO")) {
+ var idx = ++this.by_indices.BYWEEKNO;
+
+ if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) {
+ this.by_indices.BYWEEKNO = 0;
+ end_of_data = 1;
+ }
+
+ // HACK should be first month of the year
+ this.last.month = 1;
+ this.last.day = 1;
+
+ var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO];
+
+ this.last.day += 7 * week_no;
+
+ if (end_of_data) {
+ this.increment_year(1);
+ }
+ } else {
+ // Jump to the next week
+ this.increment_monthday(7 * this.rule.interval);
+ }
+
+ return end_of_data;
+ },
+
+ /**
+ * Normalize each by day rule for a given year/month.
+ * Takes into account ordering and negative rules
+ *
+ * @private
+ * @param {Number} year Current year.
+ * @param {Number} month Current month.
+ * @param {Array} rules Array of rules.
+ *
+ * @return {Array} sorted and normalized rules.
+ * Negative rules will be expanded to their
+ * correct positive values for easier processing.
+ */
+ normalizeByMonthDayRules: function(year, month, rules) {
+ var daysInMonth = ICAL.Time.daysInMonth(month, year);
+
+ // XXX: This is probably bad for performance to allocate
+ // a new array for each month we scan, if possible
+ // we should try to optimize this...
+ var newRules = [];
+
+ var ruleIdx = 0;
+ var len = rules.length;
+ var rule;
+
+ for (; ruleIdx < len; ruleIdx++) {
+ rule = rules[ruleIdx];
+
+ // if this rule falls outside of given
+ // month discard it.
+ if (Math.abs(rule) > daysInMonth) {
+ continue;
+ }
+
+ // negative case
+ if (rule < 0) {
+ // we add (not subtract its a negative number)
+ // one from the rule because 1 === last day of month
+ rule = daysInMonth + (rule + 1);
+ } else if (rule === 0) {
+ // skip zero its invalid.
+ continue;
+ }
+
+ // only add unique items...
+ if (newRules.indexOf(rule) === -1) {
+ newRules.push(rule);
+ }
+
+ }
+
+ // unique and sort
+ return newRules.sort(function(a, b) { return a - b; });
+ },
+
+ /**
+ * NOTES:
+ * We are given a list of dates in the month (BYMONTHDAY) (23, etc..)
+ * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when
+ * both conditions match a given date (this.last.day) iteration stops.
+ *
+ * @private
+ * @param {Boolean=} isInit When given true will not increment the
+ * current day (this.last).
+ */
+ _byDayAndMonthDay: function(isInit) {
+ var byMonthDay; // setup in initMonth
+ var byDay = this.by_data.BYDAY;
+
+ var date;
+ var dateIdx = 0;
+ var dateLen; // setup in initMonth
+ var dayLen = byDay.length;
+
+ // we are not valid by default
+ var dataIsValid = 0;
+
+ var daysInMonth;
+ var self = this;
+ // we need a copy of this, because a DateTime gets normalized
+ // automatically if the day is out of range. At some points we
+ // set the last day to 0 to start counting.
+ var lastDay = this.last.day;
+
+ function initMonth() {
+ daysInMonth = ICAL.Time.daysInMonth(
+ self.last.month, self.last.year
+ );
+
+ byMonthDay = self.normalizeByMonthDayRules(
+ self.last.year,
+ self.last.month,
+ self.by_data.BYMONTHDAY
+ );
+
+ dateLen = byMonthDay.length;
+
+ // For the case of more than one occurrence in one month
+ // we have to be sure to start searching after the last
+ // found date or at the last BYMONTHDAY, unless we are
+ // initializing the iterator because in this case we have
+ // to consider the last found date too.
+ while (byMonthDay[dateIdx] <= lastDay &&
+ !(isInit && byMonthDay[dateIdx] == lastDay) &&
+ dateIdx < dateLen - 1) {
+ dateIdx++;
+ }
+ }
+
+ function nextMonth() {
+ // since the day is incremented at the start
+ // of the loop below, we need to start at 0
+ lastDay = 0;
+ self.increment_month();
+ dateIdx = 0;
+ initMonth();
+ }
+
+ initMonth();
+
+ // should come after initMonth
+ if (isInit) {
+ lastDay -= 1;
+ }
+
+ // Use a counter to avoid an infinite loop with malformed rules.
+ // Stop checking after 4 years so we consider also a leap year.
+ var monthsCounter = 48;
+
+ while (!dataIsValid && monthsCounter) {
+ monthsCounter--;
+ // increment the current date. This is really
+ // important otherwise we may fall into the infinite
+ // loop trap. The initial date takes care of the case
+ // where the current date is the date we are looking
+ // for.
+ date = lastDay + 1;
+
+ if (date > daysInMonth) {
+ nextMonth();
+ continue;
+ }
+
+ // find next date
+ var next = byMonthDay[dateIdx++];
+
+ // this logic is dependant on the BYMONTHDAYS
+ // being in order (which is done by #normalizeByMonthDayRules)
+ if (next >= date) {
+ // if the next month day is in the future jump to it.
+ lastDay = next;
+ } else {
+ // in this case the 'next' monthday has past
+ // we must move to the month.
+ nextMonth();
+ continue;
+ }
+
+ // Now we can loop through the day rules to see
+ // if one matches the current month date.
+ for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) {
+ var parts = this.ruleDayOfWeek(byDay[dayIdx]);
+ var pos = parts[0];
+ var dow = parts[1];
+
+ this.last.day = lastDay;
+ if (this.last.isNthWeekDay(dow, pos)) {
+ // when we find the valid one we can mark
+ // the conditions as met and break the loop.
+ // (Because we have this condition above
+ // it will also break the parent loop).
+ dataIsValid = 1;
+ break;
+ }
+ }
+
+ // Its completely possible that the combination
+ // cannot be matched in the current month.
+ // When we reach the end of possible combinations
+ // in the current month we iterate to the next one.
+ // since dateIdx is incremented right after getting
+ // "next", we don't need dateLen -1 here.
+ if (!dataIsValid && dateIdx === dateLen) {
+ nextMonth();
+ continue;
+ }
+ }
+
+ if (monthsCounter <= 0) {
+ // Checked 4 years without finding a Byday that matches
+ // a Bymonthday. Maybe the rule is not correct.
+ throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts");
+ }
+
+
+ return dataIsValid;
+ },
+
+ next_month: function next_month() {
+ var this_freq = (this.rule.freq == "MONTHLY");
+ var data_valid = 1;
+
+ if (this.next_hour() == 0) {
+ return data_valid;
+ }
+
+ if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) {
+ data_valid = this._byDayAndMonthDay();
+ } else if (this.has_by_data("BYDAY")) {
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ var setpos = 0;
+ var setpos_total = 0;
+
+ if (this.has_by_data("BYSETPOS")) {
+ var last_day = this.last.day;
+ for (var day = 1; day <= daysInMonth; day++) {
+ this.last.day = day;
+ if (this.is_day_in_byday(this.last)) {
+ setpos_total++;
+ if (day <= last_day) {
+ setpos++;
+ }
+ }
+ }
+ this.last.day = last_day;
+ }
+
+ data_valid = 0;
+ for (var day = this.last.day + 1; day <= daysInMonth; day++) {
+ this.last.day = day;
+
+ if (this.is_day_in_byday(this.last)) {
+ if (!this.has_by_data("BYSETPOS") ||
+ this.check_set_position(++setpos) ||
+ this.check_set_position(setpos - setpos_total - 1)) {
+
+ data_valid = 1;
+ break;
+ }
+ }
+ }
+
+ if (day > daysInMonth) {
+ this.last.day = 1;
+ this.increment_month();
+
+ if (this.is_day_in_byday(this.last)) {
+ if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) {
+ data_valid = 1;
+ }
+ } else {
+ data_valid = 0;
+ }
+ }
+ } else if (this.has_by_data("BYMONTHDAY")) {
+ this.by_indices.BYMONTHDAY++;
+
+ if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) {
+ this.by_indices.BYMONTHDAY = 0;
+ this.increment_month();
+ }
+
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY];
+
+ if (day < 0) {
+ day = daysInMonth + day + 1;
+ }
+
+ if (day > daysInMonth) {
+ this.last.day = 1;
+ data_valid = this.is_day_in_byday(this.last);
+ } else {
+ this.last.day = day;
+ }
+
+ } else {
+ this.increment_month();
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ if (this.by_data.BYMONTHDAY[0] > daysInMonth) {
+ data_valid = 0;
+ } else {
+ this.last.day = this.by_data.BYMONTHDAY[0];
+ }
+ }
+
+ return data_valid;
+ },
+
+ next_weekday_by_week: function next_weekday_by_week() {
+ var end_of_data = 0;
+
+ if (this.next_hour() == 0) {
+ return end_of_data;
+ }
+
+ if (!this.has_by_data("BYDAY")) {
+ return 1;
+ }
+
+ for (;;) {
+ var tt = new ICAL.Time();
+ this.by_indices.BYDAY++;
+
+ if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) {
+ this.by_indices.BYDAY = 0;
+ end_of_data = 1;
+ }
+
+ var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
+ var parts = this.ruleDayOfWeek(coded_day);
+ var dow = parts[1];
+
+ dow -= this.rule.wkst;
+
+ if (dow < 0) {
+ dow += 7;
+ }
+
+ tt.year = this.last.year;
+ tt.month = this.last.month;
+ tt.day = this.last.day;
+
+ var startOfWeek = tt.startDoyWeek(this.rule.wkst);
+
+ if (dow + startOfWeek < 1) {
+ // The selected date is in the previous year
+ if (!end_of_data) {
+ continue;
+ }
+ }
+
+ var next = ICAL.Time.fromDayOfYear(startOfWeek + dow,
+ this.last.year);
+
+ /**
+ * The normalization horrors below are due to
+ * the fact that when the year/month/day changes
+ * it can effect the other operations that come after.
+ */
+ this.last.year = next.year;
+ this.last.month = next.month;
+ this.last.day = next.day;
+
+ return end_of_data;
+ }
+ },
+
+ next_year: function next_year() {
+
+ if (this.next_hour() == 0) {
+ return 0;
+ }
+
+ if (++this.days_index == this.days.length) {
+ this.days_index = 0;
+ do {
+ this.increment_year(this.rule.interval);
+ this.expand_year_days(this.last.year);
+ } while (this.days.length == 0);
+ }
+
+ this._nextByYearDay();
+
+ return 1;
+ },
+
+ _nextByYearDay: function _nextByYearDay() {
+ var doy = this.days[this.days_index];
+ var year = this.last.year;
+ if (doy < 1) {
+ // Time.fromDayOfYear(doy, year) indexes relative to the
+ // start of the given year. That is different from the
+ // semantics of BYYEARDAY where negative indexes are an
+ // offset from the end of the given year.
+ doy += 1;
+ year += 1;
+ }
+ var next = ICAL.Time.fromDayOfYear(doy, year);
+ this.last.day = next.day;
+ this.last.month = next.month;
+ },
+
+ ruleDayOfWeek: function ruleDayOfWeek(dow) {
+ var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);
+ if (matches) {
+ var pos = parseInt(matches[1] || 0, 10);
+ dow = ICAL.Recur.icalDayToNumericDay(matches[2]);
+ return [pos, dow];
+ } else {
+ return [0, 0];
+ }
+ },
+
+ next_generic: function next_generic(aRuleType, aInterval, aDateAttr,
+ aFollowingAttr, aPreviousIncr) {
+ var has_by_rule = (aRuleType in this.by_data);
+ var this_freq = (this.rule.freq == aInterval);
+ var end_of_data = 0;
+
+ if (aPreviousIncr && this[aPreviousIncr]() == 0) {
+ return end_of_data;
+ }
+
+ if (has_by_rule) {
+ this.by_indices[aRuleType]++;
+ var idx = this.by_indices[aRuleType];
+ var dta = this.by_data[aRuleType];
+
+ if (this.by_indices[aRuleType] == dta.length) {
+ this.by_indices[aRuleType] = 0;
+ end_of_data = 1;
+ }
+ this.last[aDateAttr] = dta[this.by_indices[aRuleType]];
+ } else if (this_freq) {
+ this["increment_" + aDateAttr](this.rule.interval);
+ }
+
+ if (has_by_rule && end_of_data && this_freq) {
+ this["increment_" + aFollowingAttr](1);
+ }
+
+ return end_of_data;
+ },
+
+ increment_monthday: function increment_monthday(inc) {
+ for (var i = 0; i < inc; i++) {
+ var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
+ this.last.day++;
+
+ if (this.last.day > daysInMonth) {
+ this.last.day -= daysInMonth;
+ this.increment_month();
+ }
+ }
+ },
+
+ increment_month: function increment_month() {
+ this.last.day = 1;
+ if (this.has_by_data("BYMONTH")) {
+ this.by_indices.BYMONTH++;
+
+ if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) {
+ this.by_indices.BYMONTH = 0;
+ this.increment_year(1);
+ }
+
+ this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH];
+ } else {
+ if (this.rule.freq == "MONTHLY") {
+ this.last.month += this.rule.interval;
+ } else {
+ this.last.month++;
+ }
+
+ this.last.month--;
+ var years = ICAL.helpers.trunc(this.last.month / 12);
+ this.last.month %= 12;
+ this.last.month++;
+
+ if (years != 0) {
+ this.increment_year(years);
+ }
+ }
+ },
+
+ increment_year: function increment_year(inc) {
+ this.last.year += inc;
+ },
+
+ increment_generic: function increment_generic(inc, aDateAttr,
+ aFactor, aNextIncrement) {
+ this.last[aDateAttr] += inc;
+ var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor);
+ this.last[aDateAttr] %= aFactor;
+ if (nextunit != 0) {
+ this["increment_" + aNextIncrement](nextunit);
+ }
+ },
+
+ has_by_data: function has_by_data(aRuleType) {
+ return (aRuleType in this.rule.parts);
+ },
+
+ expand_year_days: function expand_year_days(aYear) {
+ var t = new ICAL.Time();
+ this.days = [];
+
+ // We need our own copy with a few keys set
+ var parts = {};
+ var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"];
+ for (var p in rules) {
+ /* istanbul ignore else */
+ if (rules.hasOwnProperty(p)) {
+ var part = rules[p];
+ if (part in this.rule.parts) {
+ parts[part] = this.rule.parts[part];
+ }
+ }
+ }
+
+ if ("BYMONTH" in parts && "BYWEEKNO" in parts) {
+ var valid = 1;
+ var validWeeks = {};
+ t.year = aYear;
+ t.isDate = true;
+
+ for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) {
+ var month = this.by_data.BYMONTH[monthIdx];
+ t.month = month;
+ t.day = 1;
+ var first_week = t.weekNumber(this.rule.wkst);
+ t.day = ICAL.Time.daysInMonth(month, aYear);
+ var last_week = t.weekNumber(this.rule.wkst);
+ for (monthIdx = first_week; monthIdx < last_week; monthIdx++) {
+ validWeeks[monthIdx] = 1;
+ }
+ }
+
+ for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) {
+ var weekno = this.by_data.BYWEEKNO[weekIdx];
+ if (weekno < 52) {
+ valid &= validWeeks[weekIdx];
+ } else {
+ valid = 0;
+ }
+ }
+
+ if (valid) {
+ delete parts.BYMONTH;
+ } else {
+ delete parts.BYWEEKNO;
+ }
+ }
+
+ var partCount = Object.keys(parts).length;
+
+ if (partCount == 0) {
+ var t1 = this.dtstart.clone();
+ t1.year = this.last.year;
+ this.days.push(t1.dayOfYear());
+ } else if (partCount == 1 && "BYMONTH" in parts) {
+ for (var monthkey in this.by_data.BYMONTH) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
+ continue;
+ }
+ var t2 = this.dtstart.clone();
+ t2.year = aYear;
+ t2.month = this.by_data.BYMONTH[monthkey];
+ t2.isDate = true;
+ this.days.push(t2.dayOfYear());
+ }
+ } else if (partCount == 1 && "BYMONTHDAY" in parts) {
+ for (var monthdaykey in this.by_data.BYMONTHDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
+ continue;
+ }
+ var t3 = this.dtstart.clone();
+ var day_ = this.by_data.BYMONTHDAY[monthdaykey];
+ if (day_ < 0) {
+ var daysInMonth = ICAL.Time.daysInMonth(t3.month, aYear);
+ day_ = day_ + daysInMonth + 1;
+ }
+ t3.day = day_;
+ t3.year = aYear;
+ t3.isDate = true;
+ this.days.push(t3.dayOfYear());
+ }
+ } else if (partCount == 2 &&
+ "BYMONTHDAY" in parts &&
+ "BYMONTH" in parts) {
+ for (var monthkey in this.by_data.BYMONTH) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
+ continue;
+ }
+ var month_ = this.by_data.BYMONTH[monthkey];
+ var daysInMonth = ICAL.Time.daysInMonth(month_, aYear);
+ for (var monthdaykey in this.by_data.BYMONTHDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
+ continue;
+ }
+ var day_ = this.by_data.BYMONTHDAY[monthdaykey];
+ if (day_ < 0) {
+ day_ = day_ + daysInMonth + 1;
+ }
+ t.day = day_;
+ t.month = month_;
+ t.year = aYear;
+ t.isDate = true;
+
+ this.days.push(t.dayOfYear());
+ }
+ }
+ } else if (partCount == 1 && "BYWEEKNO" in parts) {
+ // TODO unimplemented in libical
+ } else if (partCount == 2 &&
+ "BYWEEKNO" in parts &&
+ "BYMONTHDAY" in parts) {
+ // TODO unimplemented in libical
+ } else if (partCount == 1 && "BYDAY" in parts) {
+ this.days = this.days.concat(this.expand_by_day(aYear));
+ } else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) {
+ for (var monthkey in this.by_data.BYMONTH) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
+ continue;
+ }
+ var month = this.by_data.BYMONTH[monthkey];
+ var daysInMonth = ICAL.Time.daysInMonth(month, aYear);
+
+ t.year = aYear;
+ t.month = this.by_data.BYMONTH[monthkey];
+ t.day = 1;
+ t.isDate = true;
+
+ var first_dow = t.dayOfWeek();
+ var doy_offset = t.dayOfYear() - 1;
+
+ t.day = daysInMonth;
+ var last_dow = t.dayOfWeek();
+
+ if (this.has_by_data("BYSETPOS")) {
+ var set_pos_counter = 0;
+ var by_month_day = [];
+ for (var day = 1; day <= daysInMonth; day++) {
+ t.day = day;
+ if (this.is_day_in_byday(t)) {
+ by_month_day.push(day);
+ }
+ }
+
+ for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) {
+ if (this.check_set_position(spIndex + 1) ||
+ this.check_set_position(spIndex - by_month_day.length)) {
+ this.days.push(doy_offset + by_month_day[spIndex]);
+ }
+ }
+ } else {
+ for (var daycodedkey in this.by_data.BYDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYDAY.hasOwnProperty(daycodedkey)) {
+ continue;
+ }
+ var coded_day = this.by_data.BYDAY[daycodedkey];
+ var bydayParts = this.ruleDayOfWeek(coded_day);
+ var pos = bydayParts[0];
+ var dow = bydayParts[1];
+ var month_day;
+
+ var first_matching_day = ((dow + 7 - first_dow) % 7) + 1;
+ var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7);
+
+ if (pos == 0) {
+ for (var day = first_matching_day; day <= daysInMonth; day += 7) {
+ this.days.push(doy_offset + day);
+ }
+ } else if (pos > 0) {
+ month_day = first_matching_day + (pos - 1) * 7;
+
+ if (month_day <= daysInMonth) {
+ this.days.push(doy_offset + month_day);
+ }
+ } else {
+ month_day = last_matching_day + (pos + 1) * 7;
+
+ if (month_day > 0) {
+ this.days.push(doy_offset + month_day);
+ }
+ }
+ }
+ }
+ }
+ // Return dates in order of occurrence (1,2,3,...) instead
+ // of by groups of weekdays (1,8,15,...,2,9,16,...).
+ this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers.
+ } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) {
+ var expandedDays = this.expand_by_day(aYear);
+
+ for (var daykey in expandedDays) {
+ /* istanbul ignore if */
+ if (!expandedDays.hasOwnProperty(daykey)) {
+ continue;
+ }
+ var day = expandedDays[daykey];
+ var tt = ICAL.Time.fromDayOfYear(day, aYear);
+ if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
+ this.days.push(day);
+ }
+ }
+ } else if (partCount == 3 &&
+ "BYDAY" in parts &&
+ "BYMONTHDAY" in parts &&
+ "BYMONTH" in parts) {
+ var expandedDays = this.expand_by_day(aYear);
+
+ for (var daykey in expandedDays) {
+ /* istanbul ignore if */
+ if (!expandedDays.hasOwnProperty(daykey)) {
+ continue;
+ }
+ var day = expandedDays[daykey];
+ var tt = ICAL.Time.fromDayOfYear(day, aYear);
+
+ if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 &&
+ this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
+ this.days.push(day);
+ }
+ }
+ } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) {
+ var expandedDays = this.expand_by_day(aYear);
+
+ for (var daykey in expandedDays) {
+ /* istanbul ignore if */
+ if (!expandedDays.hasOwnProperty(daykey)) {
+ continue;
+ }
+ var day = expandedDays[daykey];
+ var tt = ICAL.Time.fromDayOfYear(day, aYear);
+ var weekno = tt.weekNumber(this.rule.wkst);
+
+ if (this.by_data.BYWEEKNO.indexOf(weekno)) {
+ this.days.push(day);
+ }
+ }
+ } else if (partCount == 3 &&
+ "BYDAY" in parts &&
+ "BYWEEKNO" in parts &&
+ "BYMONTHDAY" in parts) {
+ // TODO unimplemted in libical
+ } else if (partCount == 1 && "BYYEARDAY" in parts) {
+ this.days = this.days.concat(this.by_data.BYYEARDAY);
+ } else {
+ this.days = [];
+ }
+ return 0;
+ },
+
+ expand_by_day: function expand_by_day(aYear) {
+
+ var days_list = [];
+ var tmp = this.last.clone();
+
+ tmp.year = aYear;
+ tmp.month = 1;
+ tmp.day = 1;
+ tmp.isDate = true;
+
+ var start_dow = tmp.dayOfWeek();
+
+ tmp.month = 12;
+ tmp.day = 31;
+ tmp.isDate = true;
+
+ var end_dow = tmp.dayOfWeek();
+ var end_year_day = tmp.dayOfYear();
+
+ for (var daykey in this.by_data.BYDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
+ continue;
+ }
+ var day = this.by_data.BYDAY[daykey];
+ var parts = this.ruleDayOfWeek(day);
+ var pos = parts[0];
+ var dow = parts[1];
+
+ if (pos == 0) {
+ var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1;
+
+ for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) {
+ days_list.push(doy);
+ }
+
+ } else if (pos > 0) {
+ var first;
+ if (dow >= start_dow) {
+ first = dow - start_dow + 1;
+ } else {
+ first = dow - start_dow + 8;
+ }
+
+ days_list.push(first + (pos - 1) * 7);
+ } else {
+ var last;
+ pos = -pos;
+
+ if (dow <= end_dow) {
+ last = end_year_day - end_dow + dow;
+ } else {
+ last = end_year_day - end_dow + dow - 7;
+ }
+
+ days_list.push(last - (pos - 1) * 7);
+ }
+ }
+ return days_list;
+ },
+
+ is_day_in_byday: function is_day_in_byday(tt) {
+ for (var daykey in this.by_data.BYDAY) {
+ /* istanbul ignore if */
+ if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
+ continue;
+ }
+ var day = this.by_data.BYDAY[daykey];
+ var parts = this.ruleDayOfWeek(day);
+ var pos = parts[0];
+ var dow = parts[1];
+ var this_dow = tt.dayOfWeek();
+
+ if ((pos == 0 && dow == this_dow) ||
+ (tt.nthWeekDay(dow, pos) == tt.day)) {
+ return 1;
+ }
+ }
+
+ return 0;
+ },
+
+ /**
+ * Checks if given value is in BYSETPOS.
+ *
+ * @private
+ * @param {Numeric} aPos position to check for.
+ * @return {Boolean} false unless BYSETPOS rules exist
+ * and the given value is present in rules.
+ */
+ check_set_position: function check_set_position(aPos) {
+ if (this.has_by_data('BYSETPOS')) {
+ var idx = this.by_data.BYSETPOS.indexOf(aPos);
+ // negative numbers are not false-y
+ return idx !== -1;
+ }
+ return false;
+ },
+
+ sort_byday_rules: function icalrecur_sort_byday_rules(aRules, aWeekStart) {
+ for (var i = 0; i < aRules.length; i++) {
+ for (var j = 0; j < i; j++) {
+ var one = this.ruleDayOfWeek(aRules[j])[1];
+ var two = this.ruleDayOfWeek(aRules[i])[1];
+ one -= aWeekStart;
+ two -= aWeekStart;
+ if (one < 0) one += 7;
+ if (two < 0) two += 7;
+
+ if (one > two) {
+ var tmp = aRules[i];
+ aRules[i] = aRules[j];
+ aRules[j] = tmp;
+ }
+ }
+ }
+ },
+
+ check_contract_restriction: function check_contract_restriction(aRuleType, v) {
+ var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
+ var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
+ var pass = false;
+
+ if (aRuleType in this.by_data &&
+ ruleMapValue == icalrecur_iterator.CONTRACT) {
+
+ var ruleType = this.by_data[aRuleType];
+
+ for (var bydatakey in ruleType) {
+ /* istanbul ignore else */
+ if (ruleType.hasOwnProperty(bydatakey)) {
+ if (ruleType[bydatakey] == v) {
+ pass = true;
+ break;
+ }
+ }
+ }
+ } else {
+ // Not a contracting byrule or has no data, test passes
+ pass = true;
+ }
+ return pass;
+ },
+
+ check_contracting_rules: function check_contracting_rules() {
+ var dow = this.last.dayOfWeek();
+ var weekNo = this.last.weekNumber(this.rule.wkst);
+ var doy = this.last.dayOfYear();
+
+ return (this.check_contract_restriction("BYSECOND", this.last.second) &&
+ this.check_contract_restriction("BYMINUTE", this.last.minute) &&
+ this.check_contract_restriction("BYHOUR", this.last.hour) &&
+ this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) &&
+ this.check_contract_restriction("BYWEEKNO", weekNo) &&
+ this.check_contract_restriction("BYMONTHDAY", this.last.day) &&
+ this.check_contract_restriction("BYMONTH", this.last.month) &&
+ this.check_contract_restriction("BYYEARDAY", doy));
+ },
+
+ setup_defaults: function setup_defaults(aRuleType, req, deftime) {
+ var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
+ var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
+
+ if (ruleMapValue != icalrecur_iterator.CONTRACT) {
+ if (!(aRuleType in this.by_data)) {
+ this.by_data[aRuleType] = [deftime];
+ }
+ if (this.rule.freq != req) {
+ return this.by_data[aRuleType][0];
+ }
+ }
+ return deftime;
+ },
+
+ /**
+ * Convert iterator into a serialize-able object. Will preserve current
+ * iteration sequence to ensure the seamless continuation of the recurrence
+ * rule.
+ * @return {Object}
+ */
+ toJSON: function() {
+ var result = Object.create(null);
+
+ result.initialized = this.initialized;
+ result.rule = this.rule.toJSON();
+ result.dtstart = this.dtstart.toJSON();
+ result.by_data = this.by_data;
+ result.days = this.days;
+ result.last = this.last.toJSON();
+ result.by_indices = this.by_indices;
+ result.occurrence_number = this.occurrence_number;
+
+ return result;
+ }
+ };
+
+ icalrecur_iterator._indexMap = {
+ "BYSECOND": 0,
+ "BYMINUTE": 1,
+ "BYHOUR": 2,
+ "BYDAY": 3,
+ "BYMONTHDAY": 4,
+ "BYYEARDAY": 5,
+ "BYWEEKNO": 6,
+ "BYMONTH": 7,
+ "BYSETPOS": 8
+ };
+
+ icalrecur_iterator._expandMap = {
+ "SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1],
+ "MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1],
+ "HOURLY": [2, 2, 1, 1, 1, 1, 1, 1],
+ "DAILY": [2, 2, 2, 1, 1, 1, 1, 1],
+ "WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1],
+ "MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1],
+ "YEARLY": [2, 2, 2, 2, 2, 2, 2, 2]
+ };
+ icalrecur_iterator.UNKNOWN = 0;
+ icalrecur_iterator.CONTRACT = 1;
+ icalrecur_iterator.EXPAND = 2;
+ icalrecur_iterator.ILLEGAL = 3;
+
+ return icalrecur_iterator;
+
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.RecurExpansion = (function() {
+ function formatTime(item) {
+ return ICAL.helpers.formatClassType(item, ICAL.Time);
+ }
+
+ function compareTime(a, b) {
+ return a.compare(b);
+ }
+
+ function isRecurringComponent(comp) {
+ return comp.hasProperty('rdate') ||
+ comp.hasProperty('rrule') ||
+ comp.hasProperty('recurrence-id');
+ }
+
+ /**
+ * @classdesc
+ * Primary class for expanding recurring rules. Can take multiple rrules,
+ * rdates, exdate(s) and iterate (in order) over each next occurrence.
+ *
+ * Once initialized this class can also be serialized saved and continue
+ * iteration from the last point.
+ *
+ * NOTE: it is intended that this class is to be used
+ * with ICAL.Event which handles recurrence exceptions.
+ *
+ * @example
+ * // assuming event is a parsed ical component
+ * var event;
+ *
+ * var expand = new ICAL.RecurExpansion({
+ * component: event,
+ * dtstart: event.getFirstPropertyValue('dtstart')
+ * });
+ *
+ * // remember there are infinite rules
+ * // so its a good idea to limit the scope
+ * // of the iterations then resume later on.
+ *
+ * // next is always an ICAL.Time or null
+ * var next;
+ *
+ * while (someCondition && (next = expand.next())) {
+ * // do something with next
+ * }
+ *
+ * // save instance for later
+ * var json = JSON.stringify(expand);
+ *
+ * //...
+ *
+ * // NOTE: if the component's properties have
+ * // changed you will need to rebuild the
+ * // class and start over. This only works
+ * // when the component's recurrence info is the same.
+ * var expand = new ICAL.RecurExpansion(JSON.parse(json));
+ *
+ * @description
+ * The options object can be filled with the specified initial values. It can
+ * also contain additional members, as a result of serializing a previous
+ * expansion state, as shown in the example.
+ *
+ * @class
+ * @alias ICAL.RecurExpansion
+ * @param {Object} options
+ * Recurrence expansion options
+ * @param {ICAL.Time} options.dtstart
+ * Start time of the event
+ * @param {ICAL.Component=} options.component
+ * Component for expansion, required if not resuming.
+ */
+ function RecurExpansion(options) {
+ this.ruleDates = [];
+ this.exDates = [];
+ this.fromData(options);
+ }
+
+ RecurExpansion.prototype = {
+ /**
+ * True when iteration is fully completed.
+ * @type {Boolean}
+ */
+ complete: false,
+
+ /**
+ * Array of rrule iterators.
+ *
+ * @type {ICAL.RecurIterator[]}
+ * @private
+ */
+ ruleIterators: null,
+
+ /**
+ * Array of rdate instances.
+ *
+ * @type {ICAL.Time[]}
+ * @private
+ */
+ ruleDates: null,
+
+ /**
+ * Array of exdate instances.
+ *
+ * @type {ICAL.Time[]}
+ * @private
+ */
+ exDates: null,
+
+ /**
+ * Current position in ruleDates array.
+ * @type {Number}
+ * @private
+ */
+ ruleDateInc: 0,
+
+ /**
+ * Current position in exDates array
+ * @type {Number}
+ * @private
+ */
+ exDateInc: 0,
+
+ /**
+ * Current negative date.
+ *
+ * @type {ICAL.Time}
+ * @private
+ */
+ exDate: null,
+
+ /**
+ * Current additional date.
+ *
+ * @type {ICAL.Time}
+ * @private
+ */
+ ruleDate: null,
+
+ /**
+ * Start date of recurring rules.
+ *
+ * @type {ICAL.Time}
+ */
+ dtstart: null,
+
+ /**
+ * Last expanded time
+ *
+ * @type {ICAL.Time}
+ */
+ last: null,
+
+ /**
+ * Initialize the recurrence expansion from the data object. The options
+ * object may also contain additional members, see the
+ * {@link ICAL.RecurExpansion constructor} for more details.
+ *
+ * @param {Object} options
+ * Recurrence expansion options
+ * @param {ICAL.Time} options.dtstart
+ * Start time of the event
+ * @param {ICAL.Component=} options.component
+ * Component for expansion, required if not resuming.
+ */
+ fromData: function(options) {
+ var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
+
+ if (!start) {
+ throw new Error('.dtstart (ICAL.Time) must be given');
+ } else {
+ this.dtstart = start;
+ }
+
+ if (options.component) {
+ this._init(options.component);
+ } else {
+ this.last = formatTime(options.last) || start.clone();
+
+ if (!options.ruleIterators) {
+ throw new Error('.ruleIterators or .component must be given');
+ }
+
+ this.ruleIterators = options.ruleIterators.map(function(item) {
+ return ICAL.helpers.formatClassType(item, ICAL.RecurIterator);
+ });
+
+ this.ruleDateInc = options.ruleDateInc;
+ this.exDateInc = options.exDateInc;
+
+ if (options.ruleDates) {
+ this.ruleDates = options.ruleDates.map(formatTime);
+ this.ruleDate = this.ruleDates[this.ruleDateInc];
+ }
+
+ if (options.exDates) {
+ this.exDates = options.exDates.map(formatTime);
+ this.exDate = this.exDates[this.exDateInc];
+ }
+
+ if (typeof(options.complete) !== 'undefined') {
+ this.complete = options.complete;
+ }
+ }
+ },
+
+ /**
+ * Retrieve the next occurrence in the series.
+ * @return {ICAL.Time}
+ */
+ next: function() {
+ var iter;
+ var ruleOfDay;
+ var next;
+ var compare;
+
+ var maxTries = 500;
+ var currentTry = 0;
+
+ while (true) {
+ if (currentTry++ > maxTries) {
+ throw new Error(
+ 'max tries have occured, rule may be impossible to forfill.'
+ );
+ }
+
+ next = this.ruleDate;
+ iter = this._nextRecurrenceIter(this.last);
+
+ // no more matches
+ // because we increment the rule day or rule
+ // _after_ we choose a value this should be
+ // the only spot where we need to worry about the
+ // end of events.
+ if (!next && !iter) {
+ // there are no more iterators or rdates
+ this.complete = true;
+ break;
+ }
+
+ // no next rule day or recurrence rule is first.
+ if (!next || (iter && next.compare(iter.last) > 0)) {
+ // must be cloned, recur will reuse the time element.
+ next = iter.last.clone();
+ // move to next so we can continue
+ iter.next();
+ }
+
+ // if the ruleDate is still next increment it.
+ if (this.ruleDate === next) {
+ this._nextRuleDay();
+ }
+
+ this.last = next;
+
+ // check the negative rules
+ if (this.exDate) {
+ compare = this.exDate.compare(this.last);
+
+ if (compare < 0) {
+ this._nextExDay();
+ }
+
+ // if the current rule is excluded skip it.
+ if (compare === 0) {
+ this._nextExDay();
+ continue;
+ }
+ }
+
+ //XXX: The spec states that after we resolve the final
+ // list of dates we execute exdate this seems somewhat counter
+ // intuitive to what I have seen most servers do so for now
+ // I exclude based on the original date not the one that may
+ // have been modified by the exception.
+ return this.last;
+ }
+ },
+
+ /**
+ * Converts object into a serialize-able format. This format can be passed
+ * back into the expansion to resume iteration.
+ * @return {Object}
+ */
+ toJSON: function() {
+ function toJSON(item) {
+ return item.toJSON();
+ }
+
+ var result = Object.create(null);
+ result.ruleIterators = this.ruleIterators.map(toJSON);
+
+ if (this.ruleDates) {
+ result.ruleDates = this.ruleDates.map(toJSON);
+ }
+
+ if (this.exDates) {
+ result.exDates = this.exDates.map(toJSON);
+ }
+
+ result.ruleDateInc = this.ruleDateInc;
+ result.exDateInc = this.exDateInc;
+ result.last = this.last.toJSON();
+ result.dtstart = this.dtstart.toJSON();
+ result.complete = this.complete;
+
+ return result;
+ },
+
+ /**
+ * Extract all dates from the properties in the given component. The
+ * properties will be filtered by the property name.
+ *
+ * @private
+ * @param {ICAL.Component} component The component to search in
+ * @param {String} propertyName The property name to search for
+ * @return {ICAL.Time[]} The extracted dates.
+ */
+ _extractDates: function(component, propertyName) {
+ function handleProp(prop) {
+ idx = ICAL.helpers.binsearchInsert(
+ result,
+ prop,
+ compareTime
+ );
+
+ // ordered insert
+ result.splice(idx, 0, prop);
+ }
+
+ var result = [];
+ var props = component.getAllProperties(propertyName);
+ var len = props.length;
+ var i = 0;
+ var prop;
+
+ var idx;
+
+ for (; i < len; i++) {
+ props[i].getValues().forEach(handleProp);
+ }
+
+ return result;
+ },
+
+ /**
+ * Initialize the recurrence expansion.
+ *
+ * @private
+ * @param {ICAL.Component} component The component to initialize from.
+ */
+ _init: function(component) {
+ this.ruleIterators = [];
+
+ this.last = this.dtstart.clone();
+
+ // to provide api consistency non-recurring
+ // events can also use the iterator though it will
+ // only return a single time.
+ if (!isRecurringComponent(component)) {
+ this.ruleDate = this.last.clone();
+ this.complete = true;
+ return;
+ }
+
+ if (component.hasProperty('rdate')) {
+ this.ruleDates = this._extractDates(component, 'rdate');
+
+ // special hack for cases where first rdate is prior
+ // to the start date. We only check for the first rdate.
+ // This is mostly for google's crazy recurring date logic
+ // (contacts birthdays).
+ if ((this.ruleDates[0]) &&
+ (this.ruleDates[0].compare(this.dtstart) < 0)) {
+
+ this.ruleDateInc = 0;
+ this.last = this.ruleDates[0].clone();
+ } else {
+ this.ruleDateInc = ICAL.helpers.binsearchInsert(
+ this.ruleDates,
+ this.last,
+ compareTime
+ );
+ }
+
+ this.ruleDate = this.ruleDates[this.ruleDateInc];
+ }
+
+ if (component.hasProperty('rrule')) {
+ var rules = component.getAllProperties('rrule');
+ var i = 0;
+ var len = rules.length;
+
+ var rule;
+ var iter;
+
+ for (; i < len; i++) {
+ rule = rules[i].getFirstValue();
+ iter = rule.iterator(this.dtstart);
+ this.ruleIterators.push(iter);
+
+ // increment to the next occurrence so future
+ // calls to next return times beyond the initial iteration.
+ // XXX: I find this suspicious might be a bug?
+ iter.next();
+ }
+ }
+
+ if (component.hasProperty('exdate')) {
+ this.exDates = this._extractDates(component, 'exdate');
+ // if we have a .last day we increment the index to beyond it.
+ this.exDateInc = ICAL.helpers.binsearchInsert(
+ this.exDates,
+ this.last,
+ compareTime
+ );
+
+ this.exDate = this.exDates[this.exDateInc];
+ }
+ },
+
+ /**
+ * Advance to the next exdate
+ * @private
+ */
+ _nextExDay: function() {
+ this.exDate = this.exDates[++this.exDateInc];
+ },
+
+ /**
+ * Advance to the next rule date
+ * @private
+ */
+ _nextRuleDay: function() {
+ this.ruleDate = this.ruleDates[++this.ruleDateInc];
+ },
+
+ /**
+ * Find and return the recurrence rule with the most recent event and
+ * return it.
+ *
+ * @private
+ * @return {?ICAL.RecurIterator} Found iterator.
+ */
+ _nextRecurrenceIter: function() {
+ var iters = this.ruleIterators;
+
+ if (iters.length === 0) {
+ return null;
+ }
+
+ var len = iters.length;
+ var iter;
+ var iterTime;
+ var iterIdx = 0;
+ var chosenIter;
+
+ // loop through each iterator
+ for (; iterIdx < len; iterIdx++) {
+ iter = iters[iterIdx];
+ iterTime = iter.last;
+
+ // if iteration is complete
+ // then we must exclude it from
+ // the search and remove it.
+ if (iter.completed) {
+ len--;
+ if (iterIdx !== 0) {
+ iterIdx--;
+ }
+ iters.splice(iterIdx, 1);
+ continue;
+ }
+
+ // find the most recent possible choice
+ if (!chosenIter || chosenIter.last.compare(iterTime) > 0) {
+ // that iterator is saved
+ chosenIter = iter;
+ }
+ }
+
+ // the chosen iterator is returned but not mutated
+ // this iterator contains the most recent event.
+ return chosenIter;
+ }
+ };
+
+ return RecurExpansion;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.Event = (function() {
+
+ /**
+ * @classdesc
+ * ICAL.js is organized into multiple layers. The bottom layer is a raw jCal
+ * object, followed by the component/property layer. The highest level is the
+ * event representation, which this class is part of. See the
+ * {@tutorial layers} guide for more details.
+ *
+ * @class
+ * @alias ICAL.Event
+ * @param {ICAL.Component=} component The ICAL.Component to base this event on
+ * @param {Object} options Options for this event
+ * @param {Boolean} options.strictExceptions
+ * When true, will verify exceptions are related by their UUID
+ * @param {Array<ICAL.Component|ICAL.Event>} options.exceptions
+ * Exceptions to this event, either as components or events
+ */
+ function Event(component, options) {
+ if (!(component instanceof ICAL.Component)) {
+ options = component;
+ component = null;
+ }
+
+ if (component) {
+ this.component = component;
+ } else {
+ this.component = new ICAL.Component('vevent');
+ }
+
+ this._rangeExceptionCache = Object.create(null);
+ this.exceptions = Object.create(null);
+ this.rangeExceptions = [];
+
+ if (options && options.strictExceptions) {
+ this.strictExceptions = options.strictExceptions;
+ }
+
+ if (options && options.exceptions) {
+ options.exceptions.forEach(this.relateException, this);
+ }
+ }
+
+ Event.prototype = {
+
+ THISANDFUTURE: 'THISANDFUTURE',
+
+ /**
+ * List of related event exceptions.
+ *
+ * @type {ICAL.Event[]}
+ */
+ exceptions: null,
+
+ /**
+ * When true, will verify exceptions are related by their UUID.
+ *
+ * @type {Boolean}
+ */
+ strictExceptions: false,
+
+ /**
+ * Relates a given event exception to this object. If the given component
+ * does not share the UID of this event it cannot be related and will throw
+ * an exception.
+ *
+ * If this component is an exception it cannot have other exceptions
+ * related to it.
+ *
+ * @param {ICAL.Component|ICAL.Event} obj Component or event
+ */
+ relateException: function(obj) {
+ if (this.isRecurrenceException()) {
+ throw new Error('cannot relate exception to exceptions');
+ }
+
+ if (obj instanceof ICAL.Component) {
+ obj = new ICAL.Event(obj);
+ }
+
+ if (this.strictExceptions && obj.uid !== this.uid) {
+ throw new Error('attempted to relate unrelated exception');
+ }
+
+ var id = obj.recurrenceId.toString();
+
+ // we don't sort or manage exceptions directly
+ // here the recurrence expander handles that.
+ this.exceptions[id] = obj;
+
+ // index RANGE=THISANDFUTURE exceptions so we can
+ // look them up later in getOccurrenceDetails.
+ if (obj.modifiesFuture()) {
+ var item = [
+ obj.recurrenceId.toUnixTime(), id
+ ];
+
+ // we keep them sorted so we can find the nearest
+ // value later on...
+ var idx = ICAL.helpers.binsearchInsert(
+ this.rangeExceptions,
+ item,
+ compareRangeException
+ );
+
+ this.rangeExceptions.splice(idx, 0, item);
+ }
+ },
+
+ /**
+ * Checks if this record is an exception and has the RANGE=THISANDFUTURE
+ * value.
+ *
+ * @return {Boolean} True, when exception is within range
+ */
+ modifiesFuture: function() {
+ var range = this.component.getFirstPropertyValue('range');
+ return range === this.THISANDFUTURE;
+ },
+
+ /**
+ * Finds the range exception nearest to the given date.
+ *
+ * @param {ICAL.Time} time usually an occurrence time of an event
+ * @return {?ICAL.Event} the related event/exception or null
+ */
+ findRangeException: function(time) {
+ if (!this.rangeExceptions.length) {
+ return null;
+ }
+
+ var utc = time.toUnixTime();
+ var idx = ICAL.helpers.binsearchInsert(
+ this.rangeExceptions,
+ [utc],
+ compareRangeException
+ );
+
+ idx -= 1;
+
+ // occurs before
+ if (idx < 0) {
+ return null;
+ }
+
+ var rangeItem = this.rangeExceptions[idx];
+
+ /* istanbul ignore next: sanity check only */
+ if (utc < rangeItem[0]) {
+ return null;
+ }
+
+ return rangeItem[1];
+ },
+
+ /**
+ * This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails}
+ *
+ * @typedef {Object} occurrenceDetails
+ * @memberof ICAL.Event
+ * @property {ICAL.Time} recurrenceId The passed in recurrence id
+ * @property {ICAL.Event} item The occurrence
+ * @property {ICAL.Time} startDate The start of the occurrence
+ * @property {ICAL.Time} endDate The end of the occurrence
+ */
+
+ /**
+ * Returns the occurrence details based on its start time. If the
+ * occurrence has an exception will return the details for that exception.
+ *
+ * NOTE: this method is intend to be used in conjunction
+ * with the {@link ICAL.Event#iterator iterator} method.
+ *
+ * @param {ICAL.Time} occurrence time occurrence
+ * @return {ICAL.Event.occurrenceDetails} Information about the occurrence
+ */
+ getOccurrenceDetails: function(occurrence) {
+ var id = occurrence.toString();
+ var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString();
+ var item;
+ var result = {
+ //XXX: Clone?
+ recurrenceId: occurrence
+ };
+
+ if (id in this.exceptions) {
+ item = result.item = this.exceptions[id];
+ result.startDate = item.startDate;
+ result.endDate = item.endDate;
+ result.item = item;
+ } else if (utcId in this.exceptions) {
+ item = this.exceptions[utcId];
+ result.startDate = item.startDate;
+ result.endDate = item.endDate;
+ result.item = item;
+ } else {
+ // range exceptions (RANGE=THISANDFUTURE) have a
+ // lower priority then direct exceptions but
+ // must be accounted for first. Their item is
+ // always the first exception with the range prop.
+ var rangeExceptionId = this.findRangeException(
+ occurrence
+ );
+ var end;
+
+ if (rangeExceptionId) {
+ var exception = this.exceptions[rangeExceptionId];
+
+ // range exception must modify standard time
+ // by the difference (if any) in start/end times.
+ result.item = exception;
+
+ var startDiff = this._rangeExceptionCache[rangeExceptionId];
+
+ if (!startDiff) {
+ var original = exception.recurrenceId.clone();
+ var newStart = exception.startDate.clone();
+
+ // zones must be same otherwise subtract may be incorrect.
+ original.zone = newStart.zone;
+ startDiff = newStart.subtractDate(original);
+
+ this._rangeExceptionCache[rangeExceptionId] = startDiff;
+ }
+
+ var start = occurrence.clone();
+ start.zone = exception.startDate.zone;
+ start.addDuration(startDiff);
+
+ end = start.clone();
+ end.addDuration(exception.duration);
+
+ result.startDate = start;
+ result.endDate = end;
+ } else {
+ // no range exception standard expansion
+ end = occurrence.clone();
+ end.addDuration(this.duration);
+
+ result.endDate = end;
+ result.startDate = occurrence;
+ result.item = this;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Builds a recur expansion instance for a specific point in time (defaults
+ * to startDate).
+ *
+ * @param {ICAL.Time} startTime Starting point for expansion
+ * @return {ICAL.RecurExpansion} Expansion object
+ */
+ iterator: function(startTime) {
+ return new ICAL.RecurExpansion({
+ component: this.component,
+ dtstart: startTime || this.startDate
+ });
+ },
+
+ /**
+ * Checks if the event is recurring
+ *
+ * @return {Boolean} True, if event is recurring
+ */
+ isRecurring: function() {
+ var comp = this.component;
+ return comp.hasProperty('rrule') || comp.hasProperty('rdate');
+ },
+
+ /**
+ * Checks if the event describes a recurrence exception. See
+ * {@tutorial terminology} for details.
+ *
+ * @return {Boolean} True, if the even describes a recurrence exception
+ */
+ isRecurrenceException: function() {
+ return this.component.hasProperty('recurrence-id');
+ },
+
+ /**
+ * Returns the types of recurrences this event may have.
+ *
+ * Returned as an object with the following possible keys:
+ *
+ * - YEARLY
+ * - MONTHLY
+ * - WEEKLY
+ * - DAILY
+ * - MINUTELY
+ * - SECONDLY
+ *
+ * @return {Object.<ICAL.Recur.frequencyValues, Boolean>}
+ * Object of recurrence flags
+ */
+ getRecurrenceTypes: function() {
+ var rules = this.component.getAllProperties('rrule');
+ var i = 0;
+ var len = rules.length;
+ var result = Object.create(null);
+
+ for (; i < len; i++) {
+ var value = rules[i].getFirstValue();
+ result[value.freq] = true;
+ }
+
+ return result;
+ },
+
+ /**
+ * The uid of this event
+ * @type {String}
+ */
+ get uid() {
+ return this._firstProp('uid');
+ },
+
+ set uid(value) {
+ this._setProp('uid', value);
+ },
+
+ /**
+ * The start date
+ * @type {ICAL.Time}
+ */
+ get startDate() {
+ return this._firstProp('dtstart');
+ },
+
+ set startDate(value) {
+ this._setTime('dtstart', value);
+ },
+
+ /**
+ * The end date. This can be the result directly from the property, or the
+ * end date calculated from start date and duration.
+ * @type {ICAL.Time}
+ */
+ get endDate() {
+ var endDate = this._firstProp('dtend');
+ if (!endDate) {
+ var duration = this._firstProp('duration');
+ endDate = this.startDate.clone();
+ if (duration) {
+ endDate.addDuration(duration);
+ } else if (endDate.isDate) {
+ endDate.day += 1;
+ }
+ }
+ return endDate;
+ },
+
+ set endDate(value) {
+ this._setTime('dtend', value);
+ },
+
+ /**
+ * The duration. This can be the result directly from the property, or the
+ * duration calculated from start date and end date.
+ * @type {ICAL.Duration}
+ * @readonly
+ */
+ get duration() {
+ var duration = this._firstProp('duration');
+ if (!duration) {
+ return this.endDate.subtractDate(this.startDate);
+ }
+ return duration;
+ },
+
+ /**
+ * The location of the event.
+ * @type {String}
+ */
+ get location() {
+ return this._firstProp('location');
+ },
+
+ set location(value) {
+ return this._setProp('location', value);
+ },
+
+ /**
+ * The attendees in the event
+ * @type {ICAL.Property[]}
+ * @readonly
+ */
+ get attendees() {
+ //XXX: This is way lame we should have a better
+ // data structure for this later.
+ return this.component.getAllProperties('attendee');
+ },
+
+
+ /**
+ * The event summary
+ * @type {String}
+ */
+ get summary() {
+ return this._firstProp('summary');
+ },
+
+ set summary(value) {
+ this._setProp('summary', value);
+ },
+
+ /**
+ * The event description.
+ * @type {String}
+ */
+ get description() {
+ return this._firstProp('description');
+ },
+
+ set description(value) {
+ this._setProp('description', value);
+ },
+
+ /**
+ * The organizer value as an uri. In most cases this is a mailto: uri, but
+ * it can also be something else, like urn:uuid:...
+ * @type {String}
+ */
+ get organizer() {
+ return this._firstProp('organizer');
+ },
+
+ set organizer(value) {
+ this._setProp('organizer', value);
+ },
+
+ /**
+ * The sequence value for this event. Used for scheduling
+ * see {@tutorial terminology}.
+ * @type {Number}
+ */
+ get sequence() {
+ return this._firstProp('sequence');
+ },
+
+ set sequence(value) {
+ this._setProp('sequence', value);
+ },
+
+ /**
+ * The recurrence id for this event. See {@tutorial terminology} for details.
+ * @type {ICAL.Time}
+ */
+ get recurrenceId() {
+ return this._firstProp('recurrence-id');
+ },
+
+ set recurrenceId(value) {
+ this._setProp('recurrence-id', value);
+ },
+
+ /**
+ * Set/update a time property's value.
+ * This will also update the TZID of the property.
+ *
+ * TODO: this method handles the case where we are switching
+ * from a known timezone to an implied timezone (one without TZID).
+ * This does _not_ handle the case of moving between a known
+ * (by TimezoneService) timezone to an unknown timezone...
+ *
+ * We will not add/remove/update the VTIMEZONE subcomponents
+ * leading to invalid ICAL data...
+ * @private
+ * @param {String} propName The property name
+ * @param {ICAL.Time} time The time to set
+ */
+ _setTime: function(propName, time) {
+ var prop = this.component.getFirstProperty(propName);
+
+ if (!prop) {
+ prop = new ICAL.Property(propName);
+ this.component.addProperty(prop);
+ }
+
+ // utc and local don't get a tzid
+ if (
+ time.zone === ICAL.Timezone.localTimezone ||
+ time.zone === ICAL.Timezone.utcTimezone
+ ) {
+ // remove the tzid
+ prop.removeParameter('tzid');
+ } else {
+ prop.setParameter('tzid', time.zone.tzid);
+ }
+
+ prop.setValue(time);
+ },
+
+ _setProp: function(name, value) {
+ this.component.updatePropertyWithValue(name, value);
+ },
+
+ _firstProp: function(name) {
+ return this.component.getFirstPropertyValue(name);
+ },
+
+ /**
+ * The string representation of this event.
+ * @return {String}
+ */
+ toString: function() {
+ return this.component.toString();
+ }
+
+ };
+
+ function compareRangeException(a, b) {
+ if (a[0] > b[0]) return 1;
+ if (b[0] > a[0]) return -1;
+ return 0;
+ }
+
+ return Event;
+}());
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
+
+
+/**
+ * This symbol is further described later on
+ * @ignore
+ */
+ICAL.ComponentParser = (function() {
+ /**
+ * @classdesc
+ * The ComponentParser is used to process a String or jCal Object,
+ * firing callbacks for various found components, as well as completion.
+ *
+ * @example
+ * var options = {
+ * // when false no events will be emitted for type
+ * parseEvent: true,
+ * parseTimezone: true
+ * };
+ *
+ * var parser = new ICAL.ComponentParser(options);
+ *
+ * parser.onevent(eventComponent) {
+ * //...
+ * }
+ *
+ * // ontimezone, etc...
+ *
+ * parser.oncomplete = function() {
+ *
+ * };
+ *
+ * parser.process(stringOrComponent);
+ *
+ * @class
+ * @alias ICAL.ComponentParser
+ * @param {Object=} options Component parser options
+ * @param {Boolean} options.parseEvent Whether events should be parsed
+ * @param {Boolean} options.parseTimezeone Whether timezones should be parsed
+ */
+ function ComponentParser(options) {
+ if (typeof(options) === 'undefined') {
+ options = {};
+ }
+
+ var key;
+ for (key in options) {
+ /* istanbul ignore else */
+ if (options.hasOwnProperty(key)) {
+ this[key] = options[key];
+ }
+ }
+ }
+
+ ComponentParser.prototype = {
+
+ /**
+ * When true, parse events
+ *
+ * @type {Boolean}
+ */
+ parseEvent: true,
+
+ /**
+ * When true, parse timezones
+ *
+ * @type {Boolean}
+ */
+ parseTimezone: true,
+
+
+ /* SAX like events here for reference */
+
+ /**
+ * Fired when parsing is complete
+ * @callback
+ */
+ oncomplete: /* istanbul ignore next */ function() {},
+
+ /**
+ * Fired if an error occurs during parsing.
+ *
+ * @callback
+ * @param {Error} err details of error
+ */
+ onerror: /* istanbul ignore next */ function(err) {},
+
+ /**
+ * Fired when a top level component (VTIMEZONE) is found
+ *
+ * @callback
+ * @param {ICAL.Timezone} component Timezone object
+ */
+ ontimezone: /* istanbul ignore next */ function(component) {},
+
+ /**
+ * Fired when a top level component (VEVENT) is found.
+ *
+ * @callback
+ * @param {ICAL.Event} component Top level component
+ */
+ onevent: /* istanbul ignore next */ function(component) {},
+
+ /**
+ * Process a string or parse ical object. This function itself will return
+ * nothing but will start the parsing process.
+ *
+ * Events must be registered prior to calling this method.
+ *
+ * @param {ICAL.Component|String|Object} ical The component to process,
+ * either in its final form, as a jCal Object, or string representation
+ */
+ process: function(ical) {
+ //TODO: this is sync now in the future we will have a incremental parser.
+ if (typeof(ical) === 'string') {
+ ical = ICAL.parse(ical);
+ }
+
+ if (!(ical instanceof ICAL.Component)) {
+ ical = new ICAL.Component(ical);
+ }
+
+ var components = ical.getAllSubcomponents();
+ var i = 0;
+ var len = components.length;
+ var component;
+
+ for (; i < len; i++) {
+ component = components[i];
+
+ switch (component.name) {
+ case 'vtimezone':
+ if (this.parseTimezone) {
+ var tzid = component.getFirstPropertyValue('tzid');
+ if (tzid) {
+ this.ontimezone(new ICAL.Timezone({
+ tzid: tzid,
+ component: component
+ }));
+ }
+ }
+ break;
+ case 'vevent':
+ if (this.parseEvent) {
+ this.onevent(new ICAL.Event(component));
+ }
+ break;
+ default:
+ continue;
+ }
+ }
+
+ //XXX: ideally we should do a "nextTick" here
+ // so in all cases this is actually async.
+ this.oncomplete();
+ }
+ };
+
+ return ComponentParser;
+}());
diff --git a/calendar/base/modules/moz.build b/calendar/base/modules/moz.build
new file mode 100644
index 000000000..a99d53419
--- /dev/null
+++ b/calendar/base/modules/moz.build
@@ -0,0 +1,23 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ 'calAlarmUtils.jsm',
+ 'calAsyncUtils.jsm',
+ 'calAuthUtils.jsm',
+ 'calExtract.jsm',
+ 'calHashedArray.jsm',
+ 'calItemUtils.jsm',
+ 'calIteratorUtils.jsm',
+ 'calItipUtils.jsm',
+ 'calPrintUtils.jsm',
+ 'calProviderUtils.jsm',
+ 'calRecurrenceUtils.jsm',
+ 'calUtils.jsm',
+ 'calViewUtils.jsm',
+ 'calXMLUtils.jsm',
+ 'ical.js',
+]
+
diff --git a/calendar/base/moz.build b/calendar/base/moz.build
new file mode 100644
index 000000000..697027271
--- /dev/null
+++ b/calendar/base/moz.build
@@ -0,0 +1,59 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS = [
+ 'public',
+ 'src',
+ 'modules',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ DEFINES['THEME'] = 'windows'
+else:
+ DEFINES['THEME'] = 'linux'
+
+# Window icons are not needed on mac
+if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
+ icon_path = 'themes/common/icons/'
+ window_icons = [
+ 'calendar-alarm-dialog',
+ 'calendar-event-dialog',
+ 'calendar-event-summary-dialog',
+ 'calendar-task-dialog',
+ 'calendar-task-summary-dialog',
+ ]
+
+ # Set up the icon suffix to differ between windows and linux
+ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ icon_suffix = '.ico'
+ else:
+ icon_suffix = '.png'
+
+ FINAL_TARGET_FILES.chrome.icons.default += [
+ '%s%s%s' % (icon_path, icon, icon_suffix) for icon in window_icons
+ ]
+
+with Files('content/**'):
+ BUG_COMPONENT = ('Calendar', 'Calendar Views')
+
+with Files('content/preferences/**'):
+ BUG_COMPONENT = ('Calendar', 'Preferences')
+
+with Files('content/dialogs/**'):
+ BUG_COMPONENT = ('Calendar', 'Dialogs')
+
+with Files('content/*task*'):
+ BUG_COMPONENT = ('Calendar', 'Tasks')
+
+with Files('content/dialogs/*alarm*'):
+ BUG_COMPONENT = ('Calendar', 'Alarms')
+
+with Files('content/widgets/*alarm*'):
+ BUG_COMPONENT = ('Calendar', 'Alarms')
+
+with Files('themes/**'):
+ BUG_COMPONENT = ('Calendar', 'Calendar Views')
diff --git a/calendar/base/public/calBaseCID.h b/calendar/base/public/calBaseCID.h
new file mode 100644
index 000000000..a8c47efc5
--- /dev/null
+++ b/calendar/base/public/calBaseCID.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CALBASECID_H_
+#define CALBASECID_H_
+
+/* C++ */
+#define CAL_DATETIME_CID \
+ { 0x85475b45, 0x110a, 0x443c, { 0xaf, 0x3f, 0xb6, 0x63, 0x98, 0xa5, 0xa7, 0xcd } }
+#define CAL_DATETIME_CONTRACTID \
+ "@mozilla.org/calendar/datetime;1"
+
+#define CAL_DURATION_CID \
+ { 0x63513139, 0x51cb, 0x4f5b, { 0x9a, 0x52, 0x49, 0xac, 0xcc, 0x5c, 0xae, 0x17 } }
+#define CAL_DURATION_CONTRACTID \
+ "@mozilla.org/calendar/duration;1"
+
+#define CAL_PERIOD_CID \
+ { 0x12fdd72b, 0xc5b6, 0x4720, { 0x81, 0x66, 0x2d, 0xec, 0xa1, 0x33, 0x82, 0xf5 } }
+#define CAL_PERIOD_CONTRACTID \
+ "@mozilla.org/calendar/period;1"
+
+#define CAL_ICSSERVICE_CID \
+ { 0xae4ca6c3, 0x981b, 0x4f66, { 0xa0, 0xce, 0x2f, 0x2c, 0x21, 0x8a, 0xd9, 0xe3 } }
+#define CAL_ICSSERVICE_CONTRACTID \
+ "@mozilla.org/calendar/ics-service;1"
+
+#define CAL_ICALPROPERTY_CID \
+ { 0x17349a10, 0x5d80, 0x47fa, { 0x9b, 0xea, 0xf2, 0x29, 0x57, 0x35, 0x76, 0x75 } }
+#define CAL_ICALCOMPONENT_CID \
+ { 0xc4637c40, 0x3c4c, 0x4ecd, { 0xb8, 0x02, 0x8b, 0x5b, 0x46, 0xbd, 0xf5, 0xa4 } }
+
+#define CAL_TIMEZONESERVICE_CID \
+ { 0x1a23ace4, 0xa0dd, 0x43b4, { 0x96, 0xa8, 0xb3, 0xcd, 0x41, 0x9a, 0x14, 0xa5 } }
+#define CAL_TIMEZONESERVICE_CONTRACTID \
+ "@mozilla.org/calendar/timezone-service;1"
+
+#define CAL_RECURRENCERULE_CID \
+ { 0xd9560bf9, 0x3065, 0x404a, { 0x90, 0x4c, 0xc8, 0x82, 0xfc, 0x9c, 0x9b, 0x74 } }
+#define CAL_RECURRENCERULE_CONTRACTID \
+ "@mozilla.org/calendar/recurrence-rule;1"
+
+/* JS -- Update these from calItemModule.js */
+#define CAL_EVENT_CID \
+ { 0x974339d5, 0xab86, 0x4491, { 0xaa, 0xaf, 0x2b, 0x2c, 0xa1, 0x77, 0xc1, 0x2b } }
+#define CAL_EVENT_CONTRACTID \
+ "@mozilla.org/calendar/event;1"
+
+#define CAL_TODO_CID \
+ { 0x7af51168, 0x6abe, 0x4a31, { 0x98, 0x4d, 0x6f, 0x8a, 0x39, 0x89, 0x21, 0x2d } }
+#define CAL_TODO_CONTRACTID \
+ "@mozilla.org/calendar/todo;1"
+
+#define CAL_ATTENDEE_CID \
+ { 0x5c8dcaa3, 0x170c, 0x4a73, { 0x81, 0x42, 0xd5, 0x31, 0x15, 0x6f, 0x66, 0x4d } }
+#define CAL_ATTENDEE_CONTRACTID \
+ "@mozilla.org/calendar/attendee;1"
+
+#define CAL_RECURRENCEINFO_CID \
+ { 0x04027036, 0x5884, 0x4a30, { 0xb4, 0xaf, 0xf2, 0xca, 0xd7, 0x9f, 0x6e, 0xdf } }
+#define CAL_RECURRENCEINFO_CONTRACTID \
+ "@mozilla.org/calendar/recurrence-info;1"
+
+#define NS_ERROR_CALENDAR_WRONG_COMPONENT_TYPE NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_CALENDAR, 1)
+// Until extensible xpconnect error mapping works
+// #define NS_ERROR_CALENDAR_IMMUTABLE NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_CALENDAR, 2)
+#define NS_ERROR_CALENDAR_IMMUTABLE NS_ERROR_OBJECT_IS_IMMUTABLE
+
+#endif /* CALBASECID_H_ */
diff --git a/calendar/base/public/calIAlarm.idl b/calendar/base/public/calIAlarm.idl
new file mode 100644
index 000000000..4a51d62ea
--- /dev/null
+++ b/calendar/base/public/calIAlarm.idl
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIVariant;
+interface nsISimpleEnumerator;
+
+interface calIAttachment;
+interface calIAttendee;
+interface calIDateTime;
+interface calIDuration;
+interface calIItemBase;
+interface calIIcalComponent;
+
+[scriptable, uuid(b8db7c7f-c168-4e11-becb-f26c1c4f5f8f)]
+interface calIAlarm : nsISupports
+{
+ /**
+ * Returns true if this alarm is able to be modified
+ */
+ readonly attribute boolean isMutable;
+
+ /**
+ * Makes this alarm immutable.
+ */
+ void makeImmutable();
+
+ /**
+ * Make a copy of this alarm. The returned alarm will be mutable.
+ */
+ calIAlarm clone();
+
+ /**
+ * How this alarm is shown. Special values as described in rfc2445 are
+ * AUDIO, DISPLAY, EMAIL
+ * In addition, custom actions may be defined as an X-Prop, i.e
+ * X-SMS.
+ *
+ * Note that aside from setting this action, the frontend must be able to
+ * handle the specified action. Unknown actions WILL NOT be notified for.
+ */
+ attribute AUTF8String action;
+
+ /**
+ * The offset between the item's date and the alarm time.
+ * This will be null for absolute alarms.
+ */
+ attribute calIDuration offset;
+
+ /**
+ * The absolute date and time the alarm should fire.
+ * This will be null for relative alarms.
+ */
+ attribute calIDateTime alarmDate;
+
+ /**
+ * One of the ALARM_RELATED constants below.
+ */
+ attribute unsigned long related;
+
+ /**
+ * The alarm is absolute and is therefore not related to either.
+ */
+ const unsigned long ALARM_RELATED_ABSOLUTE = 0;
+
+ /**
+ * The alarm's offset should be based off of the startDate or
+ * entryDate (for events and tasks, respectively)
+ */
+ const unsigned long ALARM_RELATED_START = 1;
+
+ /**
+ * the alarm's offset should be based off of the endDate or
+ * dueDate (for events and tasks, respectively)
+ */
+ const unsigned long ALARM_RELATED_END = 2;
+
+ /**
+ * Times the alarm should be repeated. This value is the number of
+ * ADDITIONAL alarms, aside from the actual alarm.
+ *
+ * For the alarm to be valid, if repeat is specified, the repeatOffset
+ * attribute MUST also be specified.
+ */
+ attribute unsigned long repeat;
+
+ /**
+ * The duration between the alarm and each subsequent repeat
+ *
+ * For the alarm to be valid, if repeatOffset is specified, the repeat
+ * attribute MUST also be specified.
+ */
+ attribute calIDuration repeatOffset;
+
+ /**
+ * If repeat is specified, this helper returns the first DATETIME the alarm
+ * should be repeated on.
+ * This will be null for relative alarms.
+ */
+ readonly attribute calIDateTime repeatDate;
+
+ /**
+ * The description of the alarm. Not valid for AUDIO alarms.
+ */
+ attribute AUTF8String description;
+
+ /**
+ * The summary of the alarm. Not valid for AUDIO and DISPLAY alarms.
+ */
+ attribute AUTF8String summary;
+
+ /**
+ * Manage Attendee for this alarm. Not valid for AUDIO and DISPLAY alarms.
+ */
+ void addAttendee(in calIAttendee aAttendee);
+ void deleteAttendee(in calIAttendee aAttendee);
+ void clearAttendees();
+ void getAttendees(out uint32_t count,
+ [array,size_is(count),retval] out calIAttendee attendees);
+
+ /**
+ * Manage Attachments for this alarm.
+ * For EMAIL alarms, more than one attachment can be specified.
+ * For AUDIO alarms, one Attachment can be specified.
+ * For DISPLAY alarms, attachments are invalid.
+ */
+ void addAttachment(in calIAttachment aAttachment);
+ void deleteAttachment(in calIAttachment aAttachment);
+ void clearAttachments();
+ void getAttachments(out uint32_t count,
+ [array,size_is(count),retval] out calIAttachment attachments);
+
+ /**
+ * The human readable representation of this alarm. Uses locale strings.
+ *
+ * @param aItem The item to base the string on. Defaults to an event.
+ */
+ AUTF8String toString([optional] in calIItemBase aItem);
+
+ /**
+ * The ical representation of this VALARM
+ */
+ attribute AUTF8String icalString;
+
+ /**
+ * The ical component of this VALARM
+ */
+ attribute calIIcalComponent icalComponent;
+
+ // Property bag
+ boolean hasProperty(in AUTF8String name);
+ nsIVariant getProperty(in AUTF8String name);
+ void setProperty(in AUTF8String name, in nsIVariant value);
+ void deleteProperty(in AUTF8String name);
+
+ readonly attribute nsISimpleEnumerator propertyEnumerator;
+};
diff --git a/calendar/base/public/calIAlarmService.idl b/calendar/base/public/calIAlarmService.idl
new file mode 100644
index 000000000..adce944cf
--- /dev/null
+++ b/calendar/base/public/calIAlarmService.idl
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calICalendar;
+interface calIDuration;
+interface calITimezone;
+interface calIAlarm;
+interface calIOperation;
+
+[scriptable,uuid(dc96dd04-d2dd-448e-b307-8c8ff39c72af)]
+interface calIAlarmServiceObserver : nsISupports
+{
+ /**
+ * Gets called when an alarm has fired. Depending on type of alarm, an
+ * observer could bring up a dialog or play a sound.
+ */
+ void onAlarm(in calIItemBase item, in calIAlarm alarm);
+
+ /**
+ * Called if alarm(s) of a specific item are to be removed from
+ * the alarm window.
+ *
+ * @param aItem corresponding item, maybe master item of recurring
+ * series (then all alarms belonging to this item are to
+ * be removed)
+ */
+ void onRemoveAlarmsByItem(in calIItemBase item);
+
+ /**
+ * Called if all alarms of a specific calendar are to be removed.
+ */
+ void onRemoveAlarmsByCalendar(in calICalendar calendar);
+
+ /**
+ * Called when all alarms of a specific calendar are loaded.
+ */
+ void onAlarmsLoaded(in calICalendar calendar);
+};
+
+[scriptable,uuid(42cfa9ce-49d6-11e5-b88c-5b90eedc1c47)]
+interface calIAlarmService : nsISupports
+{
+ /**
+ * Upper limit for the snooze period for an alarm. To avoid performance issues, don't change this
+ * to a value larger then 1 at least until bug 861594 or a similar concept is implemented.
+ */
+ const unsigned long MAX_SNOOZE_MONTHS = 1;
+
+ /**
+ * This is the timezone that all-day events will be converted to in order to
+ * determine when their alarms should fire.
+ */
+ attribute calITimezone timezone;
+
+ /**
+ * Will return true while the alarm service is in the process of loading alarms
+ */
+ attribute boolean isLoading;
+
+ /**
+ * Cause the alarm service to start up, create a list of upcoming
+ * alarms in all registered calendars, add observers to watch for
+ * calendar registration and unregistration, and setup a timer to
+ * maintain that list and fire alarms.
+ *
+ * @note Will throw NS_ERROR_NOT_INITIALIZED if you have not previously set
+ * the timezone attribute.
+ */
+ void startup();
+
+ /**
+ * Shuts down the alarm service, canceling all timers and removing all
+ * alarms.
+ */
+ void shutdown();
+
+ /* add and remove observers that will be notified when an
+ alarm has gone off. It is up to the application to display
+ the alarm.
+ */
+ void addObserver(in calIAlarmServiceObserver observer);
+ void removeObserver(in calIAlarmServiceObserver observer);
+
+ /**
+ * Call to reschedule an alarm to be notified at a later point. The alarm will
+ * instead fire at "now + duration" This will cause an event to be scheduled
+ * even if it was not previously scheduled.
+ *
+ * @param item The item the alarm belongs to.
+ * @param alarm The alarm to snooze.
+ * @param duration The duration in minutes to snooze for.
+ * @return The operation that modifies the item to snooze the
+ * alarm.
+ */
+ calIOperation snoozeAlarm(in calIItemBase item, in calIAlarm alarm, in calIDuration duration);
+
+ /**
+ * Dismisses the given alarm for the passed occurrence.
+ *
+ * @param item The item the alarm belongs to.
+ * @param alarm The alarm to dismiss.
+ * @return The operation that modifies the item to dismiss the
+ * alarm.
+ */
+ calIOperation dismissAlarm(in calIItemBase item, in calIAlarm alarm);
+};
diff --git a/calendar/base/public/calIAttachment.idl b/calendar/base/public/calIAttachment.idl
new file mode 100644
index 000000000..7ca054d5a
--- /dev/null
+++ b/calendar/base/public/calIAttachment.idl
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface calIIcalProperty;
+interface calIItemBase;
+
+[scriptable,uuid(7a17d45d-1c0e-4877-baa3-0eb67e770498)]
+interface calIAttachment : nsISupports
+{
+ /**
+ * The hash id is used to identify this attachment and compare it to others.
+ */
+ readonly attribute AUTF8String hashId;
+
+ /**
+ * An nsIURI object that points to the file (local or remote)
+ */
+ attribute nsIURI uri;
+
+ /**
+ * Raw attachment data, in case its not an uri
+ */
+ attribute AUTF8String rawData;
+
+ /**
+ * The type of file that this attachment refers to
+ */
+ attribute AString formatType;
+
+ /**
+ * The encoding the (local) file should be encoded with.
+ */
+ attribute AUTF8String encoding;
+
+ /**
+ * The calIIcalProperty corresponding to this object. Can be used for
+ * serializing/unserializing from ics files.
+ */
+ attribute calIIcalProperty icalProperty;
+ attribute AUTF8String icalString;
+
+ /**
+ * For accessing additional parameters, such as x-params.
+ */
+ AUTF8String getParameter(in AString name);
+ void setParameter(in AString name, in AUTF8String value);
+ void deleteParameter(in AString name);
+
+ /**
+ * Clone this calIAttachment instance into a new object.
+ */
+ calIAttachment clone();
+};
diff --git a/calendar/base/public/calIAttendee.idl b/calendar/base/public/calIAttendee.idl
new file mode 100644
index 000000000..dea621f42
--- /dev/null
+++ b/calendar/base/public/calIAttendee.idl
@@ -0,0 +1,80 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIIcalProperty;
+interface nsISimpleEnumerator;
+
+[scriptable,uuid(73a074ad-8812-4055-af75-14b509b8c5fe)]
+interface calIAttendee : nsISupports
+{
+ readonly attribute boolean isMutable;
+
+ // makes this item immutable
+ void makeImmutable();
+
+ // clone always returns a mutable event
+ calIAttendee clone();
+
+ attribute AUTF8String id;
+ attribute AUTF8String commonName;
+ attribute AUTF8String rsvp;
+
+ /**
+ * If true, indicates that this is not a standard attendee, but rather this
+ * icalProperty corresponds to the organizer of the event (rfc2445 Sec 4.8.4.3)
+ */
+ attribute boolean isOrganizer;
+
+ /**
+ * CHAIR
+ * REQ-PARTICIPANT
+ * OPT-PARTICIPANT
+ * NON-PARTICIPANT
+ */
+ attribute AUTF8String role;
+
+ /**
+ * NEEDS-ACTION
+ * ACCEPTED
+ * DECLINED
+ * TENTATIVE
+ * DELEGATED
+ * COMPLETED
+ * IN-PROCESS
+ */
+ attribute AUTF8String participationStatus;
+
+ /**
+ * INDIVIDUAL
+ * GROUP
+ * RESOURCE
+ * ROOM
+ * UNKNOWN
+ */
+ attribute AUTF8String userType;
+
+ readonly attribute nsISimpleEnumerator propertyEnumerator;
+
+ // If you use the has/get/set/deleteProperty
+ // methods, property names are case-insensitive.
+ //
+ // For purposes of ICS serialization, all property names in
+ // the hashbag are in uppercase.
+ AUTF8String getProperty(in AString name);
+ void setProperty(in AString name, in AUTF8String value);
+ void deleteProperty(in AString name);
+
+ attribute calIIcalProperty icalProperty;
+ attribute AUTF8String icalString;
+
+ /**
+ * The display name of the attendee. If the attendee has a common name, this
+ * is used. Otherwise, the attendee id is displayed (often an email), with the
+ * mailto: prefix dropped.
+ */
+ AUTF8String toString();
+};
diff --git a/calendar/base/public/calICalendar.idl b/calendar/base/public/calICalendar.idl
new file mode 100644
index 000000000..288a16ee7
--- /dev/null
+++ b/calendar/base/public/calICalendar.idl
@@ -0,0 +1,641 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+// decls for stuff from other files
+interface nsIURI;
+interface calIItemBase;
+interface nsIVariant;
+interface nsISimpleEnumerator;
+
+// forward decls for this file
+interface calICalendarACLManager;
+interface calICalendarACLEntry;
+interface calIObserver;
+interface calIOperationListener;
+interface calIRange;
+interface calIDateTime;
+interface calIOperation;
+interface calIStatusObserver;
+interface nsIDOMChromeWindow;
+
+
+[scriptable, uuid(b18782c0-6557-4e8e-931d-4bf052f0a31e)]
+interface calICalendar : nsISupports
+{
+ /**
+ * Unique ID of this calendar. Only the calendar manager is allowed to set
+ * this attribute. For everybody else, it should be considered to be
+ * read-only.
+ * The id is null for unregistered calendars.
+ */
+ attribute AUTF8String id;
+
+ /**
+ * Name of the calendar
+ * Notes: Can only be set after the calendar is registered with the calendar manager.
+ */
+ attribute AUTF8String name;
+
+ /**
+ * Type of the calendar
+ * 'memory', 'storage', 'caldav', etc
+ */
+ readonly attribute AUTF8String type;
+
+ /**
+ * If this calendar is provided by an extension, this attribute should return
+ * the extension's id, otherwise null.
+ */
+ readonly attribute AString providerID;
+
+ /**
+ * Returns the acl manager for the calendar, based on the "aclManagerClass"
+ * property. If this property is not defined, the default manager is used
+ */
+ readonly attribute calICalendarACLManager aclManager;
+
+ /**
+ * Returns the acl entry associated to the calendar.
+ */
+ readonly attribute calICalendarACLEntry aclEntry;
+
+ /**
+ * Multiple calendar instances may be composited, logically acting as a
+ * single calendar, e.g. for caching puorposing.
+ * This attribute determines the topmost calendar that returned items should
+ * belong to. If the current instance is the topmost calendar, then it should
+ * be returned directly.
+ *
+ * @see calIItemBase::calendar
+ */
+ attribute calICalendar superCalendar;
+
+ /**
+ * Setting this URI causes the calendar to be (re)loaded.
+ * This is not an unique identifier! It is also not unchangeable. Don't
+ * use it to identify a calendar, use the id attribute for that purpose.
+ */
+ attribute nsIURI uri;
+
+ /**
+ * Is this calendar read-only? Used by the UI to decide whether or not
+ * widgetry should allow editing.
+ */
+ attribute boolean readOnly;
+
+ /**
+ * Whether or not it makes sense to call refresh() on this calendar.
+ */
+ readonly attribute boolean canRefresh;
+
+ /**
+ * Setting this attribute to true will prevent the calendar to make calendar properties
+ * persistent, which is useful if you would like to set properties on unregistered
+ * calendar instances.
+ */
+ attribute boolean transientProperties;
+
+ /**
+ * Gets a calendar property.
+ * The call returns null in case the property is not known;
+ * callers should use a sensible default in that case.
+ *
+ * It's up to the provider where to store properties,
+ * e.g. on the server or in local prefs.
+ *
+ * Currently known properties are:
+ * [boolean] disabled
+ * [boolean] auto-enabled If true, the calendar will be enabled on next startup.
+ * [boolean] force-disabled If true, the calendar cannot be enabled (transient).
+ * [boolean] calendar-main-in-composite
+ * [string] name
+ * [boolean] readOnly
+ * [boolean] requiresNetwork If false, the calendar does not require
+ * network access at all. This is mainy used
+ * as a UI hint.
+ * [boolean] suppressAlarms If true, alarms of this calendar are not minded.
+ * [boolean] cache.supported If true, the calendar should to be cached,
+ * e.g. this generally applies to network calendars;
+ * default is true (if not present).
+ * [boolean] cache.enabled If true, the calendar is cached; default is false.
+ * [boolean] cache.always If true, the cache will always be enabled
+ * and the user cannot turn it off. For
+ * backward compatibility, return true for
+ * cache.enabled too.
+ *
+ * [nsresult] currentStatus The current error status of the calendar (transient).
+ *
+ * [calIItipTransport] itip.transport If the provider implements a custom calIItipTransport (transient)
+ * If null, then Email Scheduling will effectively be
+ * disabled. This means for example, the calendar will
+ * not show up in the list of calendars to store an
+ * invitation in.
+ * [boolean] itip.disableRevisionChecks If true, the iTIP handling code disables revision checks
+ * against SEQUENCE and DTSTAMP, and will never reject an
+ * iTIP message as outdated
+ * [nsIMsgIdentity] imip.identity If provided, this is the email identity used for
+ * scheduling purposes
+ * [boolean] imip.identity.disabled If true, this calendar doesn't support switching imip
+ * identities. This for example means that the
+ * dropdown of identities will not be shown in the
+ * calendar properties dialog. (transient)
+ * scheduling purposes
+ * [nsIMsgAccount] imip.account If provided, this is the email account used for
+ * scheduling purposes
+ * [string] imip.identity.key If provided, this is the email internal identity key used to
+ * get the above
+ *
+ * [string] organizerId If provided, this is the preset organizer id on creating
+ * scheduling appointments (transient)
+ * [string] organizerCN If provided, this is the preset organizer common name on creating
+ * scheduling appointments (transient)
+ *
+ * The following calendar capabilities can be used to inform the UI or backend
+ * that certain features are not supported. If not otherwise mentioned, not
+ * specifying these capabilities assumes a default value of true
+ * capabilities.alarms.popup.supported Supports popup alarms
+ * capabilities.alarms.oninviations.supported Supports alarms on inviations.
+ * capabilities.alarms.maxCount Maximum number of alarms supported per event
+ * capabilities.attachments.supported Supports attachments
+ * capabilities.categories.maxCount Maximum number of supported categories.
+ * -1 means infinite, 0 means disabled.
+ * capabilities.privacy.supported Supports a privacy state
+ * capabilities.priority.supported Supports the priority field
+ * capabilities.events.supported Supports tasks
+ * capabilities.tasks.supported Supports events
+ * capabilities.timezones.floating.supported Supports local time
+ * capabilities.timezones.UTC.supported Supports UTC/GMT timezone
+ * capabilities.autoschedule.supported Supports caldav schedule properties in
+ * icalendar (SCHEDULE-AGENT, SCHEDULE-STATUS...)
+ *
+ * The following capabilities are used to restrict the values for specific
+ * fields. An array should be specified with the values, the default
+ * values are specified here. Extensions using this need to take care of
+ * adding any UI elements needed in an overlay. To make sure the correct
+ * elements are shown, those elements should additionally specify an attribute
+ * "provider", with the type of the provider.
+ *
+ * capabilities.privacy.values = ["PUBLIC", "CONFIDENTIAL", "PRIVATE"];
+ *
+ * The following special capability disables rewriting the WWW-Authenticate
+ * header on HTTP requests to include the calendar name. The default value
+ * is false, i.e rewriting is NOT disabled.
+ *
+ * capabilities.realmrewrite.disabled = false
+ *
+ * The following capability describes if the calendar can be permanently
+ * deleted, or just unsubscribed. If this property is not specified, then
+ * only unsubscribing is allowed. If an empty array is specified, neither
+ * deleting nor unsubscribing is presented in the UI.
+ *
+ * capabilities.removeModes = ["delete", "unsubscribe"]
+ *
+ * @param aName property name
+ * @return value (string, integer and boolean values are supported),
+ * else null
+ */
+ nsIVariant getProperty(in AUTF8String aName);
+
+ /**
+ * Sets a calendar property.
+ * This will (only) cause a notification onPropertyChanged() in case
+ * the value has changed.
+ *
+ * It's up to the provider where to store properties,
+ * e.g. on the server or in local prefs.
+ *
+ * @param aName property name
+ * @param aValue value
+ * (string, integer and boolean values are supported)
+ */
+ void setProperty(in AUTF8String aName, in nsIVariant aValue);
+
+ /**
+ * Deletes a calendar property.
+ *
+ * It's up to the provider where to store properties,
+ * e.g. on the server or in local prefs.
+ *
+ * @param aName property name
+ */
+ void deleteProperty(in AUTF8String aName);
+
+ /**
+ * In combination with the other parameters to getItems(), these
+ * constants provide for a very basic filtering mechanisms for use
+ * in getting and observing items. At some point fairly soon, we're
+ * going to need to generalize this mechanism significantly (so we
+ * can allow boolean logic, categories, etc.).
+ *
+ * When adding item filters (bits which, when not set to 1, reduce the
+ * scope of the results), use bit positions <= 15, so that
+ * ITEM_FILTER_ALL_ITEMS remains compatible for components that have the
+ * constant compiled in.
+ *
+ * XXX the naming here is questionable; adding a filter (setting a bit, in
+ * this case) usually _reduces_ the set of items that pass the set of
+ * filters, rather than adding to it.
+ */
+ const unsigned long ITEM_FILTER_COMPLETED_YES = 1 << 0;
+ const unsigned long ITEM_FILTER_COMPLETED_NO = 1 << 1;
+ const unsigned long ITEM_FILTER_COMPLETED_ALL = (ITEM_FILTER_COMPLETED_YES |
+ ITEM_FILTER_COMPLETED_NO);
+
+ const unsigned long ITEM_FILTER_TYPE_TODO = 1 << 2;
+ const unsigned long ITEM_FILTER_TYPE_EVENT = 1 << 3;
+ const unsigned long ITEM_FILTER_TYPE_JOURNAL = 1 << 4;
+ const unsigned long ITEM_FILTER_TYPE_ALL = (ITEM_FILTER_TYPE_TODO |
+ ITEM_FILTER_TYPE_EVENT |
+ ITEM_FILTER_TYPE_JOURNAL);
+
+ const unsigned long ITEM_FILTER_ALL_ITEMS = 0xFFFF;
+
+ /**
+ * If set, return calIItemBase occurrences for all the appropriate instances,
+ * as determined by an item's recurrenceInfo. All of these occurrences will
+ * have their parentItem set to the recurrence parent. If not set, will
+ * return only calIItemBase parent items.
+ */
+ const unsigned long ITEM_FILTER_CLASS_OCCURRENCES = 1 << 16;
+
+ /**
+ * Scope: Attendee
+ * Filter items that correspond to an invitation from another
+ * user and the current user has not replied to it yet.
+ */
+ const unsigned long ITEM_FILTER_REQUEST_NEEDS_ACTION = 1 << 17;
+
+ /**
+ * Flags for items that have been created, modified or deleted while
+ * offline.
+ * ITEM_FILTER_OFFLINE_DELETED is a particular case in that elements *must*
+ * be excluded from searches when not specified in the filter mask.
+ */
+ const unsigned long ITEM_FILTER_OFFLINE_CREATED = 1 << 29;
+ const unsigned long ITEM_FILTER_OFFLINE_MODIFIED = 1 << 30;
+ const unsigned long ITEM_FILTER_OFFLINE_DELETED = 1 << 31;
+
+ void addObserver( in calIObserver observer );
+ void removeObserver( in calIObserver observer );
+
+ /**
+ * The following five "Item" functions are all asynchronous, and return
+ * their results to a calIOperationListener object.
+ *
+ */
+
+ /**
+ * addItem adds the given calIItemBase to the calendar.
+ *
+ * @param aItem item to add
+ * @param aListener where to call back the results
+ * @return optional operation handle to track the operation
+ *
+ * - If aItem already has an ID, that ID is used when adding.
+ * - If aItem is mutable and has no ID, the calendar is expected
+ * to generate an ID for the item.
+ * - If aItem is immutable and has no ID, an error is thrown.
+ *
+ * The results of the operation are reported through an
+ * onOperationComplete call on the listener, with the following
+ * parameters:
+ *
+ * - aOperationType: calIOperationListener::ADD
+ * - aId: the ID of the newly added item
+ * - aDetail: the calIItemBase corresponding to the immutable
+ * version of the newly added item
+ *
+ * If an item with a given ID already exists in the calendar,
+ * onOperationComplete is called with an aStatus of NS_ERROR_XXXXX,
+ * and aDetail set with the calIItemBase of the internal already
+ * existing item.
+ */
+ calIOperation addItem(in calIItemBase aItem,
+ in calIOperationListener aListener);
+
+ /**
+ * adoptItem adds the given calIItemBase to the calendar, but doesn't
+ * clone it. It adopts the item as-is. This is generally for use in
+ * performance-critical situations where there is no danger of the caller
+ * using the item after making the call.
+ *
+ * @see addItem
+ */
+ calIOperation adoptItem(in calIItemBase aItem,
+ in calIOperationListener aListener);
+
+ /**
+ * modifyItem takes a modified item and modifies the
+ * calendar's internal version of the item to match. The item is
+ * expected to have an ID that already exists in the calendar; if it
+ * doesn't, or there is no id, onOperationComplete is called with a
+ * status of NS_ERROR_XXXXX. If the item is immutable,
+ * onOperationComplete is called with a status of NS_ERROR_XXXXX.
+ *
+ * If the generation of the given aNewItem does not match the generation
+ * of the internal item (indicating that someone else modified the
+ * item), onOperationComplete is called with a status of NS_ERROR_XXXXX
+ * and aDetail is set to the latest-version internal immutable item.
+ *
+ * If you would like to disable revision checks, pass null as aOldItem. This
+ * will overwrite the item on the server.
+ *
+ * @param aNewItem new version to replace the old one
+ * @param aOldItem caller's view of the item to be changed, as it is now
+ * @param aListener where to call back the results
+ * @return optional operation handle to track the operation
+ *
+ * The results of the operation are reported through an
+ * onOperationComplete call on the listener, with the following
+ * parameters:
+ *
+ * - aOperationType: calIOperationListener::MODIFY
+ * - aId: the ID of the modified item
+ * - aDetail: the calIItemBase corresponding to the newly-updated
+ * immutable version of the modified item
+ */
+ calIOperation modifyItem(in calIItemBase aNewItem,
+ in calIItemBase aOldItem,
+ in calIOperationListener aListener);
+
+ /**
+ * deleteItem takes an item that is to be deleted. The item is
+ * expected to have an ID that already exists in the calendar; if it
+ * doesn't, or there is no id, onOperationComplete is called with
+ * a status of NS_ERROR_XXXXX.
+ *
+ * @param aItem item to delete
+ * @param aListener where to call back the results
+ * @return optional operation handle to track the operation
+ *
+ * The results of the operation are reported through an
+ * onOperationComplete call on the listener, with the following
+ * parameters:
+ *
+ * - aOperationType: calIOperationListener::DELETE
+ * - aId: the ID of the deleted item
+ * - aDetail: the calIItemBase corresponding to the immutable version
+ * of the deleted item
+ */
+ calIOperation deleteItem(in calIItemBase aItem,
+ in calIOperationListener aListener);
+
+ /**
+ * Get a single event. The event will be typed as one of the subclasses
+ * of calIItemBase (whichever concrete type is most appropriate).
+ *
+ * @param aId UID of the event
+ * @param aListener listener to which this event will be called back.
+ * @return optional operation handle to track the operation
+ *
+ * The results of the operation are reported through the listener,
+ * via zero or one onGetResult calls (with aCount set to 1)
+ * followed by an onOperationComplete.
+ *
+ * The parameters to onOperationComplete will be:
+ *
+ * - aOperationType: calIOperationListener::GET
+ * - aId: the ID of the requested item
+ * - aDetail: null (? we can also pass the item back here as well,..)
+ */
+ calIOperation getItem(in string aId, in calIOperationListener aListener);
+
+ /**
+ * XXX As mentioned above, this method isn't suitably general. It's just
+ * placeholder until it gets supplanted by something more SQL or RDF-like.
+ *
+ * Ordering: This method is currently guaranteed to return lists ordered
+ * as follows to make for the least amount of pain when
+ * migrating existing frontend code:
+ *
+ * The events are sorted based on the order of their next occurrence
+ * if they recur in the future or their last occurrence in the past
+ * otherwise. Here's a presentation of the sort criteria using the
+ * time axis:
+ *
+ * -----(Last occurrence of Event1)---(Last occurrence of Event2)----(Now)----(Next occurrence of Event3)---->
+ *
+ * (Note that Event1 and Event2 will not recur in the future.)
+ *
+ * We should probably be able get rid of this ordering constraint
+ * at some point in the future.
+ *
+ * Note that the range is intended to act as a mask on the
+ * occurrences, not just the initial recurring items. So if a
+ * getItems() call without ITEM_FILTER_CLASS_occurrenceS is made, all
+ * events and todos which have occurrences inside the range should
+ * be returned, even if some of those events or todos themselves
+ * live outside the range.
+ *
+ * @param aItemFilter ITEM_FILTER flags, or-ed together
+ * @param aCount Maximum number of items to return, or 0 for
+ * an unbounded query.
+ * @param aRangeStart Items starting at this time or after should be
+ * returned. If invalid, assume "since the beginning
+ * of time".
+ * @param aRangeEndEx Items starting before (not including) aRangeEndEx should be
+ * returned. If null, assume "until the end of time".
+ * @param aListener The results will be called back through this interface.
+ * @return optional operation handle to track the operation
+ *
+ *
+ * The results of the operation are reported through the listener,
+ * via zero or more onGetResult calls followed by an onOperationComplete.
+ *
+ * The parameters to onOperationComplete will be:
+ *
+ * - aOperationType: calIOperationListener::GET
+ * - aId: null
+ * - aDetail: null
+ */
+ calIOperation getItems(in unsigned long aItemFilter,
+ in unsigned long aCount,
+ in calIDateTime aRangeStart,
+ in calIDateTime aRangeEndEx,
+ in calIOperationListener aListener);
+
+ /**
+ * Refresh the datasource, and call the observers for any changes found.
+ * If the provider doesn't know the details of the changes it must call
+ * onLoad on its observers.
+ *
+ * @return optional operation handle to track the operation
+ */
+ calIOperation refresh();
+
+ /**
+ * Turn on batch mode. Observers will get a notification of this.
+ * They will still get notified for every individual change, but they are
+ * free to ignore those notifications.
+ * Use this when a lot of changes are about to happen, and it would be
+ * useless to refresh the display (or the backend store) for every change.
+ * Caller must make sure to also call endBatchMode. Make sure all errors
+ * are caught!
+ */
+ void startBatch();
+
+ /**
+ * Turn off batch mode.
+ */
+ void endBatch();
+};
+
+/**
+ * Used to allow multiple calendars (eg work and home) to be easily queried
+ * and displayed as a single unit. All calendars are referenced by ID, i.e.
+ * calendars need to have an ID when being added.
+ */
+[scriptable, uuid(6748fa00-79b5-4728-84f3-20dd47e0b031)]
+interface calICompositeCalendar : calICalendar
+{
+ /**
+ * Adds a calendar to the composite, if not already part of it.
+ *
+ * @param aCalendar the calendar to be added
+ */
+ void addCalendar(in calICalendar aCalendar);
+
+ /**
+ * Remove a calendar from the composite
+ *
+ * @param aCalendar the calendar to be removed
+ */
+ void removeCalendar(in calICalendar aCalendar);
+
+ /**
+ * If a calendar for the given ID exists in the CompositeCalendar,
+ * return it; otherwise return null.
+ *
+ * @param aId id of calendar
+ * @return calendar, or null if none
+ */
+ calICalendar getCalendarById(in AUTF8String aId);
+
+ /* return a list of all calendars currently registered */
+ void getCalendars(out uint32_t count,
+ [array, size_is(count), retval] out calICalendar aCalendars);
+
+ /**
+ * In order for addItem() to be called on this object, it is first necessary
+ * to set this attribute to specify which underlying calendar the item is
+ * to be added to.
+ */
+ attribute calICalendar defaultCalendar;
+
+ /**
+ * If set, the composite will initialize itself from calICalendarManager
+ * prefs keyed off of the provided prefPrefix, and update those prefs to
+ * track changes in calendar membership and default calendar.
+ */
+ attribute ACString prefPrefix;
+
+ /**
+ * If returns true there is a process running that needs to displayed
+ * by the statusObserver
+ */
+ readonly attribute boolean statusDisplayed;
+
+ /**
+ * Sets a statusobserver for status notifications like startMeteors() and StopMeteors().
+ */
+ void setStatusObserver(in calIStatusObserver aStatusObserver, in nsIDOMChromeWindow aWindow);
+};
+
+/**
+ * Make a more general nsIObserverService2 and friends to support
+ * nsISupports data and use that instead?
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
+ */
+[scriptable, uuid(2953c9b2-2c73-11d9-80b6-00045ace3b8d)]
+interface calIObserver : nsISupports
+{
+ void onStartBatch();
+ void onEndBatch();
+ void onLoad( in calICalendar aCalendar );
+ void onAddItem( in calIItemBase aItem );
+ void onModifyItem( in calIItemBase aNewItem, in calIItemBase aOldItem );
+ void onDeleteItem( in calIItemBase aDeletedItem );
+ void onError( in calICalendar aCalendar, in nsresult aErrNo, in AUTF8String aMessage );
+
+ /// Called after a property is changed.
+ void onPropertyChanged(in calICalendar aCalendar,
+ in AUTF8String aName,
+ in nsIVariant aValue,
+ in nsIVariant aOldValue);
+
+ /// Called before the property is deleted.
+ void onPropertyDeleting(in calICalendar aCalendar,
+ in AUTF8String aName);
+};
+
+/**
+ * calICompositeObserver interface adds things to observe changes to
+ * a calICompositeCalendar
+ */
+[scriptable, uuid(a3584c92-b8eb-4aa8-a638-e46a2e11d6a9)]
+interface calICompositeObserver : calIObserver
+{
+ void onCalendarAdded( in calICalendar aCalendar );
+ void onCalendarRemoved( in calICalendar aCalendar );
+ void onDefaultCalendarChanged( in calICalendar aNewDefaultCalendar );
+};
+
+/**
+ * Async operations are called back via this interface. If you know that your
+ * object is not going to get called back for either of these methods, having
+ * them return NS_ERROR_NOT_IMPLEMENTED is reasonable.
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
+ */
+[scriptable, uuid(ed3d87d8-2c77-11d9-8f5f-00045ace3b8d)]
+interface calIOperationListener : nsISupports
+{
+ /**
+ * For add, modify, and delete.
+ *
+ * @param aCalendar the calICalendar on which the operation took place
+ * @param aStatus status code summarizing what happened
+ * @param aOperationType type of operation that was completed
+ * @param aId UUID of element that was changed
+ * @param aDetail not yet fully specified. If aStatus is an error
+ * result, this will probably be an extended error
+ * string (eg one returned by a server).
+ */
+ void onOperationComplete(in calICalendar aCalendar,
+ in nsresult aStatus,
+ in unsigned long aOperationType,
+ in string aId,
+ in nsIVariant aDetail);
+ const unsigned long ADD = 1;
+ const unsigned long MODIFY = 2;
+ const unsigned long DELETE = 3;
+ const unsigned long GET = 4;
+
+ /**
+ * For getItem and getItems.
+ *
+ * @param aStatus status code summarizing what happened.
+ * @param aItemType type of interface returned in the array (@see
+ * calICalendar::GetItems).
+ * @param aDetail not yet fully specified. If aStatus is an error
+ * result, this will probably be an extended error
+ * string (eg one returned by a server).
+ * @param aCount size of array returned, in items
+ * @param aItems array of immutable items
+ *
+ * Multiple onGetResults might be called
+ */
+ void onGetResult (in calICalendar aCalendar,
+ in nsresult aStatus,
+ in nsIIDRef aItemType,
+ in nsIVariant aDetail,
+ in uint32_t aCount,
+ [array, size_is(aCount), iid_is(aItemType)] in nsQIResult aItems );
+};
diff --git a/calendar/base/public/calICalendarACLManager.idl b/calendar/base/public/calICalendarACLManager.idl
new file mode 100644
index 000000000..67b772411
--- /dev/null
+++ b/calendar/base/public/calICalendarACLManager.idl
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgIdentity;
+interface nsIURI;
+
+interface calICalendar;
+interface calIItemBase;
+interface calIOperationListener;
+
+interface calIItemACLEntry;
+
+/**
+ */
+[scriptable, uuid(a64bd8a0-e9f0-4f64-928a-1c98861e4703)]
+interface calICalendarACLManager : nsISupports
+{
+ /* Gets the calICalendarACLEntry of the current user for the specified
+ calendar. */
+ void getCalendarEntry(in calICalendar aCalendar,
+ in calIOperationListener aListener);
+
+ /* Gets the calIItemACLEntry of the current user for the specified
+ calendar item. Depending on the implementation, each item can have
+ different permissions based on specific attributes.
+ (TODO: should be made asynchronous one day) */
+ calIItemACLEntry getItemEntry(in calIItemBase aItem);
+};
+
+[scriptable, uuid(f3da7954-52a4-45a9-bd7d-96c518133d0c)]
+interface calICalendarACLEntry : nsISupports
+{
+ /* The calICalendarACLManager instance that generated this entry. */
+ readonly attribute calICalendarACLManager aclManager;
+
+ /* Whether the underlying calendar does have access control. */
+ readonly attribute boolean hasAccessControl;
+
+ /* Whether the user accessing the calendar is its owner. */
+ readonly attribute boolean userIsOwner;
+
+ /* Whether the user accessing the calendar can add items to it. */
+ readonly attribute boolean userCanAddItems;
+
+ /* Whether the user accessing the calendar can remove items from it. */
+ readonly attribute boolean userCanDeleteItems;
+
+ /* Returns the list of user ids matching the user accessing the
+ calendar. */
+ void getUserAddresses(out uint32_t aCount,
+ [array, size_is(aCount), retval] out wstring aAddresses);
+
+ /* Returns the list of instantiated identities for the user accessing the
+ calendar. */
+ void getUserIdentities(out uint32_t aCount,
+ [array, size_is(aCount), retval] out nsIMsgIdentity aIdentities);
+ /* Returns the list of instantiated identities for the user representing
+ the calendar owner. */
+ void getOwnerIdentities(out uint32_t aCount,
+ [array, size_is(aCount), retval] out nsIMsgIdentity aIdentities);
+
+ /* Helper method that forces a cleanup of any cache and a reload of the
+ current entry.
+ (TODO: should be made asynchronous one day) */
+ void refresh();
+};
+
+[scriptable, uuid(4d0b7ced-8c57-4efa-87e7-8dd5b7481312)]
+interface calIItemACLEntry : nsISupports
+{
+ /* The parent calICalendarACLEntry instance. */
+ readonly attribute calICalendarACLEntry calendarEntry;
+
+ /* Whether the active user can fully modify the item. */
+ readonly attribute boolean userCanModify;
+
+ /* Whether the active user can respond to this item, if it is an invitation. */
+ readonly attribute boolean userCanRespond;
+
+ /* Whether the active user can view all the item properties. */
+ readonly attribute boolean userCanViewAll;
+
+ /* Whether the active user can only see when this item occurs without
+ knowing any details. */
+ readonly attribute boolean userCanViewDateAndTime;
+};
diff --git a/calendar/base/public/calICalendarManager.idl b/calendar/base/public/calICalendarManager.idl
new file mode 100644
index 000000000..ac4908688
--- /dev/null
+++ b/calendar/base/public/calICalendarManager.idl
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calICalendar;
+interface calIObserver;
+interface nsIURI;
+interface nsIVariant;
+
+interface calICalendarManagerObserver;
+
+[scriptable, uuid(fd8a2565-cb0f-4ecc-945d-760d75ab16d8)]
+interface calICalendarManager : nsISupports
+{
+ /**
+ * Gives the number of registered calendars that require network access.
+ */
+ readonly attribute uint32_t networkCalendarCount;
+
+ /***
+ * Gives the number of registered readonly calendars.
+ */
+ readonly attribute uint32_t readOnlyCalendarCount;
+
+ /**
+ * Gives the number of registered calendars
+ */
+ readonly attribute uint32_t calendarCount;
+ /*
+ * create a new calendar
+ * aType is the type ("caldav", "storage", etc)
+ */
+ calICalendar createCalendar(in AUTF8String aType, in nsIURI aURL);
+
+ /* register a newly created calendar with the calendar service */
+ void registerCalendar(in calICalendar aCalendar);
+
+ /* unregister a calendar */
+ void unregisterCalendar(in calICalendar aCalendar);
+
+ /** @deprecated This method has been replaced by ::removeCalendar */
+ void deleteCalendar(in calICalendar aCalendar);
+
+ /** Remove the calendar following the calendar's capabilities.removeModes. */
+ const unsigned short REMOVE_AUTO = 0;
+
+ /** Just unsubscribe from the calendar, do not delete it. */
+ const unsigned short REMOVE_NO_DELETE = 1;
+
+ /** Passing this flag will cause the call to fail if the calendar is registered */
+ const unsigned short REMOVE_NO_UNREGISTER = 2;
+
+ /**
+ * Unregister and delete the calendar from the calendar manager. By default
+ * the calendar will be removed based on the capabilities.removeModes
+ * property of the calendar.
+ *
+ * WARNING: If the calendar supports deletion, the calendar will be
+ * permanently deleted. You can prevent this with the REMOVE_NO_DELETE flag.
+ *
+ * @param aCalendar The calendar to remove.
+ * @param aMode A combination of the above mode flags.
+ */
+ void removeCalendar(in calICalendar aCalendar, [optional] in uint8_t aMode);
+
+ /* get a calendar by its id */
+ calICalendar getCalendarById(in AUTF8String aId);
+
+ /* return a list of all calendars currently registered */
+ void getCalendars(out uint32_t count,
+ [array, size_is(count), retval] out calICalendar aCalendars);
+
+ /** Add an observer for the calendar manager, i.e when calendars are registered */
+ void addObserver(in calICalendarManagerObserver aObserver);
+ /** Remove an observer for the calendar manager */
+ void removeObserver(in calICalendarManagerObserver aObserver);
+
+ /** Add an observer to handle changes to all calendars (even disabled or unchecked ones) */
+ void addCalendarObserver(in calIObserver aObserver);
+ /** Remove an observer to handle changes to all calendars */
+ void removeCalendarObserver(in calIObserver aObserver);
+
+ /* XXX private, don't use:
+ will vanish as soon as providers will directly read/write from moz prefs
+ */
+ nsIVariant getCalendarPref_(in calICalendar aCalendar,
+ in AUTF8String aName);
+ void setCalendarPref_(in calICalendar aCalendar,
+ in nsIVariant aName,
+ in nsIVariant aValue);
+ void deleteCalendarPref_(in calICalendar aCalendar,
+ in AUTF8String aName);
+
+};
+
+/**
+ * Observer to handle actions done by the calendar manager
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
+ */
+[scriptable, uuid(383f36f1-e669-4ca4-be7f-06b43910f44a)]
+interface calICalendarManagerObserver : nsISupports
+{
+ /** Called after the calendar is registered */
+ void onCalendarRegistered(in calICalendar aCalendar);
+
+ /** Called before the unregister actually takes place */
+ void onCalendarUnregistering(in calICalendar aCalendar);
+
+ /** Called before the delete actually takes place */
+ void onCalendarDeleting(in calICalendar aCalendar);
+};
diff --git a/calendar/base/public/calICalendarProvider.idl b/calendar/base/public/calICalendarProvider.idl
new file mode 100644
index 000000000..79567db0c
--- /dev/null
+++ b/calendar/base/public/calICalendarProvider.idl
@@ -0,0 +1,80 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface calICalendar;
+interface nsIVariant;
+interface calIProviderListener;
+
+/**
+ * High-level interface to allow providers to be plugable.
+ */
+[scriptable, uuid(30e22db4-9f13-11d9-80d6-000b7d081f44)]
+interface calICalendarProvider : nsISupports
+{
+ /**
+ * XUL overlay for configuring a calendar of this type.
+ */
+ readonly attribute nsIURI prefChromeOverlay;
+
+ /**
+ * The way to refer to this provider in UI for the end-user
+ * (eg "Shared ICS File").
+ */
+ readonly attribute AUTF8String displayName;
+
+ /**
+ * Create a new empty calendar. This will typically create a new empty
+ * file, and then call getCalendar()
+ *
+ * @param aName the display name of the calendar to be created
+ * @param aURL URL of the calendar to be created.
+ * @param aListener where to call the results back to
+ */
+ void createCalendar(in AUTF8String aName, in nsIURI aURL,
+ in calIProviderListener aListener);
+
+ /**
+ * Delete a calendar. Deletes the actual underlying calendar, which
+ * could be (for example) a file or a calendar on a server
+ *
+ * @param aCalendar the calendar to delete
+ * @param aListener where to call the results back to
+ */
+ void deleteCalendar(in calICalendar aCalendar,
+ in calIProviderListener aListener);
+
+ /**
+ * Get a new calendar object with existing calendar data
+ *
+ * @param aURL URL of the calendar to be created.
+ */
+ calICalendar getCalendar(in nsIURI aURL);
+};
+
+[scriptable, uuid(0eebe99e-a22d-11d9-87a6-000b7d081f44)]
+interface calIProviderListener : nsISupports
+{
+ /**
+ * @param aStatus status code summarizing what happened
+ * @param aDetail not yet fully specified. If aStatus is an error
+ * result, this will probably be an extended error
+ * string (eg one returned by a server).
+ */
+ void onCreateCalendar(in calICalendar aCalendar, in nsresult aStatus,
+ in nsIVariant aDetail);
+
+ /**
+ * @param aStatus status code summarizing what happened
+ * @param aDetail not yet fully specified. If aStatus is an error
+ * result, this will probably be an extended error
+ * string (eg one returned by a server).
+ */
+ void onDeleteCalendar(in calICalendar aCalendar, in nsresult aStatus,
+ in nsIVariant aDetail);
+};
+
diff --git a/calendar/base/public/calICalendarSearchProvider.idl b/calendar/base/public/calICalendarSearchProvider.idl
new file mode 100644
index 000000000..96dc36810
--- /dev/null
+++ b/calendar/base/public/calICalendarSearchProvider.idl
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIOperation;
+interface calIGenericOperationListener;
+
+[scriptable, uuid(306DA1C9-DB54-4ef3-B27E-FEA709F638FF)]
+interface calICalendarSearchProvider : nsISupports
+{
+ /**
+ * Specifies whether the search string should exactly match.
+ */
+ const unsigned long HINT_EXACT_MATCH = 1;
+
+ /* ...possibly more to come... */
+
+ /**
+ * Searches for calendars matching the specified search string.
+ * It's up to the search provider what properties of a calendar
+ * it takes into account for the search. The passed hints serve
+ * for optimization purposes. Callers need to keep in mind that
+ * providers may not be able to implement all of the stated hints
+ * passed, thus are required to filter further if necessary.
+ * Results are notified to the passed listener interface.
+ *
+ * @param aString search string to match
+ * @param aHints search hints
+ * @param aMaxResults maximum number of results
+ * (0 denotes provider specific maximum)
+ * @param aListener called with an array of calICalendar objects
+ * @return optional operation handle to track the operation
+ */
+ calIOperation searchForCalendars(in AUTF8String aString,
+ in unsigned long aHints,
+ in unsigned long aMaxResults,
+ in calIGenericOperationListener aListener);
+};
+
+/**
+ * This service acts as a central access point for calendar lookup.
+ * A search request will be multiplexed to all added search providers.
+ * Adding a search provider is transient.
+ */
+[scriptable, uuid(2F2055CA-F558-4dc8-A1D4-11384A00E85C)]
+interface calICalendarSearchService : calICalendarSearchProvider
+{
+ /**
+ * Gets the currently registered set of search providers.
+ */
+ void getProviders(out uint32_t aCount,
+ [array, size_is(aCount), retval] out calICalendarSearchProvider aProviders);
+
+ /**
+ * Adds a new search provider.
+ */
+ void addProvider(in calICalendarSearchProvider aProvider);
+
+ /**
+ * Removes a search provider.
+ */
+ void removeProvider(in calICalendarSearchProvider aProvider);
+};
diff --git a/calendar/base/public/calICalendarView.idl b/calendar/base/public/calICalendarView.idl
new file mode 100644
index 000000000..3550b4617
--- /dev/null
+++ b/calendar/base/public/calICalendarView.idl
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface calICalendar;
+interface calIDateTime;
+interface calICalendarViewController;
+interface calIItemBase;
+
+/**
+ * An interface for view widgets containing calendaring data.
+ *
+ * @note Code that implements this interface is intended to be pure
+ * widgetry and thus not have any preference dependencies.
+ */
+
+[scriptable, uuid(0e392744-4b2e-4b64-8862-2fb707d900a7)]
+interface calICalendarView : nsISupports
+{
+
+ /**
+ * Oftentimes other elements in the DOM in which a calIDecoratedView is
+ * used want to be aware of whether or not the view is selected. An element
+ * whose ID is observerID can be included in that DOM, and will be set to be
+ * enabled or disabled depending on whether the view is selected.
+ */
+ readonly attribute AUTF8String observerID;
+
+ /**
+ * Generally corresponds to whether or not the view has been previously shown.
+ * Strictly speaking, it reports whether displayCalendar, startDay and endDay
+ * are all non-null.
+ */
+ readonly attribute boolean initialized;
+
+ /**
+ * the calendar that this view is displaying
+ */
+ attribute calICalendar displayCalendar;
+
+ /**
+ * the controller for this view
+ */
+ attribute calICalendarViewController controller;
+
+ /**
+ * If true, the view supports workdays only
+ */
+ readonly attribute boolean supportsWorkdaysOnly;
+
+ /**
+ * If this is set to 'true', the view should not display days specified to be
+ * non-workdays. The implementor is responsible for obtaining what those
+ * days are on its own.
+ */
+ attribute boolean workdaysOnly;
+
+ /**
+ * Whether or not tasks are to be displayed in the calICalendarView
+ */
+ attribute boolean tasksInView;
+
+ /**
+ * If true, the view is rotatable
+ */
+ readonly attribute boolean supportsRotation;
+
+ /**
+ * If set, the view will be rotated (i.e time on top, date at left)
+ */
+ attribute boolean rotated;
+
+ /**
+ * If true, the view is zoomable
+ */
+ readonly attribute boolean supportsZoom;
+
+ /**
+ * Zoom view in one level. Defaults to one level.
+ */
+ void zoomIn([optional] in uint32_t level);
+
+ /**
+ * Zoom view out one level. Defaults to one level.
+ */
+ void zoomOut([optional] in uint32_t level);
+
+ /**
+ * Reset view zoom.
+ */
+ void zoomReset();
+
+ /**
+ * Whether or not completed tasks are shown in the calICalendarView
+ */
+ attribute boolean showCompleted;
+
+ /**
+ * Ensure that the given date is visible; the view is free
+ * to show more dates than the given date (e.g. week view
+ * would show the entire week).
+ */
+ void showDate(in calIDateTime aDate);
+
+ /**
+ * Set a date range for the view to display, from aStartDate
+ * to aEndDate, inclusive.
+ *
+ * Some views may decide to utilize the time portion of these
+ * calIDateTimes; pass in calIDateTimes that are dates if you
+ * want to make sure this doesn't happen.
+ */
+ void setDateRange(in calIDateTime aStartDate, in calIDateTime aEndDate);
+
+ /**
+ * The start date of the view's display. If the view is displaying
+ * disjoint dates, this will be the earliest date that's displayed.
+ */
+ readonly attribute calIDateTime startDate;
+
+ /**
+ * The end date of the view's display. If the view is displaying
+ * disjoint dates, this will be the latest date that's displayed.
+ *
+ * Note that this won't be equivalent to the aEndDate passed to
+ * setDateRange, because that date isn't actually displayed!
+ */
+ readonly attribute calIDateTime endDate;
+
+ /**
+ * The first day shown in the embedded view
+ */
+ readonly attribute calIDateTime startDay;
+
+ /**
+ * The last day shown in the embedded view
+ */
+ readonly attribute calIDateTime endDay;
+
+ /**
+ * True if this view supports disjoint dates
+ */
+ readonly attribute boolean supportsDisjointDates;
+
+ /**
+ * True if this view currently has a disjoint date set.
+ */
+ readonly attribute boolean hasDisjointDates;
+
+ /**
+ * Returns the list of dates being shown by this calendar.
+ * If a date range is set, it will expand out the date range by
+ * day and return the full set.
+ */
+ void getDateList(out unsigned long aCount, [array,size_is(aCount),retval] out calIDateTime aDates);
+
+ /**
+ * Get the items currently selected in this view.
+ *
+ * @param aCount a variable to hold the number of items in this array
+ *
+ * @return the array of items currently selected in this.
+ */
+ void getSelectedItems(out unsigned long aCount,
+ [array,size_is(aCount),retval] out calIItemBase aItems);
+
+ /**
+ * Select an array of items in the view. Items outside the view's current
+ * display range will be ignored.
+ *
+ * @param aCount the number of items to select
+ * @param aItems an array of items to select
+ * @param aSuppressEvent if true, the 'itemselect' event will not be fired.
+ */
+ void setSelectedItems(in unsigned long aCount,
+ [array,size_is(aCount)] in calIItemBase aItems,
+ in boolean aSuppressEvent);
+
+ /**
+ * Make as many of the selected items as possible are visible in the view.
+ */
+ void centerSelectedItems();
+
+ /**
+ * Get or set the selected day.
+ */
+ attribute calIDateTime selectedDay;
+
+ /**
+ * Get or set the timezone that the view's elements should be displayed in.
+ * Setting this does not refresh the view.
+ */
+ attribute AUTF8String timezone;
+
+ /**
+ * Ensures that the given date is visible, and that the view is centered
+ * around this date. aDate becomes the selectedDay of the view. Calling
+ * this function with the current selectedDay effectively refreshes the view
+ *
+ * @param aDate the date that must be shown in the view and becomes
+ * the selected day
+ */
+ void goToDay(in calIDateTime aDate);
+
+ /**
+ * Moves the view a specific number of pages. Negative numbers correspond to
+ * moving the view backwards. Note that it is up to the view to determine
+ * how the selected day ought to move as well.
+ *
+ * @param aNumber the number of pages to move the view
+ */
+ void moveView(in long aNumber);
+
+ /**
+ * gets the description of the range displayed by the view
+ */
+ AString getRangeDescription();
+
+ /**
+ * The type of the view e.g "day", "week", "multiweek" or "month" that refers
+ * to the displayed time period.
+ */
+ readonly attribute string type;
+ /**
+ * removes the dropshadows that are inserted into childelements during a
+ * drag and drop session
+ */
+
+ void removeDropShadows();
+};
diff --git a/calendar/base/public/calICalendarViewController.idl b/calendar/base/public/calICalendarViewController.idl
new file mode 100644
index 000000000..caadb8569
--- /dev/null
+++ b/calendar/base/public/calICalendarViewController.idl
@@ -0,0 +1,71 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface calICalendar;
+interface calIDateTime;
+interface calIEvent;
+interface calIItemBase;
+
+[scriptable, uuid(40430501-a666-4c24-b234-eeac5ccb70f6)]
+interface calICalendarViewController : nsISupports
+{
+ /**
+ * Create an event, with an optional start time and optional end
+ * time in the given Calendar. The Calendar will be the
+ * displayCalendar set on the View which invokes this method
+ * on the controller, or null, if the views wish to delegate the
+ * choice of the calendar to the controller.
+ *
+ * If neither aStartTime or aEndTime are given, the user wants to
+ * create a generic event with no information prefilled.
+ *
+ * If aStartTime is given and is a date, the user wants to
+ * create an all day event, optionally a multi-all-day event if
+ * aEndTime is given (and is also a date).
+ *
+ * If aStartTime is given and is a time, but no aEndTime is
+ * given, the user wants to create an event starting at
+ * aStartTime and of the default duration. The controller has the
+ * option of creating this event automatically or via the dialog.
+ *
+ * If both aStartTime and aEndTime are given as times, then
+ * the user wants to create an event going from aStartTime
+ * to aEndTime.
+ */
+ void createNewEvent (in calICalendar aCalendar,
+ in calIDateTime aStartTime,
+ in calIDateTime aEndTime);
+
+ /**
+ * Modify aOccurrence. If aNewStartTime and aNewEndTime are given,
+ * update the event to those times. If aNewTitle is given, modify the title
+ * of the item. If no parameters are given, ask the user to modify.
+ */
+ void modifyOccurrence (in calIItemBase aOccurrence,
+ in calIDateTime aNewStartTime,
+ in calIDateTime aNewEndTime,
+ in AString aNewTitle);
+ /**
+ * Delete all events in the given array. If more than one event is passed,
+ * this will prompt whether to delete just this occurrence or all occurrences.
+ * All passed events will be handled in one transaction, i.e undoing this will
+ * make all events reappear.
+ *
+ * @param aCount The number of events in the array
+ * @param aOccurrences An array of Items/Occurrences to delete
+ * @param aUseParentItems If set, each occurrence will have its parent item
+ * deleted.
+ * @param aDoNotConfirm If set, the events will be deleted without
+ * confirmation.
+ */
+ void deleteOccurrences (in uint32_t aCount,
+ [array, size_is(aCount)] in calIItemBase aOccurrences,
+ in boolean aUseParentItems,
+ in boolean aDoNotConfirm);
+};
+
diff --git a/calendar/base/public/calIChangeLog.idl b/calendar/base/public/calIChangeLog.idl
new file mode 100644
index 000000000..973fe2c61
--- /dev/null
+++ b/calendar/base/public/calIChangeLog.idl
@@ -0,0 +1,147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "calICalendar.idl"
+
+interface calIGenericOperationListener;
+interface calIOperation;
+
+/**
+ * Interface for managing offline flags in offline storage
+ * (calStorageCalendar), in particular from calICachedCalendar.
+ */
+[scriptable, uuid(36dc2c93-5851-40d2-9ba9-b1f6e682c75c)]
+interface calIOfflineStorage : calICalendar {
+ /**
+ * Mark the item of which the id is passed as parameter as new.
+ *
+ * @param aItem the item to add
+ * @param aListener where to call back the results
+ */
+ void addOfflineItem(in calIItemBase aItem, in calIOperationListener aListener);
+
+ /**
+ * Mark the item of which the id is passed as parameter as modified.
+ *
+ * @param aItem the item to modify
+ * @param aListener where to call back the results
+ */
+ void modifyOfflineItem(in calIItemBase aItem, in calIOperationListener aListener);
+
+ /**
+ * Mark the item of which the id is passed as parameter as deleted.
+ *
+ * @param aItem the item to delete
+ * @param aListener where to call back the results
+ */
+ void deleteOfflineItem(in calIItemBase aItem, in calIOperationListener aListener);
+
+ /**
+ * Retrieves the offline flag for the given item. The flag is returned using the
+ * detail parameter of the onOperationComplete function in calIOperationLIstener.
+ *
+ * @param aItem the item to reset
+ * @param aListener where to call back the results
+ */
+ void getItemOfflineFlag(in calIItemBase aItem, in calIOperationListener aListener);
+
+ /**
+ * Remove any offline flag from the item record.
+ *
+ * @param aItem the item to reset
+ * @param aListener where to call back the results
+ */
+ void resetItemOfflineFlag(in calIItemBase aItem, in calIOperationListener aListener);
+};
+
+/**
+ * Interface for synchronously working providers on storing items,
+ * e.g. storage, memory. All modifying commands return after the
+ * modification has been performed.
+ *
+ * @note
+ * This interface is used in conjunction with changelog-based synchronization
+ * and additionally offers storing meta-data for items for this purpose.
+ * The meta data is stored as long as the corresponding items persist in
+ * the calendar and automatically cleanup up once the item is deleted from
+ * the calendar, but is not altered when an item is modified (modifyItem).
+ * Meta data can be fetched/stored per (master) item, i.e. if you need to
+ * store meta data for individual overridden items, you need to store it
+ * along with the master item's meta data.
+ * Finally, keep in mind that the meta data is "calendar local" and not
+ * automatically transferred when storing the item on another calISyncWriteCalendar.
+ */
+[scriptable, uuid(651e137b-2f3a-4595-af89-da51b6a37f85)]
+interface calISyncWriteCalendar : calICalendar {
+ /**
+ * Adds or replaces meta data of an item.
+ *
+ * @param id an item id
+ * @param value an arbitrary string
+ */
+ void setMetaData(in AUTF8String id,
+ in AUTF8String value);
+
+ /**
+ * Deletes meta data of an item.
+ *
+ * @param id an item id
+ */
+ void deleteMetaData(in AUTF8String id);
+
+ /**
+ * Gets meta data of an item or null if there's none or the item id is invalid.
+ *
+ * @param id an item id
+ */
+ AUTF8String getMetaData(in AUTF8String id);
+
+ /**
+ * Gets all meta data. The returned arrays are of the same length.
+ */
+ void getAllMetaData(out uint32_t count,
+ [array, size_is(count)] out wstring ids,
+ [array, size_is(count)] out wstring values);
+};
+
+/**
+ * Calendar implementing this interface have improved means of replaying their
+ * changelog data. This could for example mean, that the provider can retrieve
+ * changes between now and the last sync.
+ *
+ * Not implementing this interface is perfectly valid for calendars, that need
+ * to do a full sync each time anyway (i.e ics)
+ */
+[scriptable, uuid(0bf4c6a2-b4c7-4cae-993a-4408d8bded3e)]
+interface calIChangeLog : nsISupports {
+
+ // To denote no offline flag, use null
+ const long OFFLINE_FLAG_CREATED_RECORD = 1;
+ const long OFFLINE_FLAG_MODIFIED_RECORD = 2;
+ const long OFFLINE_FLAG_DELETED_RECORD = 4;
+
+ /**
+ * Enable the changelog calendar to retrieve offline data right after instanciation.
+ */
+ attribute calISyncWriteCalendar offlineStorage;
+
+ /**
+ * Resets the changelog. This is used if the cache should be refreshed.
+ */
+ void resetLog();
+
+ /**
+ * Instructs the calendar to replay remote changes into the above offlineStorage
+ * calendar. The calendar itself is responsible for storing anything needed
+ * to keep track of what items need updating.
+ *
+ * TODO: We might reconsider to replay on calICalendar,
+ * but this complicates implementing this interface
+ * enormously for providers.
+ *
+ * @param aDestination The calendar to sync changes into
+ * @param aListener The listener to notify when the operation completes.
+ */
+ calIOperation replayChangesOn(in calIGenericOperationListener aListener);
+};
diff --git a/calendar/base/public/calIDateTime.idl b/calendar/base/public/calIDateTime.idl
new file mode 100644
index 000000000..d6c94052b
--- /dev/null
+++ b/calendar/base/public/calIDateTime.idl
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDuration;
+interface calITimezone;
+
+[scriptable, uuid(fe3e9a58-2938-4b2c-9085-4989d5f7244f)]
+interface calIDateTime : nsISupports
+{
+ /**
+ * isMutable is true if this instance is modifiable.
+ * If isMutable is false, any attempts to modify
+ * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE.
+ */
+ readonly attribute boolean isMutable;
+
+ /**
+ * Make this calIDateTime instance immutable.
+ */
+ void makeImmutable();
+
+ /**
+ * Clone this calIDateTime instance into a new
+ * mutable object.
+ */
+ calIDateTime clone();
+
+ /**
+ * valid is true if this object contains a valid
+ * time/date.
+ */
+ // true if this thing is set/valid
+ readonly attribute boolean isValid;
+
+ /**
+ * nativeTime contains this instance's PRTime value relative
+ * to the UTC epoch, regardless of the timezone that's set
+ * on this instance. If nativeTime is set, the given UTC PRTime
+ * value is exploded into year/month/etc, forcing the timezone
+ * setting to UTC.
+ *
+ * @warning: When the timezone is set to 'floating', this will return
+ * the nativeTime as-if the timezone was UTC. Take this into account
+ * when comparing values.
+ *
+ * @note on objects that are pinned to a timezone and have isDate set,
+ * nativeTime will be 00:00:00 in the timezone of that date, not 00:00:00 in
+ * UTC.
+ */
+ attribute PRTime nativeTime;
+
+ /**
+ * Full 4-digit year value (e.g. "1989", "2004")
+ */
+ attribute short year;
+
+ /**
+ * Month, 0-11, 0 = January
+ */
+ attribute short month;
+
+ /**
+ * Day of month, 1-[28,29,30,31]
+ */
+ attribute short day;
+
+ /**
+ * Hour, 0-23
+ */
+ attribute short hour;
+
+ /**
+ * Minute, 0-59
+ */
+ attribute short minute;
+
+ /**
+ * Second, 0-59
+ */
+ attribute short second;
+
+ /**
+ * Gets or sets the timezone of this calIDateTime instance.
+ * Setting the timezone does not change the actual date/time components;
+ * to convert between timezones, use getInTimezone().
+ *
+ * @throws NS_ERROR_INVALID_ARG if null is passed in.
+ */
+ attribute calITimezone timezone;
+
+ /**
+ * Resets the datetime object.
+ *
+ * @param year full 4-digit year value (e.g. "1989", "2004")
+ * @param month month, 0-11, 0 = January
+ * @param day day of month, 1-[28,29,31]
+ * @param hour hour, 0-23
+ * @param minute minute, 0-59
+ * @param second decond, 0-59
+ * @param timezone timezone
+ *
+ * The passed datetime will be normalized, e.g. a minute value of 60 will
+ * increase the hour.
+ *
+ * @throws NS_ERROR_INVALID_ARG if no timezone is passed in.
+ */
+ void resetTo(in short year,
+ in short month,
+ in short day,
+ in short hour,
+ in short minute,
+ in short second,
+ in calITimezone timezone);
+
+ /**
+ * The offset of the timezone this datetime is in, relative to UTC, in
+ * seconds. A positive number means that the timezone is ahead of UTC.
+ */
+ readonly attribute long timezoneOffset;
+
+ /**
+ * isDate indicates that this calIDateTime instance represents a date
+ * (a whole day), and not a specific time on that day. If isDate is set,
+ * accessing the hour/minute/second fields will return 0, and and setting
+ * them is an illegal operation.
+ */
+ attribute boolean isDate;
+
+ /*
+ * computed values
+ */
+
+ /**
+ * Day of the week. 0-6, with Sunday = 0.
+ */
+ readonly attribute short weekday;
+
+ /**
+ * Day of the year, 1-[365,366].
+ */
+ readonly attribute short yearday;
+
+ /*
+ * Methods
+ */
+
+ /**
+ * Resets this instance to Jan 1, 1970 00:00:00 UTC.
+ */
+ void reset();
+
+ /**
+ * Return a string representation of this instance.
+ */
+ AUTF8String toString();
+
+ /**
+ * Return a new calIDateTime instance that's the result of
+ * converting this one into the given timezone. Valid values
+ * for aTimezone are the same as the timezone field. If
+ * the "floating" timezone is given, then this object
+ * is just cloned, and the timezone is set to floating.
+ */
+ calIDateTime getInTimezone(in calITimezone aTimezone);
+
+ // add the given calIDateTime, treating it as a duration, to
+ // this item.
+ // XXX will change
+ void addDuration (in calIDuration aDuration);
+
+ // Subtract two dates and return a duration
+ // returns duration of this - aOtherDate
+ // if aOtherDate is > this the duration will be negative
+ calIDuration subtractDate (in calIDateTime aOtherDate);
+
+ /**
+ * Compare this calIDateTime instance to aOther. Returns -1, 0, 1 to
+ * indicate if this < aOther, this == aOther, or this > aOther,
+ * respectively.
+ *
+ * This comparison is timezone-aware; the given values are converted
+ * to a common timezone before comparing. If either this or aOther is
+ * floating, both objects are treated as floating for the comparison.
+ *
+ * If either this or aOther has isDate set, then only the date portion is
+ * compared.
+ *
+ * @exception calIErrors.INVALID_TIMEZONE bad timezone on this object
+ * (not the argument object)
+ */
+ long compare (in calIDateTime aOther);
+
+ //
+ // Some helper getters for calculating useful ranges
+ //
+
+ /**
+ * Returns SUNDAY of the given datetime object's week.
+ */
+ readonly attribute calIDateTime startOfWeek;
+
+ /**
+ * Returns SATURDAY of the datetime object's week.
+ */
+ readonly attribute calIDateTime endOfWeek;
+
+ // the start/end of the current object's month
+ readonly attribute calIDateTime startOfMonth;
+ readonly attribute calIDateTime endOfMonth;
+
+ // the start/end of the current object's year
+ readonly attribute calIDateTime startOfYear;
+ readonly attribute calIDateTime endOfYear;
+
+ /**
+ * This object as either an iCalendar DATE or DATETIME string, as
+ * appropriate and sets the timezone to either UTC or floating.
+ */
+ attribute ACString icalString;
+};
+
+/** Libical specific interfaces */
+
+[ptr] native icaltimetypeptr(struct icaltimetype);
+[scriptable, uuid(04139dff-a6f0-446d-9aec-2062df887ef2)]
+interface calIDateTimeLibical : calIDateTime
+{
+ [noscript,notxpcom] void toIcalTime(in icaltimetypeptr itt);
+};
diff --git a/calendar/base/public/calIDateTimeFormatter.idl b/calendar/base/public/calIDateTimeFormatter.idl
new file mode 100644
index 000000000..6812d4df3
--- /dev/null
+++ b/calendar/base/public/calIDateTimeFormatter.idl
@@ -0,0 +1,162 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDateTime;
+interface calIItemBase;
+
+[scriptable, uuid(69741510-5f5d-11e4-9803-0800200c9a66)]
+interface calIDateTimeFormatter : nsISupports
+{
+ /**
+ * Format a date in either short or long format, depending on the
+ * users preference
+ *
+ * @see
+ * formatDateShort
+ * formatDateLong
+ */
+ AString formatDate(in calIDateTime aDate);
+
+ /**
+ * Format a date into a short format, for example
+ * "12/17/2005"
+ *
+ * @param aDate
+ * the datetime to format
+ * @returns
+ * a string representing the date part of the datetime
+ */
+ AString formatDateShort(in calIDateTime aDate);
+
+ /**
+ * Format a date into a long format, for example
+ * "Sat Dec 17 2005"
+ *
+ * @param aDate
+ * the datetime to format
+ * @returns
+ * a string representing the date part of the datetime
+ */
+ AString formatDateLong(in calIDateTime aDate);
+
+ /**
+ * Format a date into a short format without mentioning the year, for
+ * example "Dec 17"
+ *
+ * @param aDate
+ * the datetime to format
+ * @returns
+ * a string representing the date part of the datetime
+ */
+ AString formatDateWithoutYear(in calIDateTime aDate);
+
+ /**
+ * Format a time into the format specified by the OS settings.
+ * Will omit the seconds from the output.
+ *
+ * @param aDate
+ * the datetime to format
+ * @returns
+ * a string representing the time part of the datetime
+ */
+ AString formatTime(in calIDateTime aDate);
+
+ /**
+ * Format a datetime into the format specified by the OS settings.
+ * Will omit the seconds from the output.
+ *
+ * @param aDateTime
+ * the datetime to format
+ * @returns
+ * a string representing the datetime
+ */
+ AString formatDateTime(in calIDateTime aDate);
+
+ /**
+ * Format a time interval that is defined by an item with the default
+ * timezone Internally it calls "formatInterval" after retrieving
+ * the start/entry and end/due date of the item.
+ *
+ * @param aItem
+ * The item describing the interval
+ */
+ AUTF8String formatItemInterval(in calIItemBase aItem);
+
+ /**
+ * Format a time interval like formatItemInterval, but only show times.
+ *
+ * @param aItem The item providing the interval
+ * @return The string describing the interval
+ */
+ AUTF8String formatItemTimeInterval(in calIItemBase aItem);
+
+ /**
+ * Format a date/time interval. The returned string may assume that the
+ * dates are so close to each other, that it can leave out some parts of the
+ * part string denoting the end date.
+ *
+ * @param aStartDate The start of the interval
+ * @param aEndDate The end of the interval
+ * @return A String describing the interval in a legible form
+ */
+ AUTF8String formatInterval(in calIDateTime aStartDate,
+ in calIDateTime aEndDate);
+
+ /**
+ * Format a time interval like formatInterval, but show only the time.
+ *
+ * @param aStartDate The start of the interval.
+ * @param aEndDate The end of the interval.
+ * @return The formatted time interval.
+ */
+ AUTF8String formatTimeInterval(in calIDateTime aStartTime,
+ in calIDateTime aEndTime);
+
+ /**
+ * Get the monthday followed by its ordinal symbol in the current locale.
+ * e.g. monthday 1 -> 1st
+ * monthday 2 -> 2nd etc.
+ *
+ * @param aMonthdayIndex
+ * a number from 1 to 31
+ * @returns
+ * the monthday number in ordinal format in the current locale
+ */
+ AUTF8String formatDayWithOrdinal(in unsigned long aMonthdayIndex);
+
+ /**
+ * Get the month name
+ *
+ * @param aMonthIndex
+ * zero-based month number (0 is january, 11 is december)
+ * @returns
+ * the month name in the current locale
+ */
+ AString monthName(in unsigned long aMonthIndex);
+
+ /**
+ * Get the abbrevation of the month name
+ *
+ * @see monthName
+ */
+ AString shortMonthName(in unsigned long aMonthIndex);
+
+ /**
+ * Get the day name
+ * @param aMonthIndex
+ * zero-based month number (0 is sunday, 6 is saturday)
+ * @returns
+ * the day name in the current locale
+ */
+ AString dayName(in unsigned long aDayIndex);
+
+ /**
+ * Get the abbrevation of the day name
+ * @see dayName
+ */
+ AString shortDayName(in unsigned long aDayIndex);
+};
diff --git a/calendar/base/public/calIDecoratedView.idl b/calendar/base/public/calIDecoratedView.idl
new file mode 100644
index 000000000..2aae7584f
--- /dev/null
+++ b/calendar/base/public/calIDecoratedView.idl
@@ -0,0 +1,140 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface calICalendar;
+interface calIDateTime;
+interface calICalendarViewController;
+interface calIItemBase;
+/**
+ * calIDecoratedView is an interface for modifying/extending the standard
+ * calICalendarView, typically to add specific navigation functions while
+ * removing the unnecessary code duplication this would generally require.
+ * Because it contains a calICalendarView as an anonymous node, not easily
+ * accessible, it must therefore expose several of that interface's methods and
+ * attributes, as well as its own.
+ *
+ * @note Implementations of this interface are intended to be the home
+ * for view preference handling. The lower-level views (ie
+ * implementers of calICalendarView) are intended to be pure widgetry,
+ * and this sort of view should be pluggable, meaning that callers
+ * shouldn't need to know about view-specific preferences.
+ */
+
+[scriptable, uuid(7ba617eb-f19b-400b-9a7d-4156b7c6f028)]
+interface calIDecoratedView : nsISupports
+{
+ /**
+ * Oftentimes other elements in the DOM in which a calIDecoratedView is
+ * used want to be aware of whether or not the view is selected. An element
+ * whose ID is observerID can be included in that DOM, and will be set to be
+ * enabled or disabled depending on whether the view is selected.
+ */
+ readonly attribute AUTF8String observerID;
+
+ /**
+ * Generally corresponds to whether or not the view has been previously shown.
+ * Strictly speaking, it reports whether displayCalendar, startDay and endDay
+ * are all non-null.
+ */
+ readonly attribute boolean initialized;
+
+ /**
+ * The displayCalendar of the embedded calICalendarView. This *must* be set
+ * prior to calling goToDay the first time.
+ */
+ attribute calICalendar displayCalendar;
+
+ /**
+ * The controller of the calICalendarView that is embedded
+ */
+ attribute calICalendarViewController controller;
+
+ /**
+ * If this is set to 'true', the view should not display days specified to be
+ * non-workdays. The implementor is responsible for obtaining what those
+ * days are on its own.
+ */
+ attribute boolean workdaysOnly;
+
+ /**
+ * Whether or not tasks are to be displayed in the calICalendarView
+ */
+ attribute boolean tasksInView;
+
+ /**
+ * If set, the view will be rotated (i.e time on top, date at left)
+ */
+ attribute boolean rotated;
+
+ /**
+ * Whether or not completed tasks are shown in the calICalendarView
+ */
+ attribute boolean showCompleted;
+
+ /**
+ * See calICalendarView.idl for the description of these functions.
+ */
+ void getSelectedItems(out unsigned long aCount,
+ [array,size_is(aCount),retval] out calIItemBase aItems);
+ void setSelectedItems(in unsigned long aCount,
+ [array,size_is(aCount)] in calIItemBase aItems,
+ in boolean aSuppressEvent);
+
+ /**
+ * The selectedDay in the embedded view. Use the goToDay function to set a
+ * particular day to be selected.
+ */
+ readonly attribute calIDateTime selectedDay;
+
+ /**
+ * The first day shown in the embedded view
+ */
+ readonly attribute calIDateTime startDay;
+
+ /**
+ * The last day shown in the embedded view
+ */
+ readonly attribute calIDateTime endDay;
+
+ /**
+ * Get or set the timezone that the view's elements should be displayed in.
+ * Setting this does not refresh the view.
+ */
+ attribute AUTF8String timezone;
+
+ /**
+ * Ensures that the given date is visible, and that the view is centered
+ * around this date. aDate becomes the selectedDay of the view. Calling
+ * this function with the current selectedDay effectively refreshes the view
+ *
+ * @param aDate the date that must be shown in the view and becomes
+ * the selected day
+ */
+ void goToDay(in calIDateTime aDate);
+
+ /**
+ * Moves the view a specific number of pages. Negative numbers correspond to
+ * moving the view backwards. Note that it is up to the view to determine
+ * how the selected day ought to move as well.
+ *
+ * @param aNumber the number of pages to move the view
+ */
+ void moveView(in long aNumber);
+
+ /**
+ * gets the description of the range displayed by the view
+ */
+ AString getRangeDescription();
+
+ /**
+ * The type of the view e.g "day", "week", "multiweek" or "month" that refers
+ * to the displayed time period.
+ */
+ readonly attribute string type;
+
+};
diff --git a/calendar/base/public/calIDeletedItems.idl b/calendar/base/public/calIDeletedItems.idl
new file mode 100644
index 000000000..8045fa24b
--- /dev/null
+++ b/calendar/base/public/calIDeletedItems.idl
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDateTime;
+
+[scriptable, uuid(2414729b-37dc-456e-ba72-f9c33891e6ee)]
+interface calIDeletedItems : nsISupports
+{
+ /**
+ * Clean the database of all deleted items older than an internal threshold.
+ */
+ void flush();
+
+ /**
+ * Gets the time the item with given id was deleted at. If passed, the
+ * search will be restricted to a certain calendar
+ *
+ * @param aId The ID of the item to search for.
+ * @param aCalId The calendar id to restrict the search to.
+ * @return The date/time the item was deleted, or null if not found.
+ */
+ calIDateTime getDeletedDate(in AUTF8String aId, [optional] in AUTF8String aCalId);
+};
diff --git a/calendar/base/public/calIDuration.idl b/calendar/base/public/calIDuration.idl
new file mode 100644
index 000000000..993a82c19
--- /dev/null
+++ b/calendar/base/public/calIDuration.idl
@@ -0,0 +1,113 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(78537f21-fd5c-4e02-ab26-8ff6a3d946cb)]
+interface calIDuration : nsISupports
+{
+ /**
+ * isMutable is true if this instance is modifiable.
+ * If isMutable is false, any attempts to modify
+ * the object will throw CAL_ERROR_ITEM_IS_MUTABLE.
+ */
+ readonly attribute boolean isMutable;
+
+ /**
+ * Make this calIDuration instance immutable.
+ */
+ void makeImmutable();
+
+ /**
+ * Clone this calIDuration instance into a new
+ * mutable object.
+ */
+ calIDuration clone();
+
+ /**
+ * Is Negative
+ */
+ attribute boolean isNegative;
+
+ /**
+ * Weeks
+ */
+ attribute short weeks;
+
+ /**
+ * Days
+ */
+ attribute short days;
+
+ /**
+ * Hours
+ */
+ attribute short hours;
+
+ /**
+ * Minutes
+ */
+ attribute short minutes;
+
+ /**
+ * Seconds
+ */
+ attribute short seconds;
+
+ /**
+ * total duration in seconds
+ */
+ attribute long inSeconds;
+
+ /*
+ * Methods
+ */
+
+ /**
+ * Add a duration
+ */
+ void addDuration(in calIDuration aDuration);
+
+ /**
+ * Compare with another duration
+ *
+ * @param aOther to be compared with this object
+ *
+ * @return -1, 0, 1 if this < aOther, this == aOther, or this > aOther,
+ * respectively.
+ */
+ long compare(in calIDuration aOther);
+
+ /**
+ * Reset this duration to 0
+ */
+ void reset();
+
+ /**
+ * Normalize the duration
+ */
+ void normalize();
+
+ /**
+ * Return a string representation of this instance.
+ */
+ AUTF8String toString();
+
+ attribute jsval icalDuration;
+
+ /**
+ * This object as an iCalendar DURATION string
+ */
+ attribute ACString icalString;
+};
+
+/** Libical specific interfaces */
+
+[ptr] native icaldurationtypeptr(struct icaldurationtype);
+[scriptable, uuid(f5e1c987-e722-4dec-bf91-93d4062b504a)]
+interface calIDurationLibical : calIDuration
+{
+ [noscript,notxpcom] void toIcalDuration(in icaldurationtypeptr idt);
+};
diff --git a/calendar/base/public/calIErrors.idl b/calendar/base/public/calIErrors.idl
new file mode 100644
index 000000000..238322bf0
--- /dev/null
+++ b/calendar/base/public/calIErrors.idl
@@ -0,0 +1,118 @@
+/* -*- Mode: IDL; tab-width: 50; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(404c7d78-bec7-474c-aa2a-82c0d0563bb6)]
+interface calIErrors : nsISupports
+{
+ /**
+ * The first two constants are copied from nsError.h, but named slightly
+ * differently, because if they're named the same, the names collide and
+ * the compiler can't deal.
+ */
+ const unsigned long CAL_ERROR_MODULE_CALENDAR = 5;
+ const unsigned long CAL_ERROR_MODULE_BASE_OFFSET = 0x45;
+
+ /**
+ * The beginning of this set of error codes, also copied from the macros
+ * in nsError.h.
+ */
+ const unsigned long ERROR_BASE = (1<<31) |
+ (CAL_ERROR_MODULE_CALENDAR + CAL_ERROR_MODULE_BASE_OFFSET) << 16;
+
+ /* Onto the actual errors! */
+
+ /**
+ * An invalid or nonexistent timezone was encountered.
+ */
+ const unsigned long INVALID_TIMEZONE = ERROR_BASE + 1;
+
+ /**
+ * Attempted to modify a readOnly calendar.
+ */
+ const unsigned long CAL_IS_READONLY = ERROR_BASE + 2;
+
+ /**
+ * Error while decoding an (ics) file from utf8
+ */
+ const unsigned long CAL_UTF8_DECODING_FAILED = ERROR_BASE + 3;
+
+ /**
+ * Tried to add an item to a calendar in which an item with the
+ * same ID already existed
+ */
+ const unsigned long DUPLICATE_ID = ERROR_BASE + 4;
+
+ /**
+ * Operation has been cancelled.
+ */
+ const unsigned long OPERATION_CANCELLED = ERROR_BASE + 5;
+
+ /**
+ * Creation of calendar object failed
+ */
+ const unsigned long PROVIDER_CREATION_FAILED = ERROR_BASE + 6;
+
+ /**
+ * Profile data has newer schema than we know in this calendar version.
+ */
+ const unsigned long STORAGE_UNKNOWN_SCHEMA_ERROR = ERROR_BASE + 7;
+
+ /**
+ * Profile data may refer to newer timezones than we know.
+ */
+ const unsigned long STORAGE_UNKNOWN_TIMEZONES_ERROR = ERROR_BASE + 8;
+
+ /**
+ * The calendar could not be accessed for reading.
+ */
+ const unsigned long READ_FAILED = ERROR_BASE + 9;
+
+ /**
+ * The calendar could not be accessed for modification.
+ */
+ const unsigned long MODIFICATION_FAILED = ERROR_BASE + 10;
+
+ /* ICS specific errors */
+ const unsigned long ICS_ERROR_BASE = ERROR_BASE + 0x100;
+
+ /**
+ * ICS errors, copied from icalerror.h.
+ * The numbers (minus ICS_ERROR_BASE) should match with the enum
+ * values from icalerror.h
+ */
+ const unsigned long ICS_NO_ERROR = ICS_ERROR_BASE + 0;
+ const unsigned long ICS_BADARG = ICS_ERROR_BASE + 1;
+ const unsigned long ICS_NEWFAILED = ICS_ERROR_BASE + 2;
+ const unsigned long ICS_ALLOCATION = ICS_ERROR_BASE + 3;
+ const unsigned long ICS_MALFORMEDDATA = ICS_ERROR_BASE + 4;
+ const unsigned long ICS_PARSE = ICS_ERROR_BASE + 5;
+ const unsigned long ICS_INTERNAL = ICS_ERROR_BASE + 6;
+ const unsigned long ICS_FILE = ICS_ERROR_BASE + 7;
+ const unsigned long ICS_USAGE = ICS_ERROR_BASE + 8;
+ const unsigned long ICS_UNIMPLEMENTED = ICS_ERROR_BASE + 9;
+ const unsigned long ICS_UNKNOWN = ICS_ERROR_BASE + 10;
+
+ /**
+ * WCAP specific errors, defined in
+ * calendar/providers/wcap/public/calIWcapErrors.idl
+ * Range claimed is [ERROR_BASE + 0x200, ERROR_BASE + 0x300)
+ */
+ const unsigned long WCAP_ERROR_BASE = ERROR_BASE + 0x200;
+
+ /**
+ * (Cal)DAV specific errors
+ * Range is [ERROR_BASE + 0x301, ERROR_BASE + 0x399]
+ */
+ const unsigned long DAV_ERROR_BASE = ERROR_BASE + 0x301;
+ const unsigned long DAV_NOT_DAV = DAV_ERROR_BASE + 0;
+ const unsigned long DAV_DAV_NOT_CALDAV = DAV_ERROR_BASE + 1;
+ const unsigned long DAV_NO_PROPS = DAV_ERROR_BASE + 2;
+ const unsigned long DAV_PUT_ERROR = DAV_ERROR_BASE + 3;
+ const unsigned long DAV_REMOVE_ERROR = DAV_ERROR_BASE + 4;
+ const unsigned long DAV_REPORT_ERROR = DAV_ERROR_BASE + 5;
+};
+
diff --git a/calendar/base/public/calIEvent.idl b/calendar/base/public/calIEvent.idl
new file mode 100644
index 000000000..d9bbd5ef8
--- /dev/null
+++ b/calendar/base/public/calIEvent.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "calIItemBase.idl"
+
+interface calIDuration;
+
+//
+// calIEvent
+//
+// An interface for an event (analogous to a VEVENT)
+//
+
+[scriptable, uuid(5ab15c1c-e295-4d8e-a9a9-ba5bc848b59a)]
+interface calIEvent : calIItemBase
+{
+ // these attributes are marked readonly, as the calIDates are owned
+ // by the event; however, the actual calIDate objects are not read
+ // only and are intended to be manipulated to adjust dates.
+
+ /**
+ * The (inclusive) start of the event.
+ */
+ attribute calIDateTime startDate;
+
+ /**
+ * The (non-inclusive) end of the event.
+ * Note that for all-day events, non-inclusive means that this will be set
+ * to the day after the last day of the event.
+ * If startDate.isDate is set, endDate.isDate must also be set.
+ */
+ attribute calIDateTime endDate;
+
+ /**
+ * The duration of the event.
+ * equal to endDate - startDate
+ */
+ readonly attribute calIDuration duration;
+
+};
diff --git a/calendar/base/public/calIFreeBusyProvider.idl b/calendar/base/public/calIFreeBusyProvider.idl
new file mode 100644
index 000000000..5215f5499
--- /dev/null
+++ b/calendar/base/public/calIFreeBusyProvider.idl
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDateTime;
+interface calIPeriod;
+interface calIOperation;
+interface calIGenericOperationListener;
+
+[scriptable, uuid(EB24424C-DD22-4306-9379-FA098C61F5AF)]
+interface calIFreeBusyProvider : nsISupports
+{
+ /**
+ * Gets free/busy intervals.
+ * Results are notified to the passed listener interface.
+ *
+ * @param aCalId calid or MAILTO:rfc822addr
+ * @param aRangeStart start time of free-busy search
+ * @param aRangeEnd end time of free-busy search
+ * @param aBusyTypes what free-busy intervals should be returned
+ * @param aListener called with an array of calIFreeBusyInterval objects
+ * @return optional operation handle to track the operation
+ */
+ calIOperation getFreeBusyIntervals(in AUTF8String aCalId,
+ in calIDateTime aRangeStart,
+ in calIDateTime aRangeEnd,
+ in unsigned long aBusyTypes,
+ in calIGenericOperationListener aListener);
+};
+
+/**
+ * This interface reflects a free or busy interval in time.
+ * Referring to RFC 2445, section 4.2.9, for the different types.
+ */
+[scriptable, uuid(CCBEAF5E-DB87-4bc9-8BB7-24754B76BCB5)]
+interface calIFreeBusyInterval : nsISupports
+{
+ /**
+ * The calId this free-busy period belongs to.
+ */
+ readonly attribute AUTF8String calId;
+
+ /**
+ * The free-busy time interval.
+ */
+ readonly attribute calIPeriod interval;
+
+ /**
+ * The value UNKNOWN indicates that the free-busy information for the time interval is
+ * not known.
+ */
+ const unsigned long UNKNOWN = 0;
+
+ /**
+ * The value FREE indicates that the time interval is free for scheduling.
+ */
+ const unsigned long FREE = 1;
+
+ /**
+ * The value BUSY indicates that the time interval is busy because one
+ * or more events have been scheduled for that interval.
+ */
+ const unsigned long BUSY = 1 << 1;
+
+ /**
+ * The value BUSY_UNAVAILABLE indicates that the time interval is busy
+ * and that the interval can not be scheduled.
+ */
+ const unsigned long BUSY_UNAVAILABLE = 1 << 2;
+
+ /**
+ * The value BUSY_TENTATIVE indicates that the time interval is busy because
+ * one or more events have been tentatively scheduled for that interval.
+ */
+ const unsigned long BUSY_TENTATIVE = 1 << 3;
+
+ /**
+ * All BUSY* states.
+ */
+ const unsigned long BUSY_ALL = (BUSY |
+ BUSY_UNAVAILABLE |
+ BUSY_TENTATIVE);
+
+ /**
+ * One of the above types.
+ */
+ readonly attribute unsigned long freeBusyType;
+};
+
+/**
+ * This service acts as a central access point for free-busy lookup.
+ * A free-busy request will be multiplexed to all added free-busy providers.
+ * Adding a free-busy provider is transient.
+ */
+[scriptable, uuid(BE1796CF-CB53-482e-8942-D6CAA0A11BAA)]
+interface calIFreeBusyService : calIFreeBusyProvider
+{
+ /**
+ * Adds a new free-busy provider.
+ */
+ void addProvider(in calIFreeBusyProvider aProvider);
+
+ /**
+ * Removes a free-busy provider.
+ */
+ void removeProvider(in calIFreeBusyProvider aProvider);
+};
diff --git a/calendar/base/public/calIICSService.idl b/calendar/base/public/calIICSService.idl
new file mode 100644
index 000000000..454d1a4fc
--- /dev/null
+++ b/calendar/base/public/calIICSService.idl
@@ -0,0 +1,280 @@
+/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// XXX use strings for kind values instead of enumerated constants?
+
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+interface calIDuration;
+interface calITimezone;
+interface calITimezoneProvider;
+
+interface calIIcalProperty;
+interface nsIUTF8StringEnumerator;
+interface nsIInputStream;
+
+/**
+ * General notes:
+ *
+ * As with libical, use of getNextFoo(footype) is only valid if there have been
+ * no intervening getNextFoo(otherfootype)s, or removeFoo()s, or addFoo()s. In
+ * general, you want to do as little manipulation of your FooContainers as
+ * possible while iterating over them.
+ */
+[scriptable,uuid(59132cf2-e48c-4807-ab53-779f414a7fbc)]
+interface calIIcalComponent : nsISupports
+{
+ /**
+ * The parent ical property
+ */
+ readonly attribute calIIcalComponent parent;
+
+ /**
+ * Access to the inner ical.js objects. Only use these if you know what you
+ * are doing.
+ */
+ attribute jsval icalComponent;
+ attribute jsval icalTimezone;
+
+ /**
+ * This is the value that an integer-valued getter will provide if
+ * there is no such property on the wrapped ical structure.
+ */
+ const int32_t INVALID_VALUE = -1;
+
+ /**
+ * @param kind ANY, XROOT, VCALENDAR, VEVENT, etc.
+ */
+ calIIcalComponent getFirstSubcomponent(in AUTF8String componentType);
+ calIIcalComponent getNextSubcomponent(in AUTF8String componentType);
+
+ readonly attribute AUTF8String componentType;
+
+ attribute AUTF8String uid;
+ attribute AUTF8String prodid;
+ attribute AUTF8String version;
+
+ /**
+ * PUBLISH, REQUEST, REPLY, etc.
+ */
+ attribute AUTF8String method;
+
+ /**
+ * TENTATIVE, CONFIRMED, CANCELLED, etc.
+ */
+ attribute AUTF8String status;
+
+ attribute AUTF8String summary;
+ attribute AUTF8String description;
+ attribute AUTF8String location;
+ attribute AUTF8String categories;
+ attribute AUTF8String URL;
+
+ attribute int32_t priority;
+
+ attribute calIDateTime startTime;
+ attribute calIDateTime endTime;
+ readonly attribute calIDuration duration;
+ attribute calIDateTime dueTime;
+ attribute calIDateTime stampTime;
+
+ attribute calIDateTime createdTime;
+ attribute calIDateTime completedTime;
+ attribute calIDateTime lastModified;
+
+ /**
+ * The recurrence ID, a.k.a. DTSTART-of-calculated-occurrence,
+ * or null if this isn't an occurrence.
+ */
+ attribute calIDateTime recurrenceId;
+
+ AUTF8String serializeToICS();
+
+ /**
+ * Return a string representation of this instance.
+ */
+ AUTF8String toString();
+
+ /**
+ * Serializes this component (and subcomponents) directly to an
+ * input stream. Typically used for performance to avoid
+ * unnecessary conversions and XPConnect traversals.
+ *
+ * @result an input stream which can be read to get the serialized
+ * version of this component, encoded in UTF-8. Implements
+ * nsISeekableStream so that it can be used with
+ * nsIUploadChannel.
+ */
+ nsIInputStream serializeToICSStream();
+
+ void addSubcomponent(in calIIcalComponent comp);
+// If you add then remove a property/component, the referenced
+// timezones won't get purged out. There's currently no client code.
+// void removeSubcomponent(in calIIcalComponent comp);
+
+ /**
+ * @param kind ANY, ATTENDEE, X-WHATEVER, etc.
+ */
+ calIIcalProperty getFirstProperty(in AUTF8String kind);
+ calIIcalProperty getNextProperty(in AUTF8String kind);
+ void addProperty(in calIIcalProperty prop);
+// If you add then remove a property/component, the referenced
+// timezones won't get purged out. There's currently no client code.
+// void removeProperty(in calIIcalProperty prop);
+
+ /**
+ * Timezones need special handling, as they must be
+ * emitted as children of VCALENDAR, but can be referenced by
+ * any sub component.
+ * Adding a second timezone (of the same TZID) will remove the
+ * first one.
+ */
+ void addTimezoneReference(in calITimezone aTimezone);
+
+ /**
+ * Returns an array of VTIMEZONE components.
+ * These are the timezones that are in use by this
+ * component and its children.
+ */
+ void getReferencedTimezones(out uint32_t aCount,
+ [array,size_is(aCount),retval] out calITimezone aTimezones);
+
+ /**
+ * Clones the component. The cloned component is decoupled from any parent.
+ * @return cloned component
+ */
+ calIIcalComponent clone();
+};
+
+[scriptable,uuid(5b13a69c-53d3-44a0-9203-f89f7e5e1604)]
+interface calIIcalProperty : nsISupports
+{
+ /**
+ * The whole property as an ical string.
+ * @exception Any libical error will be thrown as an calIError::ICS_ error.
+ */
+ readonly attribute AUTF8String icalString;
+
+ /**
+ * Access to the inner ical.js objects. Only use these if you know what you
+ * are doing.
+ */
+ attribute jsval icalProperty;
+
+ /**
+ * The parent component containing this property
+ */
+ readonly attribute calIIcalComponent parent;
+
+ /**
+ * Return a string representation of this instance.
+ */
+ AUTF8String toString();
+
+ /**
+ * The value of the property as string.
+ * The exception for properties of TEXT or X- type, those will be unescaped
+ * when getting, and also expects an unescaped string when setting.
+ * Datetime, numeric and other non-text types are represented as ical string
+ */
+ attribute AUTF8String value;
+
+ /**
+ * The value of the property in (escaped) ical format.
+ */
+ attribute AUTF8String valueAsIcalString;
+
+ /**
+ * The value of the property as date/datetime value, keeping
+ * track of the used timezone referenced in the owning component.
+ */
+ attribute calIDateTime valueAsDatetime;
+
+ // XXX attribute AUTF8String stringValueWithParams; ?
+ readonly attribute AUTF8String propertyName;
+
+ AUTF8String getParameter(in AUTF8String paramname);
+ void setParameter(in AUTF8String paramname, in AUTF8String paramval);
+
+ AUTF8String getFirstParameterName();
+ AUTF8String getNextParameterName();
+
+ void removeParameter(in AUTF8String paramname);
+ void clearXParameters();
+};
+
+[scriptable,uuid(eda9565f-f9bb-4846-b134-1e0653b2e767)]
+interface calIIcsComponentParsingListener : nsISupports
+{
+ /**
+ * Called when the parsing has completed.
+ *
+ * @param rc The result code of parsing
+ * @param rootComp The root ical component that was parsed
+ */
+ void onParsingComplete(in nsresult rc, in calIIcalComponent rootComp);
+};
+
+[scriptable,uuid(31e7636b-5a64-4d15-bc60-67b67cd85176)]
+interface calIICSService : nsISupports
+{
+ /**
+ * Parses an ICS string and uses the passed tzProvider instance to
+ * resolve timezones not contained withing the VCALENDAR.
+ *
+ * @param serialized an ICS string
+ * @param tzProvider timezone provider used to resolve TZIDs
+ * not contained within the VCALENDAR;
+ * if null is passed, parsing falls back to
+ * using the timezone service
+ */
+ calIIcalComponent parseICS(in AUTF8String serialized,
+ in calITimezoneProvider tzProvider);
+
+ /**
+ * Asynchronously parse an ICS string
+ *
+ * @param serialized an ICS string
+ * @param tzProvider timezone provider used to resolve TZIDs
+ * not contained within the VCALENDAR;
+ * if null is passed, parsing falls back to
+ * using the timezone service
+ * @param listener The listener that notifies the root component
+ */
+ void parseICSAsync(in AUTF8String serialized,
+ in calITimezoneProvider tzProvider,
+ in calIIcsComponentParsingListener listener);
+
+ calIIcalComponent createIcalComponent(in AUTF8String kind);
+ calIIcalProperty createIcalProperty(in AUTF8String kind);
+ calIIcalProperty createIcalPropertyFromString(in AUTF8String str);
+ /* I wish I could write this function atop libical!
+ boolean isLegalParameterValue(in AUTF8String paramKind,
+ in AUTF8String paramValue);
+ */
+};
+
+/** Libical specific interfaces */
+
+[ptr] native icalpropertyptr(struct icalproperty_impl);
+[ptr] native icalcomponentptr(struct icalcomponent_impl);
+[ptr] native icaltimezoneptr(struct _icaltimezone);
+
+[scriptable,uuid(d2fc0264-191e-435e-8ef2-b2ab1fa81ca9)]
+interface calIIcalComponentLibical : calIIcalComponent
+{
+ [noscript,notxpcom] icalcomponentptr getLibicalComponent();
+ [noscript,notxpcom] icaltimezoneptr getLibicalTimezone();
+};
+
+[scriptable,uuid(e0b9067f-0a53-4724-9c69-63599681877e)]
+interface calIIcalPropertyLibical : calIIcalProperty
+{
+ [noscript,notxpcom] icalpropertyptr getLibicalProperty();
+ [noscript,notxpcom] icalcomponentptr getLibicalComponent();
+};
diff --git a/calendar/base/public/calIIcsParser.idl b/calendar/base/public/calIIcsParser.idl
new file mode 100644
index 000000000..dd2c3ed7d
--- /dev/null
+++ b/calendar/base/public/calIIcsParser.idl
@@ -0,0 +1,112 @@
+/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIIcalProperty;
+interface calIIcalComponent;
+interface calIItemBase;
+interface nsIInputStream;
+interface calITimezoneProvider;
+interface calIIcsParser;
+
+/**
+ * Listener being called once asynchronous parsing is done.
+ */
+[scriptable, uuid(d22527da-b0e2-41b7-b6f4-ee9c243cd285)]
+interface calIIcsParsingListener : nsISupports
+{
+ void onParsingComplete(in nsresult rc, in calIIcsParser parser);
+};
+
+/**
+ * An interface for parsing an ics string or stream into its items.
+ * Note that this is not a service. A new instance must be created for every new
+ * string or stream to be parsed.
+ */
+[scriptable, uuid(83e9befe-5e9e-49de-8bc2-d882f464f7e7)]
+interface calIIcsParser : nsISupports
+{
+ /**
+ * Parse an ics string into its items, and store top-level properties and
+ * components that are not interpreted.
+ *
+ * @param aICSString
+ * The ICS string to parse
+ * @param optional aTzProvider
+ * The timezone provider used to resolve timezones not contained in the
+ * parent VCALENDAR or null (falls back to timezone service)
+ * @param optional aAsyncParsing
+ * If non-null, parsing will be performed on a worker thread,
+ * and the passed listener is called when it's done
+ */
+ void parseString(in AString aICSString,
+ [optional] in calITimezoneProvider aTzProvider,
+ [optional] in calIIcsParsingListener aAsyncParsing);
+
+ /**
+ * Parse an input stream.
+ *
+ * @see parseString
+ * @param aICSString
+ * The stream to parse
+ * @param optional aTzProvider
+ * The timezone provider used to resolve timezones not contained in the
+ * parent VCALENDAR or null (falls back to timezone service)
+ * @param optional aAsyncParsing
+ * If non-null, parsing will be performed on a worker thread,
+ * and the passed listener is called when it's done
+ */
+ void parseFromStream(in nsIInputStream aStream,
+ [optional] in calITimezoneProvider aTzProvider,
+ [optional] in calIIcsParsingListener aAsyncParsing);
+
+ /**
+ * Get the items that were in the string or stream. In case an item represents a
+ * recurring series, the (unexpanded) parent item is returned only.
+ * Please keep in mind that any parentless items (see below) are not contained
+ * in the returned set of items.
+ *
+ * @param aCount
+ * Will hold the number of items that were parsed
+ * @param aItems
+ * The items
+ */
+ void getItems(out uint32_t aCount,
+ [array,size_is(aCount),retval] out calIItemBase aItems);
+
+ /**
+ * Get the parentless items that may have occurred, i.e. overridden items of a
+ * recurring series (having a RECURRENCE-ID) missing their parent item in the
+ * parsed content.
+ *
+ * @param aCount
+ * Will hold the number of items that were parsed
+ * @param aItems
+ * The items
+ */
+ void getParentlessItems(out uint32_t aCount,
+ [array,size_is(aCount),retval] out calIItemBase aItems);
+
+ /**
+ * Get the top-level properties that were not interpreted as anything special
+ * @param aCount
+ * Will hold the number of properties that were found
+ * @param aProperties
+ * The properties
+ */
+ void getProperties(out uint32_t aCount,
+ [array,size_is(aCount),retval] out calIIcalProperty aProperties);
+
+ /**
+ * Get the top-level components that were not interpreted as anything special
+ * @param aCount
+ * Will hold the number of components that were found
+ * @param aComponents
+ * The components
+ */
+ void getComponents(out uint32_t aCount,
+ [array,size_is(aCount),retval] out calIIcalComponent aComponents);
+};
diff --git a/calendar/base/public/calIIcsSerializer.idl b/calendar/base/public/calIIcsSerializer.idl
new file mode 100644
index 000000000..73c33d929
--- /dev/null
+++ b/calendar/base/public/calIIcsSerializer.idl
@@ -0,0 +1,76 @@
+/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIIcalProperty;
+interface calIIcalComponent;
+interface calIItemBase;
+interface nsIOutputStream;
+interface nsIInputStream;
+
+/**
+ * An interface for serializing calendar items into an ICS string.
+ * Note that this is not a service. A new instance must be created for every new
+ * set of items to be serialized.
+ */
+[scriptable, uuid(4dcf6b4e-7322-4a61-a191-8d8cc1aea42e)]
+interface calIIcsSerializer : nsISupports
+{
+ /**
+ * Add some items to the items that are to be serialized. Can be called
+ * multiple times, and appends to the set on every call.
+ *
+ * @param aItems
+ * The items to be added
+ * @param aCount
+ * The number of items to add
+ */
+ void addItems([array, size_is(aCount)] in calIItemBase aItems,
+ in unsigned long aCount);
+
+ /**
+ * Add a property to the top-level properties to be added on serializing. Can
+ * be called multiple times, and appends to the set on every call.
+ *
+ * @param aProperty
+ * The property to be added
+ */
+ void addProperty(in calIIcalProperty aProperty);
+
+ /**
+ * Add a component to the top-level components to be added on serializing. Can
+ * be called multiple times, and appends to the set on every call.
+ *
+ * @param aComponent
+ * The component to be added
+ */
+ void addComponent(in calIIcalComponent aComponent);
+
+ /**
+ * Serialize the added items, properties and components into an ICS string
+ *
+ * @returns
+ * A string containing the serialized items, properties and components.
+ */
+ AString serializeToString();
+
+ /**
+ * Serialize the added items, properties and components into an ICS stream
+ *
+ * @returns
+ * A stream containing the serialized items, properties and components.
+ */
+ nsIInputStream serializeToInputStream();
+
+ /**
+ * Serialize the added items, properties and components into an ICS stream
+ *
+ * @param aStream
+ * A stream into which the serialized items, properties and components
+ * will be written.
+ */
+ void serializeToStream(in nsIOutputStream aStream);
+};
diff --git a/calendar/base/public/calIImportExport.idl b/calendar/base/public/calIImportExport.idl
new file mode 100644
index 000000000..49bd77274
--- /dev/null
+++ b/calendar/base/public/calIImportExport.idl
@@ -0,0 +1,64 @@
+/* -*- Mode: IDL; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface nsIInputStream;
+interface nsIOutputStream;
+
+[scriptable, uuid(efef8333-e995-4f45-bdf7-bfcabbd9793e)]
+interface calIFileType : nsISupports
+{
+ /**
+ * The default extension that should be associated
+ * with files of this type.
+ */
+ readonly attribute AString defaultExtension;
+
+ /**
+ * The extension filter to use in the filepicker's filter list.
+ * Separate multiple extensions with semicolon and space.
+ * For example "*.html; *.htm".
+ */
+ readonly attribute AString extensionFilter;
+
+ /**
+ * The description to show to the user in the filter list.
+ */
+ readonly attribute AString description;
+};
+
+[scriptable, uuid(dbe262ca-d6c6-4691-8d46-e7f6bbe632ec)]
+interface calIImporter : nsISupports
+{
+ void getFileTypes(out unsigned long aCount,
+ [retval, array, size_is(aCount)] out calIFileType aTypes);
+
+ void importFromStream(in nsIInputStream aStream,
+ out unsigned long aCount,
+ [retval, array, size_is(aCount)] out calIItemBase aItems);
+};
+
+[scriptable, uuid(18c75bb3-6309-4c33-903f-6055fec39d07)]
+interface calIExporter : nsISupports
+{
+ void getFileTypes(out unsigned long aCount,
+ [retval, array, size_is(aCount)] out calIFileType aTypes);
+
+ /**
+ * Export the items into the stream
+ *
+ * @param aStream the stream to put the data into
+ * @param aCount the number of items being exported
+ * @param aItems an array of items to be exported
+ * @param aTitle a title the exporter can choose to use
+ */
+ void exportToStream(in nsIOutputStream aStream,
+ in unsigned long aCount,
+ [array, size_is(aCount)] in calIItemBase aItems,
+ in AString aTitle);
+};
diff --git a/calendar/base/public/calIItemBase.idl b/calendar/base/public/calIItemBase.idl
new file mode 100644
index 000000000..6641f684e
--- /dev/null
+++ b/calendar/base/public/calIItemBase.idl
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsISimpleEnumerator;
+interface nsIVariant;
+
+interface nsIPropertyBag;
+
+interface calIItemACLEntry;
+interface calIAlarm;
+interface calIAttachment;
+interface calIAttendee;
+interface calICalendar;
+interface calIDateTime;
+interface calIDuration;
+interface calIIcalComponent;
+interface calIRecurrenceInfo;
+interface calIRelation;
+
+//
+// calIItemBase
+//
+// Base for Events, Todos, Journals, etc.
+//
+
+[scriptable, uuid(9c988b8d-af45-4046-b05e-34417bba9058)]
+interface calIItemBase : nsISupports
+{
+ // returns true if this thing is able to be modified;
+ // if the item is not mutable, attempts to modify
+ // any data will throw CAL_ERROR_ITEM_IS_IMMUTABLE
+ readonly attribute boolean isMutable;
+
+ // makes this item immutable
+ void makeImmutable();
+
+ // clone always returns a mutable event
+ calIItemBase clone();
+
+ /**
+ * Hash Id that incorporates the item's UID, RECURRENCE-ID and calendar.id
+ * to be used for lookup of items that come from different calendars.
+ * Setting either id, recurrenceId or the calendar attribute leads to
+ * a recomputation of hashId.
+ *
+ * @attention Individual implementors of calIItemBase must stick to the
+ * same algorithm that base/src/calItemBase.js uses.
+ */
+ readonly attribute AUTF8String hashId;
+
+ /**
+ * Checks whether the argument object refers the same calendar item as
+ * this one, by testing both the id and recurrenceId property. This
+ *
+ * @arg aItem the item to compare against this one
+ *
+ * @return true if both ids match, false otherwise
+ */
+ boolean hasSameIds(in calIItemBase aItem);
+
+ /**
+ * Returns the acl entry associated to the item.
+ */
+ readonly attribute calIItemACLEntry aclEntry;
+
+ //
+ // the generation number of this item
+ //
+ attribute uint32_t generation;
+
+ // the time when this item was created
+ readonly attribute calIDateTime creationDate;
+
+ // last time any attribute was modified on this item, in UTC
+ readonly attribute calIDateTime lastModifiedTime;
+
+ // last time a "significant change" was made to this item
+ readonly attribute calIDateTime stampTime;
+
+ // the calICalendar to which this event belongs
+ attribute calICalendar calendar;
+
+ // the ID of this event
+ attribute AUTF8String id;
+
+ // event title
+ attribute AUTF8String title;
+
+ // event priority
+ attribute short priority;
+ attribute AUTF8String privacy;
+
+ // status of the event
+ attribute AUTF8String status;
+
+ // ical interop; writing this means parsing
+ // the ical string into this event
+ attribute AUTF8String icalString;
+
+ // an icalComponent for this item, suitable for serialization.
+ // the icalComponent returned is not live: changes in it or this
+ // item will not be reflected in the other.
+ attribute calIIcalComponent icalComponent;
+
+ //
+ // alarms
+ //
+
+ /**
+ * Get all alarms assigned to this item
+ *
+ * @param count The number of alarms
+ * @param aAlarms The array of calIAlarms
+ */
+ void getAlarms(out uint32_t count, [array, size_is(count), retval] out calIAlarm aAlarms);
+
+ /**
+ * Add an alarm to the item
+ *
+ * @param aAlarm The calIAlarm to add
+ */
+ void addAlarm(in calIAlarm aAlarm);
+
+ /**
+ * Delete an alarm from the item
+ *
+ * @param aAlarm The calIAlarm to delete
+ */
+ void deleteAlarm(in calIAlarm aAlarm);
+
+ /**
+ * Clear all alarms from the item
+ */
+ void clearAlarms();
+
+ // The last time this alarm was fired and acknowledged by the user; coerced to UTC.
+ attribute calIDateTime alarmLastAck;
+
+ //
+ // recurrence
+ //
+ attribute calIRecurrenceInfo recurrenceInfo;
+ readonly attribute calIDateTime recurrenceStartDate;
+
+ //
+ // All event properties are stored in a property bag;
+ // some number of these are "promoted" to top-level
+ // accessor attributes. For example, "SUMMARY" is
+ // promoted to the top-level "title" attribute.
+ //
+ // If you use the has/get/set/deleteProperty
+ // methods, property names are case-insensitive.
+ //
+ // For purposes of ICS serialization, all property names in
+ // the hashbag are in uppercase.
+ //
+ // The isPropertyPromoted() attribute can will indicate
+ // if a particular property is promoted or not, for
+ // serialization purposes.
+ //
+
+ // Note that if this item is a proxy, then any requests for
+ // non-existant properties will be forward to the parent item.
+
+ // some other properties that may exist:
+ //
+ // 'description' - description (string)
+ // 'location' - location (string)
+ // 'categories' - categories (string)
+ // 'syncId' - sync id (string)
+ // 'inviteEmailAddress' - string
+ // alarmLength/alarmUnits/alarmEmailAddress/lastAlarmAck
+ // recurInterval/recurCount/recurWeekdays/recurWeeknumber
+
+ // these forward to an internal property bag; implemented here, so we can
+ // do access control on set/delete to have control over mutability.
+ readonly attribute nsISimpleEnumerator propertyEnumerator;
+ boolean hasProperty(in AString name);
+
+ /**
+ * Gets a particular property.
+ * Objects passed back are still owned by the item, e.g. if callers need to
+ * store or modify a calIDateTime they must clone it.
+ */
+ nsIVariant getProperty(in AString name);
+
+ /**
+ * Sets a particular property.
+ * Ownership of objects gets passed to the item, e.g. callers must not
+ * modify a calIDateTime after it's been passed to an item.
+ *
+ * @warning this reflects the current implementation
+ * xxx todo: rethink whether it's more sensible to store
+ * clones in calItemBase.
+ */
+ void setProperty(in AString name, in nsIVariant value);
+
+ // will not throw an error if you delete a property that doesn't exist
+ void deleteProperty(in AString name);
+
+ // returns true if the given property is promoted to some
+ // top-level attribute (e.g. id or title)
+ boolean isPropertyPromoted(in AString name);
+
+ /**
+ * Returns a particular parameter value for a property, or null if the
+ * parameter does not exist. If the property does not exist, throws.
+ *
+ * @param aPropertyName the name of the property
+ * @param aParameterName the name of the parameter on the property
+ */
+ AString getPropertyParameter(in AString aPropertyName,
+ in AString aParameterName);
+
+ /**
+ * Checks if the given property has the given parameter.
+ *
+ * @param aPropertyName The name of the property.
+ * @param aParameterName The name of the parameter on the property.
+ * @return True, if the parameter exists on the property
+ */
+ boolean hasPropertyParameter(in AString aPropertyName,
+ in AString aParameterName);
+
+ /**
+ * Sets a particular parameter value for a property, or unsets if null is
+ * passed. If the property does not exist, throws.
+ *
+ * @param aPropertyName The name of the property
+ * @param aParameterName The name of the parameter on the property
+ * @param aParameterValue The value of the parameter to set
+ */
+ void setPropertyParameter(in AString aPropertyName,
+ in AString aParameterName,
+ in AUTF8String aParameterValue);
+
+ /**
+ * Returns a property parameter enumerator for the given property name
+ *
+ * @param aPropertyName The name of the property.
+ * @return The parameter enumerator.
+ */
+ nsISimpleEnumerator getParameterEnumerator(in AString aPropertyName);
+
+ /**
+ * The organizer (originator) of the item. We will likely not
+ * honour or preserve all fields in the calIAttendee passed around here.
+ * A base class like calIPerson might be more appropriate here, if we ever
+ * grow one.
+ */
+ attribute calIAttendee organizer;
+
+ //
+ // Attendees
+ //
+
+ // The array returned here is not live; it will not reflect calls to
+ // removeAttendee/addAttendee that follow the call to getAttendees.
+ void getAttendees(out uint32_t count,
+ [array,size_is(count),retval] out calIAttendee attendees);
+
+ /**
+ * getAttendeeById's matching is done in a case-insensitive manner to handle
+ * places where "MAILTO:" or similar properties are capitalized arbitrarily
+ * by different calendar clients.
+ */
+ calIAttendee getAttendeeById(in AUTF8String id);
+ void addAttendee(in calIAttendee attendee);
+ void removeAttendee(in calIAttendee attendee);
+ void removeAllAttendees();
+
+ //
+ // Attachments
+ //
+ void getAttachments(out uint32_t count,
+ [array,size_is(count),retval] out calIAttachment attachments);
+ void addAttachment(in calIAttachment attachment);
+ void removeAttachment(in calIAttachment attachment);
+ void removeAllAttachments();
+
+ //
+ // Categories
+ //
+
+ /**
+ * Gets the array of categories this item belongs to.
+ */
+ void getCategories(out uint32_t aCount,
+ [array, size_is(aCount), retval] out wstring aCategories);
+
+ /**
+ * Sets the array of categories this item belongs to.
+ */
+ void setCategories(in uint32_t aCount,
+ [array, size_is(aCount)] in wstring aCategories);
+
+ //
+ // Relations
+ //
+
+ /**
+ * This gives back every relation where the item is neighter the owner of the
+ * relation nor the referred relation
+ */
+ void getRelations(out uint32_t count,
+ [array,size_is(count),retval] out calIRelation relations);
+
+ /**
+ * Adds a relation to the item
+ */
+ void addRelation(in calIRelation relation);
+
+ /**
+ * Removes the relation for this item and the referred item
+ */
+ void removeRelation(in calIRelation relation);
+
+ /**
+ * Removes every relation for this item (in this items and also where it is referred
+ */
+ void removeAllRelations();
+
+ // Occurrence querying
+ //
+
+ /**
+ * Return a list of occurrences of this item between the given dates. The items
+ * returned are the same type as this one, as proxies.
+ */
+ void getOccurrencesBetween (in calIDateTime aStartDate, in calIDateTime aEndDate,
+ out uint32_t aCount,
+ [array,size_is(aCount),retval] out calIItemBase aOccurrences);
+
+ /**
+ * If this item is a proxy or overridden item, parentItem will point upwards
+ * to our parent. Otherwise, it will point to this.
+ * parentItem can thus always be used for modifyItem() calls
+ * to providers.
+ */
+ attribute calIItemBase parentItem;
+
+ /**
+ * The recurrence ID, a.k.a. DTSTART-of-calculated-occurrence,
+ * or null if this isn't an occurrence.
+ * Be conservative about setting this. It isn't marked as such, but
+ * consider it as readonly.
+ */
+ attribute calIDateTime recurrenceId;
+};
diff --git a/calendar/base/public/calIItipItem.idl b/calendar/base/public/calIItipItem.idl
new file mode 100644
index 000000000..c53e6854c
--- /dev/null
+++ b/calendar/base/public/calIItipItem.idl
@@ -0,0 +1,114 @@
+/* -*- Mode: idl; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calICalendar;
+interface nsISimpleEnumerator;
+
+/**
+ * calIItipItem is an interface used to carry information between the mime
+ * parser, the imip-bar UI, and the iTIP processor. It encapsulates a list of
+ * calIItemBase objects and provides specialized iTIP methods for those items.
+ */
+[scriptable, uuid(7539c158-c30d-41d0-90e9-41d315ac3eb1)]
+interface calIItipItem : nsISupports
+{
+ /**
+ * Initializes the item with an ics string
+ * @param - in parameter - AString of ical Data
+ */
+ void init(in AUTF8String icalData);
+
+ /**
+ * Creates a new calItipItem with the same attributes as the one that
+ * clone() is called upon.
+ */
+ calIItipItem clone();
+
+ /**
+ * Attribute: isSend - set to TRUE when sending this item to initiate an
+ * iMIP communication. This will be used by the iTIP processor to route
+ * the item directly to the email subsystem so that communication can be
+ * initiated. For example, if you are Sending a REQUEST, you would set
+ * this flag, and send the iTIP Item into the iTIP processor, which would
+ * handle everything else.
+ */
+ attribute boolean isSend;
+
+ /**
+ * Attribute: sender - set to the email address of the sender if part of an
+ * iMIP communication.
+ */
+ attribute AUTF8String sender;
+
+ /**
+ * Attribute: receivedMethod - method the iTIP item had upon reciept
+ */
+ attribute AUTF8String receivedMethod;
+
+ /**
+ * Attribute: responseMethod - method that the protocol handler (or the
+ * user) decides to use to respond to the iTIP item (could be COUNTER,
+ * REPLY, DECLINECOUNTER, etc)
+ */
+ attribute AUTF8String responseMethod;
+
+ /**
+ * Attribute: autoResponse Set to one of the three constants below
+ */
+ attribute unsigned long autoResponse;
+
+ /**
+ * Used to tell the iTIP processor to use an automatic response when
+ * handling this iTIP item
+ */
+ const unsigned long AUTO = 0;
+
+ /**
+ * Used to tell the iTIP processor to allow the user to edit the response
+ */
+ const unsigned long USER = 1;
+
+ /**
+ * Used to tell the iTIP processor not to respond at all.
+ */
+ const unsigned long NONE = 2;
+
+ /**
+ * Attribute: targetCalendar - the calendar that this thing should be
+ * stored in, if it should be stored onto a calendar. This is a calendar ID
+ */
+ attribute calICalendar targetCalendar;
+
+ /**
+ * The identity this item was received on. Helps to determine which
+ * attendee to manipulate. This should be the full email address of the
+ * attendee that is considered to be the local user.
+ */
+ attribute AUTF8String identity;
+
+ /**
+ * localStatus: The response that the user has made to the invitation in
+ * this ItipItem.
+ */
+ attribute AUTF8String localStatus;
+
+ /**
+ * Get the list of items that are encapsulated in this calIItipItem
+ * @returns An array of calIItemBase items that are inside this
+ * calIItipItem
+ */
+ void getItemList(out unsigned long itemCount,
+ [retval, array, size_is(itemCount)] out calIItemBase items);
+
+ /**
+ * Modifies the state of the given attendee in the item's ics
+ * @param attendeeId - AString containing attendee address
+ * @param status - AString containing the new attendee status
+ */
+ void setAttendeeStatus(in AString attendeeId, in AString status);
+};
diff --git a/calendar/base/public/calIItipTransport.idl b/calendar/base/public/calIItipTransport.idl
new file mode 100644
index 000000000..76934bd54
--- /dev/null
+++ b/calendar/base/public/calIItipTransport.idl
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIItipItem;
+interface calIAttendee;
+interface calIDateTime;
+
+/**
+ * calIItipTransport is a generic transport interface that is implemented
+ * by transports (eg: email, XMPP, etc.) wishing to send calIItipItems
+ */
+[scriptable, uuid(caedabb9-d886-4814-ada5-a5636d2fb939)]
+interface calIItipTransport : nsISupports
+{
+ /**
+ * Scheme to be used to prefix attendees. For example, the Email transport
+ * should return "mailto".
+ */
+ readonly attribute AUTF8String scheme;
+
+ /**
+ * Sending identity. This can be set to change the "sender" identity from
+ * defaultIdentity above.
+ */
+ attribute AUTF8String senderAddress;
+
+ /**
+ * Type of the transport: email, xmpp, etc.
+ */
+ readonly attribute AUTF8String type;
+
+ /**
+ * Sends a calIItipItem to the recipients using the specified title and
+ * alternative representation. If a calIItipItem is attached, then an ICS
+ * representation of those objects are generated and attached to the email.
+ * If the calIItipItem is null, then the item(s) is sent without any
+ * text/calendar mime part.
+ * @param count size of recipient array
+ * @param recipientArray array of recipients
+ * @param calIItipItem set of calIItems encapsulated as calIItipItems
+ */
+ boolean sendItems(in uint32_t count,
+ [array, size_is(count)] in calIAttendee recipientArray,
+ in calIItipItem item);
+};
diff --git a/calendar/base/public/calIOperation.idl b/calendar/base/public/calIOperation.idl
new file mode 100644
index 000000000..92f3e8bb8
--- /dev/null
+++ b/calendar/base/public/calIOperation.idl
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIVariant.idl"
+
+[scriptable, uuid(B96C2997-7AAA-4619-AD48-B7EBD9236C93)]
+interface calIOperation : nsISupports
+{
+ /**
+ * Id for easy management of pending requests.
+ */
+ readonly attribute AUTF8String id;
+
+ /**
+ * Determines whether the request is pending, i.e. has not been completed.
+ */
+ readonly attribute boolean isPending;
+
+ /**
+ * Status of the request, e.g. NS_OK while pending or after successful
+ * completion, or NS_ERROR_FAILED when failed.
+ */
+ readonly attribute nsIVariant status;
+
+ /**
+ * Cancels a pending request and changes status.
+ * @param aStatus operation status to be set;
+ * defaults to calIErrors.OPERATION_CANCELLED if null
+ */
+ void cancel([optional] in nsIVariant aStatus);
+};
+
+[scriptable, uuid(1FA39726-63D2-440c-A464-296D2822B9DA)]
+interface calIGenericOperationListener : nsISupports
+{
+ /**
+ * Generic callback receiving result.
+ * Results may appear in multiple calls, i.e. callees have to collect
+ * until isPending is false.
+ *
+ * @param aOperation operation object
+ * @param aResult result or null in case of an error
+ */
+ void onResult(in calIOperation aOperation, in nsIVariant aResult);
+};
diff --git a/calendar/base/public/calIPeriod.idl b/calendar/base/public/calIPeriod.idl
new file mode 100644
index 000000000..714b1f080
--- /dev/null
+++ b/calendar/base/public/calIPeriod.idl
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDateTime;
+interface calIDuration;
+
+[scriptable,uuid(ace2a74c-bd08-476f-be8b-6565abc50339)]
+interface calIPeriod : nsISupports
+{
+ attribute jsval icalPeriod;
+
+ /**
+ * isMutable is true if this instance is modifiable.
+ * If isMutable is false, any attempts to modify
+ * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE.
+ */
+ readonly attribute boolean isMutable;
+
+ /**
+ * Make this calIPeriod instance immutable.
+ */
+ void makeImmutable();
+
+ /**
+ * Clone this calIPeriod instance into a new
+ * mutable object.
+ */
+ calIPeriod clone();
+
+ /**
+ * The start datetime of this period
+ */
+ attribute calIDateTime start;
+
+ /**
+ * The end datetime of this period
+ */
+ attribute calIDateTime end;
+
+ /**
+ * The duration, equal to end-start
+ */
+ readonly attribute calIDuration duration;
+
+
+ /**
+ * Return a string representation of this instance.
+ */
+ AUTF8String toString();
+
+ /**
+ * This object as an iCalendar DURATION string
+ */
+ attribute ACString icalString;
+};
+
+/** Libical specific interfaces */
+
+[ptr] native icalperiodtypeptr(struct icalperiodtype);
+[scriptable,uuid(04ee525f-96db-4731-8d61-688e754df24f)]
+interface calIPeriodLibical : calIPeriod
+{
+ [noscript,notxpcom] void toIcalPeriod(in icalperiodtypeptr idt);
+};
diff --git a/calendar/base/public/calIPrintFormatter.idl b/calendar/base/public/calIPrintFormatter.idl
new file mode 100644
index 000000000..08952b8e0
--- /dev/null
+++ b/calendar/base/public/calIPrintFormatter.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: IDL; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+interface nsIOutputStream;
+
+[scriptable, uuid(014dea21-90cd-4563-b1bd-13b842a465e0)]
+interface calIPrintFormatter : nsISupports
+{
+ /**
+ * The name of this layout. Implementers should make sure this string
+ * is localizable, ie uses nsIStringBundle
+ */
+ readonly attribute AString name;
+
+ /**
+ * Format the items into the stream, as html code.
+ * May assume that all the items are inside the given daterange.
+ * The user requested to show all the days in the daterange, so unless
+ * there is a special reason, all the days should be shown.
+ * aStart and aEnd may be null, in which case the implementation can
+ * show the minimal days needed to show all the events. It can skip
+ * months without events, for example.
+ *
+ * @param aStream the stream to put the html data into
+ * @param aStart the first date that should be printed
+ * @param aEnd the last date that should be printed
+ * @param aCount the number of items being printed
+ * @param aItems the items to print
+ * @param aTitle a title for the HTML page
+ */
+ void formatToHtml(in nsIOutputStream aStream,
+ in calIDateTime aStart,
+ in calIDateTime aEnd,
+ in unsigned long aCount,
+ [array, size_is(aCount)] in calIItemBase aItems,
+ in AString aTitle);
+};
diff --git a/calendar/base/public/calIRecurrenceDate.idl b/calendar/base/public/calIRecurrenceDate.idl
new file mode 100644
index 000000000..33f1d4b48
--- /dev/null
+++ b/calendar/base/public/calIRecurrenceDate.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+#include "calIRecurrenceItem.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+interface calIIcalProperty;
+
+// an interface implementing a RDATE or EXDATE
+
+[scriptable, uuid(c5b331d4-b470-475b-9497-db9e2731e559)]
+interface calIRecurrenceDate : calIRecurrenceItem
+{
+ //
+ // recurrence date set
+ //
+ attribute calIDateTime date;
+};
diff --git a/calendar/base/public/calIRecurrenceInfo.idl b/calendar/base/public/calIRecurrenceInfo.idl
new file mode 100644
index 000000000..85515f3d6
--- /dev/null
+++ b/calendar/base/public/calIRecurrenceInfo.idl
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+interface calIRecurrenceItem;
+
+interface calIIcalProperty;
+
+[scriptable, uuid(8ca5db89-2583-4f0c-b845-4a6d2f229efd)]
+interface calIRecurrenceInfo : nsISupports
+{
+ // returns true if this thing is able to be modified;
+ // if the item is not mutable, attempts to modify
+ // any data will throw CAL_ERROR_ITEM_IS_IMMUTABLE
+ readonly attribute boolean isMutable;
+
+ // makes this item immutable
+ void makeImmutable();
+
+ // clone always returns a mutable event
+ calIRecurrenceInfo clone();
+
+ // initialize this with the item for which this recurrence
+ // applies, so that the start date can be tracked
+ attribute calIItemBase item;
+
+ /**
+ * The start date of an item is directly referenced by parts of calIRecurrenceInfo,
+ * thus changing the former without adjusting the latter would break the internal structure.
+ * This method provides the necessary functionality. There's no need to call it manually
+ * after writing to the start date of an item, since it's called automatically in the
+ * appropriate setter of an item.
+ */
+ void onStartDateChange(in calIDateTime aNewStartTime, in calIDateTime aOldStartTime);
+
+ /**
+ * If the base item's UID changes, this implicitly has to change all overridden items' UID, too.
+ *
+ * @param id new UID
+ */
+ void onIdChange(in AUTF8String aNewId);
+
+ /*
+ * Set of recurrence items; the order of these matters.
+ */
+
+ void getRecurrenceItems(out unsigned long aCount, [array,size_is(aCount),retval] out calIRecurrenceItem aItems);
+ void setRecurrenceItems(in unsigned long aCount, [array,size_is(aCount)] in calIRecurrenceItem aItems);
+
+ unsigned long countRecurrenceItems();
+ void clearRecurrenceItems();
+ void appendRecurrenceItem(in calIRecurrenceItem aItem);
+
+ calIRecurrenceItem getRecurrenceItemAt(in unsigned long aIndex);
+ void deleteRecurrenceItemAt(in unsigned long aIndex);
+ void deleteRecurrenceItem(in calIRecurrenceItem aItem);
+ // inserts the item at the given index, pushing the item that was previously there forward
+ void insertRecurrenceItemAt(in calIRecurrenceItem aItem, in unsigned long aIndex);
+
+ /**
+ * isFinite is true if the recurrence items specify a finite number
+ * of occurrences. This is useful for UI and for possibly other users.
+ */
+ readonly attribute boolean isFinite;
+
+ /**
+ * This is a shortcut to appending or removing a single negative date
+ * assertion. aRecurrenceId needs to be a normal recurrence id, it may not be
+ * RDATE.
+ */
+ void removeOccurrenceAt(in calIDateTime aRecurrenceId);
+ void restoreOccurrenceAt(in calIDateTime aRecurrenceId);
+
+ /*
+ * exceptions
+ */
+
+ /**
+ * Modify an a particular occurrence with the given exception proxy
+ * item. If the recurrenceId isn't an already existing exception item,
+ * a new exception is added. Otherwise, the existing exception
+ * is modified.
+ *
+ * The item's parentItem must be equal to this RecurrenceInfo's
+ * item. <-- XXX check this, compare by calendar/id only
+ *
+ * @param anItem exceptional/overridden item
+ * @param aTakeOverOwnership whether the recurrence info object can take over
+ * the item or needs to clone it
+ */
+ void modifyException(in calIItemBase anItem, in boolean aTakeOverOwnership);
+
+ /**
+ * Return an existing exception item for the given recurrence ID.
+ * If an exception does not exist, null is returned.
+ */
+ calIItemBase getExceptionFor(in calIDateTime aRecurrenceId);
+
+ /**
+ * Removes an exception item for the given recurrence ID, if
+ * any exist.
+ */
+ void removeExceptionFor(in calIDateTime aRecurrenceId);
+
+ /**
+ * Returns a list of all recurrence ids that have exceptions.
+ */
+ void getExceptionIds(out unsigned long aCount, [array,size_is(aCount),retval] out calIDateTime aIds);
+
+ /*
+ * Recurrence calculation
+ */
+
+ /*
+ * Get the occurrence at the given recurrence ID; if there is no
+ * exception, then create a new proxy object with the normal occurrence.
+ * Otherwise, return the exception.
+ *
+ * @param aRecurrenceId The recurrence ID to get the occurrence for.
+ * @return The occurrence or exception corresponding to the id
+ */
+ calIItemBase getOccurrenceFor(in calIDateTime aRecurrenceId);
+
+ /**
+ * Return the chronologically next occurrence after aTime. This takes
+ * exceptions and EXDATE/RDATEs into account.
+ *
+ * @param aTime The (exclusive) date to start searching.
+ * @return The next occurrence, or null if there is none.
+ */
+ calIItemBase getNextOccurrence(in calIDateTime aTime);
+
+ /**
+ * Return the chronologically previous occurrence after aTime. This takes
+ * exceptions and EXDATE/RDATEs into account.
+ *
+ * @param aTime The (exclusive) date to start searching.
+ * @return The previous occurrence, or null if there is none.
+ */
+ calIItemBase getPreviousOccurrence(in calIDateTime aTime);
+
+ /**
+ * Return an array of calIDateTime representing all start times of this event
+ * between start (inclusive) and end (non-inclusive). Exceptions are taken
+ * into account.
+ *
+ * @param aRangeStart The (inclusive) date to start searching.
+ * @param aRangeEnd The (exclusive) date to end searching.
+ * @param aMaxCount The maximum number of dates to return
+ *
+ * @param aCount The number of dates returned.
+ * @return The array of dates.
+ */
+ void getOccurrenceDates(in calIDateTime aRangeStart,
+ in calIDateTime aRangeEnd,
+ in unsigned long aMaxCount,
+ out unsigned long aCount, [array,size_is(aCount),retval] out calIDateTime aDates);
+
+ /**
+ * Return an array of calIItemBase representing all occurrences of this event
+ * between start (inclusive) and end (non-inclusive). Exceptions are taken
+ * into account.
+ *
+ * @param aRangeStart The (inclusive) date to start searching.
+ * @param aRangeEnd The (exclusive) date to end searching.
+ * @param aMaxCount The maximum number of occurrences to return
+ *
+ * @param aCount The number of occurrences returned.
+ * @return The array of occurrences.
+ */
+ void getOccurrences(in calIDateTime aRangeStart,
+ in calIDateTime aRangeEnd,
+ in unsigned long aMaxCount,
+ out unsigned long aCount, [array,size_is(aCount),retval] out calIItemBase aItems);
+};
diff --git a/calendar/base/public/calIRecurrenceItem.idl b/calendar/base/public/calIRecurrenceItem.idl
new file mode 100644
index 000000000..58261cfb5
--- /dev/null
+++ b/calendar/base/public/calIRecurrenceItem.idl
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+interface calIIcalProperty;
+
+[scriptable, uuid(918a243b-d887-41b0-8b4b-9cd56a9dd55f)]
+interface calIRecurrenceItem : nsISupports
+{
+ // returns true if this thing is able to be modified;
+ // if the item is not mutable, attempts to modify
+ // any data will throw CAL_ERROR_ITEM_IS_IMMUTABLE
+ readonly attribute boolean isMutable;
+
+ // makes this item immutable
+ void makeImmutable();
+
+ // clone always returns a mutable event
+ calIRecurrenceItem clone();
+
+ // defaults to false; if true, this item is to be interpreted
+ // as a negative rule (e.g. exceptions instead of rdates)
+ attribute boolean isNegative;
+
+ // returns whether this item has a finite number of dates
+ // or not (e.g. a rule with no end date)
+ readonly attribute boolean isFinite;
+
+ /**
+ * Search for the next occurrence after aTime and return its recurrence id.
+ * aRecurrenceId must be the recurrence id of an occurrence to search after.
+ *
+ * @require (aTime >= aRecurrenceId)
+ * @param aRecurrenceId The recurrence id to start searching at.
+ * @param aTime The earliest time to find the occurrence after.
+ */
+ calIDateTime getNextOccurrence(in calIDateTime aRecurrenceId,
+ in calIDateTime aTime);
+
+ // return array of calIDateTime of the start of all occurrences of
+ // this event starting at aStartTime, between rangeStart and an
+ // optional rangeEnd
+ void getOccurrences (in calIDateTime aStartTime,
+ in calIDateTime aRangeStart,
+ in calIDateTime aRangeEnd,
+ in unsigned long aMaxCount,
+ out unsigned long aCount, [array,size_is(aCount),retval] out calIDateTime aDates);
+
+ attribute calIIcalProperty icalProperty;
+ attribute AUTF8String icalString;
+};
diff --git a/calendar/base/public/calIRecurrenceRule.idl b/calendar/base/public/calIRecurrenceRule.idl
new file mode 100644
index 000000000..5aba51069
--- /dev/null
+++ b/calendar/base/public/calIRecurrenceRule.idl
@@ -0,0 +1,59 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+#include "calIRecurrenceItem.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+// an interface implementing a RRULE
+
+[scriptable, uuid(e965a91a-49fa-41b5-b668-1a824a73bdbf)]
+interface calIRecurrenceRule : calIRecurrenceItem
+{
+ //
+ // rule-based recurrence
+ //
+
+ // null/"", "SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY",
+ // "MONTHLY", "YEARLY"
+ attribute AUTF8String type;
+
+ // repeat every N of type
+ // XXX Please mind an implementation detail:
+ // the underlying libical currently only supports C short values for interval,
+ // i.e. commonly 16 bits on most platforms, thus please use only 0 <= interval <= 0x7fff.
+ // It is open whether we go with IDL short here or tweak libical to support at least 32 bits.
+ attribute long interval;
+
+ // These two are mutually exclusive; whichever is set
+ // invalidates the other. It's only valid to read the one
+ // that was set; the other will throw NS_ERROR_FAILURE. Use
+ // isByCount to figure out whether count or untilDate is valid.
+ // Setting count to -1 or untilDate to null indicates infinite
+ // recurrence.
+ attribute long count;
+ attribute calIDateTime untilDate;
+
+ // if this isn't infinite recurrence, this flag indicates whether
+ // it was set by count or not
+ readonly attribute boolean isByCount;
+
+ // The week start for this rule, used for certain calculations. This is a
+ // value from 0=Sunday to 6=Saturday.
+ attribute short weekStart;
+
+ // the components defining the recurrence
+ // "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
+ // "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH",
+ // "BYSETPOS"
+ void getComponent (in AUTF8String aComponentType,
+ out unsigned long aCount, [array,size_is(aCount),retval] out short aValues);
+ void setComponent (in AUTF8String aComponentType,
+ in unsigned long aCount, [array,size_is(aCount)] in short aValues);
+
+};
diff --git a/calendar/base/public/calIRelation.idl b/calendar/base/public/calIRelation.idl
new file mode 100644
index 000000000..ee73dc9aa
--- /dev/null
+++ b/calendar/base/public/calIRelation.idl
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIIcalProperty;
+interface calIItemBase;
+
+[scriptable,uuid(77f0820a-2b49-4c8e-86bf-2b6bda46e391)]
+interface calIRelation : nsISupports
+{
+ /**
+ * The type of the relation between the items:
+ * PARENT
+ * CHILD
+ * SIBLING
+ */
+ attribute AUTF8String relType;
+
+ /**
+ * The id of the related item
+ **/
+
+ attribute AUTF8String relId;
+
+ /**
+ * The calIIcalProperty corresponding to this object. Can be used for
+ * serializing/unserializing from ics files.
+ */
+ attribute calIIcalProperty icalProperty;
+ attribute AUTF8String icalString;
+
+ /**
+ * For accessing additional parameters, such as x-params.
+ */
+ AUTF8String getParameter(in AString name);
+ void setParameter(in AString name, in AUTF8String value);
+ void deleteParameter(in AString name);
+
+ /**
+ * Clone this calIRelation instance into a new object.
+ */
+ calIRelation clone();
+};
diff --git a/calendar/base/public/calISchedulingSupport.idl b/calendar/base/public/calISchedulingSupport.idl
new file mode 100644
index 000000000..123ca3519
--- /dev/null
+++ b/calendar/base/public/calISchedulingSupport.idl
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface calIItemBase;
+interface calIAttendee;
+
+/**
+ * Accesses scheduling specific information of calendar items.
+ * Implementation by providers is optional.
+ */
+[scriptable, uuid(9221e243-c97e-4c5f-9e00-5d7d3521bb44)]
+interface calISchedulingSupport : nsISupports
+{
+ /**
+ * Tests whether the passed item corresponds to an invitation, e.g.
+ * the CUA or server has placed it in the calendar.
+ *
+ * @param aItem Item to be tested.
+ * @return Whether the passed item corresponds to an invitation.
+ */
+ boolean isInvitation(in calIItemBase aItem);
+
+ /**
+ * Gets the invited attendee if the passed item corresponds to
+ * an invitation. UI code will use that attendee to modify e.g. PARTSTAT.
+ * If isInvitation returns true, getInvitedAttendee must return
+ * an attendee. If isInvitation is false, getInvitedAttendee may return
+ * an attendee in case the organizer (and owner of the calendar) has
+ * invited himself.
+ *
+ * @param aItem Invitation item.
+ * @return Attendee object, or null.
+ */
+ calIAttendee getInvitedAttendee(in calIItemBase aItem);
+
+ /**
+ * Checks whether the provider keeps track of sending out the proper
+ * iTIP/iMIP message for a particular item.
+ *
+ * @param aMethod a iTIP method
+ * @param aItem an item that has been modified/deleted etc.
+ * @return true, if the provider keeps track of sending out passed message
+ */
+ boolean canNotify(in AUTF8String aMethod, in calIItemBase aItem);
+};
diff --git a/calendar/base/public/calIStartupService.idl b/calendar/base/public/calIStartupService.idl
new file mode 100644
index 000000000..c387bf5a4
--- /dev/null
+++ b/calendar/base/public/calIStartupService.idl
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "calIOperation.idl"
+
+/**
+ * Interface that can be used on services that need to be started up and shut
+ * down. The service needs to be registered within calStartupService.js, so this
+ * is only useful from within calendar code. If you want calendar code to be
+ * fully initialized, listen to "calendar-startup-done" via nsIObserverService.
+ */
+[scriptable, uuid(99d52094-37f9-4c81-9c55-32fbeb6a79cf)]
+interface calIStartupService: nsISupports
+{
+ /**
+ * Function called when the service should be started
+ *
+ * @param completeListener The listener to call on startup completion.
+ */
+ void startup(in calIGenericOperationListener completeListener);
+
+ /**
+ * Function called when the service should be shut down.
+ *
+ * @param completeListener The listener to call on shutdown completion.
+ */
+ void shutdown(in calIGenericOperationListener completeListener);
+};
diff --git a/calendar/base/public/calIStatusObserver.idl b/calendar/base/public/calIStatusObserver.idl
new file mode 100644
index 000000000..2c803ce37
--- /dev/null
+++ b/calendar/base/public/calIStatusObserver.idl
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface calICalendar;
+interface nsIDOMChromeWindow;
+
+[scriptable, uuid(60160f68-4514-41b4-a19d-2f2cf0143426)]
+interface calIStatusObserver : nsISupports
+{
+
+ void initialize(in nsIDOMChromeWindow aWindow);
+
+ /**
+ * Starts the display of an operation to check a series of calendars
+ * This operation may either be determined or undetermined
+ * @param aProgressMode An integer value that can accept DETERMINED_PROGRESS,
+ * UNDETERMINED_PROGRESS or NO_PROGRESS
+ * @param aCalendarsCount If the first parameter is DETERMINED_PROGRESS
+ * aCalendarCount is the number of Calendars
+ * which completion is to be displayed
+ */
+ void startMeteors(in unsigned long aProgressMode, in unsigned long aCalendarCount);
+
+ /**
+ * stops the display of an progressed operation
+ */
+ void stopMeteors();
+
+ /**
+ * increments the display value denoting that a calendar has been processed
+ */
+ void calendarCompleted(in calICalendar aCalendar);
+
+ /**
+ * @return An integer value denoting wheter a progress is running or not;
+ * if it returns DETERMINED_PROGRESS a determined progress
+ is running;
+ * if it returns UNDETERMINED_PROGRESS an undetermined progress
+ is running;
+ * if it returns NO_PROGRESS no Progress is running.
+ */
+ readonly attribute unsigned long spinning;
+
+ /**
+ * A constant that denotes that no operation is running
+ */
+ const unsigned long NO_PROGRESS = 0;
+
+ /**
+ * A constant that refers to whether an operation is determined
+ */
+ const unsigned long DETERMINED_PROGRESS = 1;
+
+ /**
+ * A constant that refers to whether an operation is undetermined
+ */
+ const unsigned long UNDETERMINED_PROGRESS = 2;
+};
diff --git a/calendar/base/public/calITimezone.idl b/calendar/base/public/calITimezone.idl
new file mode 100644
index 000000000..a291d8746
--- /dev/null
+++ b/calendar/base/public/calITimezone.idl
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIIcalComponent;
+interface calITimezoneProvider;
+
+[scriptable, uuid(D79161E7-0DB9-427d-A0C3-27E0DB3B030F)]
+interface calITimezone : nsISupports
+{
+ /**
+ * The timezone provider this timezone belongs to, if any.
+ */
+ readonly attribute calITimezoneProvider provider;
+
+ /**
+ * VTIMEZONE ical component, null if floating or UTC.
+ */
+ readonly attribute calIIcalComponent icalComponent;
+
+ /**
+ * The TZID of this timezone.
+ */
+ readonly attribute AUTF8String tzid;
+
+ /**
+ * Whether this timezone is the "floating" timezone.
+ */
+ readonly attribute boolean isFloating;
+
+ /**
+ * Whether this is the "UTC" timezone.
+ */
+ readonly attribute boolean isUTC;
+
+ /**
+ * Latitude of timezone or void/null if unknown.
+ */
+ readonly attribute AUTF8String latitude;
+
+ /**
+ * Longitude of timezone or void/null if unknown.
+ */
+ readonly attribute AUTF8String longitude;
+
+ /**
+ * Localized name of the timezone; falls back to TZID if unknown.
+ */
+ readonly attribute AString displayName;
+
+ /**
+ * For debugging purposes.
+ *
+ * @return "UTC", "floating" or component's ical representation
+ */
+ AUTF8String toString();
+};
+
diff --git a/calendar/base/public/calITimezoneProvider.idl b/calendar/base/public/calITimezoneProvider.idl
new file mode 100644
index 000000000..95bfc03ce
--- /dev/null
+++ b/calendar/base/public/calITimezoneProvider.idl
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIUTF8StringEnumerator;
+interface calITimezone;
+
+[scriptable, uuid(331a7c6d-805a-4926-940b-2d78dcd90554)]
+interface calITimezoneProvider : nsISupports
+{
+ readonly attribute nsIUTF8StringEnumerator timezoneIds;
+ readonly attribute nsIUTF8StringEnumerator aliasIds;
+
+ /**
+ * Gets a timezone defintion passing a TZID.
+ * Returns null in case of an unknown TZID.
+ *
+ * @param tzid a TZID to be resolved
+ * @return a timezone object or null
+ */
+ calITimezone getTimezone(in AUTF8String tzid);
+};
+
+/**
+ * This service acts as a central access point for the up to date set
+ * of Olson timezone definitions.
+ */
+[scriptable, uuid(AB1BFE6A-EE95-4038-B594-34AEEDA9911A)]
+interface calITimezoneService : calITimezoneProvider
+{
+ readonly attribute calITimezone floating;
+ readonly attribute calITimezone UTC;
+
+ /**
+ * Provides the version of the underlying timezone database.
+ */
+ readonly attribute AString version;
+
+ /**
+ * Returns the default timezone from calendar.timezone.local. If no timezone
+ * has been set, a best guess is taken from the operating system and the
+ * timezone is saved into the above mentioned pref.
+ */
+ readonly attribute calITimezone defaultTimezone;
+};
diff --git a/calendar/base/public/calITodo.idl b/calendar/base/public/calITodo.idl
new file mode 100644
index 000000000..1ed322829
--- /dev/null
+++ b/calendar/base/public/calITodo.idl
@@ -0,0 +1,72 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "calIItemBase.idl"
+
+//
+// calITodo
+//
+// An interface for a todo item (analogous to a VTODO)
+//
+
+[scriptable, uuid(0a93fdad-8a5c-44e9-8f90-16a6df819e03)]
+interface calITodo : calIItemBase
+{
+ const long CAL_TODO_STATUS_NEEDSACTION = 4;
+ const long CAL_TODO_STATUS_COMPLETED = 5;
+ const long CAL_TODO_STATUS_INPROCESS = 6;
+
+ // as per the rather broken RFC2445,
+
+ // entryDate maps to DTSTART, which is the day
+ // this todo shows up on, if set. (optional).
+ //
+ // dueDate maps to DUE, which is the day
+ // this todo is due, if set. (optional).
+ //
+ // If neither DUE nor DTSTART are set, then
+ // the todo appears "today" until it is completed.
+ //
+ // The completeDate is the date the todo was completed,
+ // or null if it hasn't been completed yet.
+
+ attribute calIDateTime entryDate;
+ attribute calIDateTime dueDate;
+ attribute calIDateTime completedDate;
+ attribute short percentComplete;
+
+ // A todo isCompleted if any of the following is true:
+ // - percentComplete is 100, or
+ // - completedDate is non-null, or
+ // - status is COMPLETED.
+ // Setting isCompleted to true will
+ // - set percentComplete to 100, and
+ // - set completedDate to the current time, if it is not already set, and
+ // - set status to COMPLETED.
+ // Setting isCompleted to false will remove percentComplete, completedDate,
+ // and status properties. (This returns the todo to its state at creation,
+ // in terms of completion-relevant properties.)
+ //
+ // If you would like to take advantage of the full, confusing disaster that
+ // is the RFC2445 VTODO status state space, you can feel free to set the
+ // fields individually, instead of setting isCompleted directly. (And then
+ // hope that whatever else you're talking to has the same set of rules for
+ // determining if something is completed or not.)
+ //
+ // Setting percentComplete, completedDate, or status individually does not
+ // affect any of the others at present. (E.g., setting the percentComplete
+ // from 100 to 50 doesn't clear completedDate, or change status to
+ // IN-PROCESS.) It's not clear that we want any more magic than a simple
+ // property to control "all complete" vs "not complete in any way".
+ attribute boolean isCompleted;
+
+ /**
+ * The duration of the todo, which is either set or defined as
+ * dueDate - entryDate.
+ * Please note that null is returned if there is no duration set and entry
+ * Date or dueDate don't exist.
+ */
+ attribute calIDuration duration;
+};
diff --git a/calendar/base/public/calITransactionManager.idl b/calendar/base/public/calITransactionManager.idl
new file mode 100644
index 000000000..2dac89023
--- /dev/null
+++ b/calendar/base/public/calITransactionManager.idl
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsITransactionManager;
+interface calICalendar;
+interface calIItemBase;
+interface calIOperationListener;
+
+/**
+ * calITransactionManager is a service designed to handle nsITransactions
+ * regarding the calendar. It is here as a service so that we can keep the
+ * transactions around without holding onto the whole global js scope+window.
+ */
+[scriptable, uuid(40a1ccf4-5f54-4815-b842-abf06f84dbfd)]
+interface calITransactionManager : nsISupports
+{
+ /**
+ * @param aAction The Action to execute. This can be one of:
+ * add Adds an item
+ * modify Modfifies an item
+ * delete Deletes an item
+ * move Move an item from one calendar to the
+ * next. With this operation, aCalendar is
+ * the calendar that the event should be
+ * moved to.
+ * @param aCalendar The Calendar to execute the transaction on
+ * @param aItem The changed item for this transaction. This item
+ * should be immutable
+ * @param aOldItem The Item in its original form. Only needed for
+ * modify.
+ * @param aListener The listener to call when the transaction has
+ * completed. This parameter can be null.
+ */
+ void createAndCommitTxn(in AUTF8String aAction,
+ in calIItemBase aItem,
+ in calICalendar aCalendar,
+ in calIItemBase aOldItem,
+ in calIOperationListener aListener);
+
+ /**
+ * Signals the transaction manager that a series of transactions are going
+ * to be performed, but that, for the purposes of undo and redo, they should
+ * all be regarded as a single transaction. See also
+ * nsITransactionManager::beginBatch
+ */
+ void beginBatch();
+
+ /**
+ * Ends the batch transaction in process. See also
+ * nsITransactionManager::endBatch
+ */
+ void endBatch();
+
+ /**
+ * Undo the last transaction in the transaction manager's stack
+ */
+ void undo();
+
+ /**
+ * Returns true if it is possible to undo a transaction at this time
+ */
+ boolean canUndo();
+
+ /**
+ * Redo the last transaction
+ */
+ void redo();
+
+ /**
+ * Returns true if it is possible to redo a transaction at this time
+ */
+ boolean canRedo();
+
+ /**
+ * A reference to the transaction manager for calendar operations
+ */
+ readonly attribute nsITransactionManager transactionManager;
+};
diff --git a/calendar/base/public/calIWeekInfoService.idl b/calendar/base/public/calIWeekInfoService.idl
new file mode 100644
index 000000000..46693e52c
--- /dev/null
+++ b/calendar/base/public/calIWeekInfoService.idl
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface calIDateTime;
+
+/**
+ * This interface will calculate a week title from a given datetime. This
+ * will depends on the users preferences.
+ * Extensions might override the default implementation, in order to
+ * generate week titles aimed at special cases (like weeknumbers for a
+ * schoolyear)
+ */
+[scriptable, uuid(650fd33b-ebf4-46fa-b9ca-dd80b2451498)]
+interface calIWeekInfoService: nsISupports
+{
+ /**
+ * Return the week title. It's meant to be displayed.
+ * (Usually, will return a weeknumber, but might return a string like Q1W4)
+ *
+ * @param dateTime
+ * The dateTime to get the weektitle for
+ * @returns
+ * A string, representing the week title. Will usually be the
+ * week number. Every week (7 days) should get a different string,
+ * but the switch from one week to the next isn't necessarily
+ * on sunday.
+ */
+ AString getWeekTitle(in calIDateTime dateTime);
+
+ /**
+ * Gets the first day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate The dateTime to get get the start of the week for
+ * @return A dateTime-object denoting the first day of the week
+ */
+ calIDateTime getStartOfWeek(in calIDateTime dateTime);
+
+ /**
+ * Gets the last day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate The dateTime to get get the last day of the week for
+ * @return A dateTime-object denoting the last day of the week
+ */
+ calIDateTime getEndOfWeek(in calIDateTime dateTime);
+};
diff --git a/calendar/base/public/moz.build b/calendar/base/public/moz.build
new file mode 100644
index 000000000..9ba1d9a03
--- /dev/null
+++ b/calendar/base/public/moz.build
@@ -0,0 +1,61 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'calIAlarm.idl',
+ 'calIAlarmService.idl',
+ 'calIAttachment.idl',
+ 'calIAttendee.idl',
+ 'calICalendar.idl',
+ 'calICalendarACLManager.idl',
+ 'calICalendarManager.idl',
+ 'calICalendarProvider.idl',
+ 'calICalendarSearchProvider.idl',
+ 'calICalendarView.idl',
+ 'calICalendarViewController.idl',
+ 'calIChangeLog.idl',
+ 'calIDateTime.idl',
+ 'calIDateTimeFormatter.idl',
+ 'calIDeletedItems.idl',
+ 'calIDuration.idl',
+ 'calIErrors.idl',
+ 'calIEvent.idl',
+ 'calIFreeBusyProvider.idl',
+ 'calIIcsParser.idl',
+ 'calIIcsSerializer.idl',
+ 'calIICSService.idl',
+ 'calIImportExport.idl',
+ 'calIItemBase.idl',
+ 'calIItipItem.idl',
+ 'calIItipTransport.idl',
+ 'calIOperation.idl',
+ 'calIPeriod.idl',
+ 'calIPrintFormatter.idl',
+ 'calIRecurrenceDate.idl',
+ 'calIRecurrenceInfo.idl',
+ 'calIRecurrenceItem.idl',
+ 'calIRecurrenceRule.idl',
+ 'calIRelation.idl',
+ 'calISchedulingSupport.idl',
+ 'calIStartupService.idl',
+ 'calIStatusObserver.idl',
+ 'calITimezone.idl',
+ 'calITimezoneProvider.idl',
+ 'calITodo.idl',
+ 'calITransactionManager.idl',
+ 'calIWeekInfoService.idl',
+]
+
+XPIDL_MODULE = 'calbase'
+
+EXPORTS += [
+ 'calBaseCID.h',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Calendar', 'Internal Components')
+
+with Files('calIAlarm*'):
+ BUG_COMPONENT = ('Calendar', 'Alarms')
diff --git a/calendar/base/src/WindowsNTToZoneInfoTZId.properties b/calendar/base/src/WindowsNTToZoneInfoTZId.properties
new file mode 100644
index 000000000..124ffce22
--- /dev/null
+++ b/calendar/base/src/WindowsNTToZoneInfoTZId.properties
@@ -0,0 +1,98 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Mapping
+# from: Microsoft Windows NT/2K/XP/Vista registry timezone subkey (not localized name)
+# to: ZoneInfo timezone identifier (Eggert & Olson)
+#
+Afghanistan Standard Time: Asia/Kabul
+Alaskan Standard Time: America/Anchorage
+Arab Standard Time: Asia/Riyadh
+Arabian Standard Time: Asia/Muscat
+Arabic Standard Time: Asia/Baghdad
+Armenian Standard Time: Asia/Yerevan
+Atlantic Standard Time: America/Halifax
+AUS Central Standard Time: Australia/Darwin
+AUS Eastern Standard Time: Australia/Sydney
+Azerbaijan Standard Time: Asia/Baku
+Azores Standard Time: Atlantic/Azores
+Bangkok Standard Time: Asia/Bangkok
+Canada Central Standard Time: America/Regina
+Cape Verde Standard Time: Atlantic/Cape_Verde
+Caucasus Standard Time: Asia/Yerevan
+Cen. Australia Standard Time: Australia/Adelaide
+Central America Standard Time: America/El_Salvador
+Central Asia Standard Time: Asia/Dhaka
+Central Brazilian Standard Time: America/Manaus
+Central Europe Standard Time: Europe/Prague
+Central European Standard Time: Europe/Belgrade
+Central Pacific Standard Time: Pacific/Guadalcanal
+Central Standard Time: America/Chicago
+Central Standard Time (Mexico): America/Mexico_City
+China Standard Time: Asia/Shanghai
+Dateline Standard Time: Pacific/Kwajalein
+E. Africa Standard Time: Africa/Nairobi
+E. Australia Standard Time: Australia/Brisbane
+E. Europe Standard Time: Europe/Bucharest
+E. South America Standard Time: America/Sao_Paulo
+Eastern Standard Time: America/New_York
+Egypt Standard Time: Africa/Cairo
+Ekaterinburg Standard Time: Asia/Yekaterinburg
+Fiji Standard Time: Pacific/Fiji
+FLE Standard Time: Europe/Helsinki
+Georgian Standard Time: Asia/Tbilisi
+GFT Standard Time: Europe/Athens
+GMT Standard Time: Europe/London
+Greenland Standard Time: America/Godthab
+Greenwich Standard Time: Africa/Casablanca
+GTB Standard Time: Europe/Athens
+Hawaiian Standard Time: Pacific/Honolulu
+India Standard Time: Asia/Calcutta
+Iran Standard Time: Asia/Tehran
+Israel Standard Time: Asia/Jerusalem
+Jordan Standard Time: Asia/Amman
+Korea Standard Time: Asia/Seoul
+Mexico Standard Time: America/Mexico_City
+Mid-Atlantic Standard Time: Atlantic/South_Georgia
+Middle East Standard Time: Asia/Beirut
+Montevideo Standard Time: America/Montevideo
+Mountain Standard Time: America/Denver
+Mountain Standard Time (Mexico): America/Chihuahua
+Myanmar Standard Time: Asia/Rangoon
+N. Central Asia Standard Time: Asia/Novosibirsk
+Namibia Standard Time: Africa/Windhoek
+Nepal Standard Time: Asia/Katmandu
+New Zealand Standard Time: Pacific/Auckland
+Newfoundland Standard Time: America/St_Johns
+North Asia East Standard Time: Asia/Ulaanbaatar
+North Asia Standard Time: Asia/Krasnoyarsk
+Pacific SA Standard Time: America/Santiago
+Pacific Standard Time: America/Los_Angeles
+Pacific Standard Time (Mexico): America/Tijuana
+Romance Standard Time: Europe/Paris
+Russian Standard Time: Europe/Moscow
+SA Eastern Standard Time: America/Argentina/Buenos_Aires
+SA Pacific Standard Time: America/Bogota
+SA Western Standard Time: America/Caracas
+Samoa Standard Time: Pacific/Apia
+Saudi Arabia Standard Time: Asia/Riyadh
+SE Asia Standard Time: Asia/Bangkok
+Singapore Standard Time: Asia/Singapore
+South Africa Standard Time: Africa/Johannesburg
+Sri Lanka Standard Time: Asia/Colombo
+Sydney Standard Time: Australia/Sydney
+Taipei Standard Time: Asia/Taipei
+Tasmania Standard Time: Australia/Hobart
+Tokyo Standard Time: Asia/Tokyo
+Tonga Standard Time: Pacific/Tongatapu
+US Eastern Standard Time: America/Indiana/Indianapolis
+US Mountain Standard Time: America/Phoenix
+Vladivostok Standard Time: Asia/Vladivostok
+W. Australia Standard Time: Australia/Perth
+W. Central Africa Standard Time: Africa/Kinshasa
+W. Europe Standard Time: Europe/Berlin
+West Asia Standard Time: Asia/Karachi
+West Pacific Standard Time: Pacific/Guam
+Western Brazilian Standard Time: America/Rio_Branco
+Yakutsk Standard Time: Asia/Yakutsk
diff --git a/calendar/base/src/calAlarm.js b/calendar/base/src/calAlarm.js
new file mode 100644
index 000000000..11e9c12f8
--- /dev/null
+++ b/calendar/base/src/calAlarm.js
@@ -0,0 +1,698 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var ALARM_RELATED_ABSOLUTE = Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE;
+var ALARM_RELATED_START = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+var ALARM_RELATED_END = Components.interfaces.calIAlarm.ALARM_RELATED_END;
+
+function calAlarm() {
+ this.wrappedJSObject = this;
+ this.mProperties = new calPropertyBag();
+ this.mPropertyParams = {};
+ this.mAttendees = [];
+ this.mAttachments = [];
+}
+
+var calAlarmClassID = Components.ID("{b8db7c7f-c168-4e11-becb-f26c1c4f5f8f}");
+var calAlarmInterfaces = [Components.interfaces.calIAlarm];
+calAlarm.prototype = {
+
+ mProperties: null,
+ mPropertyParams: null,
+ mAction: null,
+ mAbsoluteDate: null,
+ mOffset: null,
+ mDuration: null,
+ mAttendees: null,
+ mAttachments: null,
+ mSummary: null,
+ mDescription: null,
+ mLastAck: null,
+ mImmutable: false,
+ mRelated: 0,
+ mRepeat: 0,
+
+ classID: calAlarmClassID,
+ QueryInterface: XPCOMUtils.generateQI(calAlarmInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calAlarmClassID,
+ contractID: "@mozilla.org/calendar/alarm;1",
+ classDescription: "Describes a VALARM",
+ interfaces: calAlarmInterfaces
+ }),
+
+ /**
+ * calIAlarm
+ */
+
+ ensureMutable: function() {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ },
+
+ get isMutable() {
+ return !this.mImmutable;
+ },
+
+ makeImmutable: function() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ const objectMembers = ["mAbsoluteDate",
+ "mOffset",
+ "mDuration",
+ "mLastAck"];
+ for (let member of objectMembers) {
+ if (this[member] && this[member].isMutable) {
+ this[member].makeImmutable();
+ }
+ }
+
+ // Properties
+ let e = this.mProperties.enumerator;
+ while (e.hasMoreElements()) {
+ let prop = e.getNext();
+
+ if (prop.value instanceof Components.interfaces.calIDateTime) {
+ if (prop.value.isMutable) {
+ prop.value.makeImmutable();
+ }
+ }
+ }
+
+ this.mImmutable = true;
+ },
+
+ clone: function() {
+ let cloned = new calAlarm();
+
+ cloned.mImmutable = false;
+
+ const simpleMembers = ["mAction",
+ "mSummary",
+ "mDescription",
+ "mRelated",
+ "mRepeat"];
+
+ const arrayMembers = ["mAttendees",
+ "mAttachments"];
+
+ const objectMembers = ["mAbsoluteDate",
+ "mOffset",
+ "mDuration",
+ "mLastAck"];
+
+ for (let member of simpleMembers) {
+ cloned[member] = this[member];
+ }
+
+ for (let member of arrayMembers) {
+ let newArray = [];
+ for (let oldElem of this[member]) {
+ newArray.push(oldElem.clone());
+ }
+ cloned[member] = newArray;
+ }
+
+ for (let member of objectMembers) {
+ if (this[member] && this[member].clone) {
+ cloned[member] = this[member].clone();
+ } else {
+ cloned[member] = this[member];
+ }
+ }
+
+ // X-Props
+ cloned.mProperties = new calPropertyBag();
+ for (let [name, value] of this.mProperties) {
+ if (value instanceof Components.interfaces.calIDateTime) {
+ value = value.clone();
+ }
+
+ cloned.mProperties.setProperty(name, value);
+
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ let newBucket = {};
+ for (let param in propBucket) {
+ newBucket[param] = propBucket[param];
+ }
+ cloned.mPropertyParams[name] = newBucket;
+ }
+ }
+ return cloned;
+ },
+
+
+ get related() {
+ return this.mRelated;
+ },
+ set related(aValue) {
+ this.ensureMutable();
+ switch (aValue) {
+ case ALARM_RELATED_ABSOLUTE:
+ this.mOffset = null;
+ break;
+ case ALARM_RELATED_START:
+ case ALARM_RELATED_END:
+ this.mAbsoluteDate = null;
+ break;
+ }
+
+ return (this.mRelated = aValue);
+ },
+
+ get action() {
+ return this.mAction || "DISPLAY";
+ },
+ set action(aValue) {
+ this.ensureMutable();
+ return (this.mAction = aValue);
+ },
+
+ get description() {
+ if (this.action == "AUDIO") {
+ return null;
+ }
+ return this.mDescription;
+ },
+ set description(aValue) {
+ this.ensureMutable();
+ return (this.mDescription = aValue);
+ },
+
+ get summary() {
+ if (this.mAction == "DISPLAY" ||
+ this.mAction == "AUDIO") {
+ return null;
+ }
+ return this.mSummary;
+ },
+ set summary(aValue) {
+ this.ensureMutable();
+ return (this.mSummary = aValue);
+ },
+
+ get offset() {
+ return this.mOffset;
+ },
+ set offset(aValue) {
+ if (aValue && !(aValue instanceof Components.interfaces.calIDuration)) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.related != ALARM_RELATED_START &&
+ this.related != ALARM_RELATED_END) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ this.ensureMutable();
+ return (this.mOffset = aValue);
+ },
+
+ get alarmDate() {
+ return this.mAbsoluteDate;
+ },
+ set alarmDate(aValue) {
+ if (aValue && !(aValue instanceof Components.interfaces.calIDateTime)) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ if (this.related != ALARM_RELATED_ABSOLUTE) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ this.ensureMutable();
+ return (this.mAbsoluteDate = aValue);
+ },
+
+ get repeat() {
+ if ((this.mRepeat != 0) ^ (this.mDuration != null)) {
+ return 0;
+ }
+ return this.mRepeat || 0;
+ },
+ set repeat(aValue) {
+ this.ensureMutable();
+ if (aValue === null) {
+ this.mRepeat = null;
+ } else {
+ this.mRepeat = parseInt(aValue, 10);
+ if (isNaN(this.mRepeat)) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ }
+ return aValue;
+ },
+
+ get repeatOffset() {
+ if ((this.mRepeat != 0) ^ (this.mDuration != null)) {
+ return null;
+ }
+ return this.mDuration;
+ },
+ set repeatOffset(aValue) {
+ this.ensureMutable();
+ if (aValue !== null &&
+ !(aValue instanceof Components.interfaces.calIDuration)) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ return (this.mDuration = aValue);
+ },
+
+ get repeatDate() {
+ if (this.related != ALARM_RELATED_ABSOLUTE ||
+ !this.mAbsoluteDate ||
+ !this.mRepeat ||
+ !this.mDuration) {
+ return null;
+ }
+
+ let alarmDate = this.mAbsoluteDate.clone();
+
+ // All Day events are handled as 00:00:00
+ alarmDate.isDate = false;
+ alarmDate.addDuration(this.mDuration);
+ return alarmDate;
+ },
+
+ getAttendees: function(aCount) {
+ let attendees;
+ if (this.action == "AUDIO" || this.action == "DISPLAY") {
+ attendees = [];
+ } else {
+ attendees = this.mAttendees.concat([]);
+ }
+ aCount.value = attendees.length;
+ return attendees;
+ },
+
+ addAttendee: function(aAttendee) {
+ // Make sure its not duplicate
+ this.deleteAttendee(aAttendee);
+
+ // Now check if its valid
+ if (this.action == "AUDIO" || this.action == "DISPLAY") {
+ throw new Error("Alarm type AUDIO/DISPLAY may not have attendees");
+ }
+
+ // And add it (again)
+ this.mAttendees.push(aAttendee);
+ },
+
+ deleteAttendee: function(aAttendee) {
+ let deleteId = aAttendee.id;
+ for (let i = 0; i < this.mAttendees.length; i++) {
+ if (this.mAttendees[i].id == deleteId) {
+ this.mAttendees.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ clearAttendees: function() {
+ this.mAttendees = [];
+ },
+
+ getAttachments: function(aCount) {
+ let attachments;
+ if (this.action == "AUDIO") {
+ attachments = (this.mAttachments.length ? [this.mAttachments[0]] : []);
+ } else if (this.action == "DISPLAY") {
+ attachments = [];
+ } else {
+ attachments = this.mAttachments.concat([]);
+ }
+ aCount.value = attachments.length;
+ return attachments;
+ },
+
+ addAttachment: function(aAttachment) {
+ // Make sure its not duplicate
+ this.deleteAttachment(aAttachment);
+
+ // Now check if its valid
+ if (this.action == "AUDIO" && this.mAttachments.length) {
+ throw new Error("Alarm type AUDIO may only have one attachment");
+ } else if (this.action == "DISPLAY") {
+ throw new Error("Alarm type DISPLAY may not have attachments");
+ }
+
+ // And add it (again)
+ this.mAttachments.push(aAttachment);
+ },
+
+ deleteAttachment: function(aAttachment) {
+ let deleteHash = aAttachment.hashId;
+ for (let i = 0; i < this.mAttachments.length; i++) {
+ if (this.mAttachments[i].hashId == deleteHash) {
+ this.mAttachments.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ clearAttachments: function() {
+ this.mAttachments = [];
+ },
+
+ get icalString() {
+ let comp = this.icalComponent;
+ return (comp ? comp.serializeToICS() : "");
+ },
+ set icalString(val) {
+ this.ensureMutable();
+ return (this.icalComponent = getIcsService().parseICS(val, null));
+ },
+
+ promotedProps: {
+ "ACTION": "action",
+ "TRIGGER": "offset",
+ "REPEAT": "repeat",
+ "DURATION": "duration",
+ "SUMMARY": "summary",
+ "DESCRIPTION": "description",
+ "X-MOZ-LASTACK": "lastAck"
+ },
+
+ get icalComponent() {
+ let icssvc = getIcsService();
+ let comp = icssvc.createIcalComponent("VALARM");
+
+ // Set up action (REQUIRED)
+ let actionProp = icssvc.createIcalProperty("ACTION");
+ actionProp.value = this.action;
+ comp.addProperty(actionProp);
+
+ // Set up trigger (REQUIRED)
+ let triggerProp = icssvc.createIcalProperty("TRIGGER");
+ if (this.related == ALARM_RELATED_ABSOLUTE && this.mAbsoluteDate) {
+ // Set the trigger to a specific datetime
+ triggerProp.setParameter("VALUE", "DATE-TIME");
+ triggerProp.valueAsDatetime = this.mAbsoluteDate.getInTimezone(cal.UTC());
+ } else if (this.related != ALARM_RELATED_ABSOLUTE && this.mOffset) {
+ triggerProp.valueAsIcalString = this.mOffset.icalString;
+ if (this.related == ALARM_RELATED_END) {
+ // An alarm related to the end of the event.
+ triggerProp.setParameter("RELATED", "END");
+ }
+ } else {
+ // No offset or absolute date is not valid.
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ comp.addProperty(triggerProp);
+
+ // Set up repeat and duration (OPTIONAL, but if one exists, the other
+ // MUST also exist)
+ if (this.repeat && this.repeatOffset) {
+ let repeatProp = icssvc.createIcalProperty("REPEAT");
+ let durationProp = icssvc.createIcalProperty("DURATION");
+
+ repeatProp.value = this.repeat;
+ durationProp.valueAsIcalString = this.repeatOffset.icalString;
+
+ comp.addProperty(repeatProp);
+ comp.addProperty(durationProp);
+ }
+
+ // Set up attendees (REQUIRED for EMAIL action)
+ /* TODO should we be strict here?
+ if (this.action == "EMAIL" && !this.getAttendees({}).length) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ } */
+ for (let attendee of this.getAttendees({})) {
+ comp.addProperty(attendee.icalProperty);
+ }
+
+ /* TODO should we be strict here?
+ if (this.action == "EMAIL" && !this.attachments.length) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ } */
+
+ for (let attachment of this.getAttachments({})) {
+ comp.addProperty(attachment.icalProperty);
+ }
+
+ // Set up summary (REQUIRED for EMAIL)
+ if (this.summary || this.action == "EMAIL") {
+ let summaryProp = icssvc.createIcalProperty("SUMMARY");
+ // Summary needs to have a non-empty value
+ summaryProp.value = this.summary ||
+ calGetString("calendar", "alarmDefaultSummary");
+ comp.addProperty(summaryProp);
+ }
+
+ // Set up the description (REQUIRED for DISPLAY and EMAIL)
+ if (this.description ||
+ this.action == "DISPLAY" ||
+ this.action == "EMAIL") {
+ let descriptionProp = icssvc.createIcalProperty("DESCRIPTION");
+ // description needs to have a non-empty value
+ descriptionProp.value = this.description ||
+ calGetString("calendar", "alarmDefaultDescription");
+ comp.addProperty(descriptionProp);
+ }
+
+ // Set up lastAck
+ if (this.lastAck) {
+ let lastAckProp = icssvc.createIcalProperty("X-MOZ-LASTACK");
+ lastAckProp.value = this.lastAck;
+ comp.addProperty(lastAckProp);
+ }
+
+ // Set up X-Props. mProperties contains only non-promoted props
+ // eslint-disable-next-line array-bracket-spacing
+ for (let [propName, ] of this.mProperties) {
+ let icalprop = icssvc.createIcalProperty(propName);
+ icalprop.value = this.mProperties.getProperty(propName);
+
+ // Add parameters
+ let propBucket = this.mPropertyParams[propName];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName,
+ propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid alarm parameter value " + paramName + "=" + propBucket[paramName]);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ comp.addProperty(icalprop);
+ }
+ return comp;
+ },
+ set icalComponent(aComp) {
+ this.ensureMutable();
+ if (!aComp || aComp.componentType != "VALARM") {
+ // Invalid Component
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ let actionProp = aComp.getFirstProperty("ACTION");
+ let triggerProp = aComp.getFirstProperty("TRIGGER");
+ let repeatProp = aComp.getFirstProperty("REPEAT");
+ let durationProp = aComp.getFirstProperty("DURATION");
+ let summaryProp = aComp.getFirstProperty("SUMMARY");
+ let descriptionProp = aComp.getFirstProperty("DESCRIPTION");
+ let lastAckProp = aComp.getFirstProperty("X-MOZ-LASTACK");
+
+ if (actionProp) {
+ this.action = actionProp.value;
+ } else {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ if (triggerProp) {
+ if (triggerProp.getParameter("VALUE") == "DATE-TIME") {
+ this.mAbsoluteDate = triggerProp.valueAsDatetime;
+ this.related = ALARM_RELATED_ABSOLUTE;
+ } else {
+ this.mOffset = cal.createDuration(triggerProp.valueAsIcalString);
+
+ let related = triggerProp.getParameter("RELATED");
+ this.related = (related == "END" ? ALARM_RELATED_END : ALARM_RELATED_START);
+ }
+ } else {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ if (durationProp && repeatProp) {
+ this.repeatOffset = cal.createDuration(durationProp.valueAsIcalString);
+ this.repeat = repeatProp.value;
+ } else if (durationProp || repeatProp) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ } else {
+ this.repeatOffset = null;
+ this.repeat = 0;
+ }
+
+ // Set up attendees
+ this.clearAttendees();
+ for (let attendeeProp of cal.ical.propertyIterator(aComp, "ATTENDEE")) {
+ let attendee = cal.createAttendee();
+ attendee.icalProperty = attendeeProp;
+ this.addAttendee(attendee);
+ }
+
+ // Set up attachments
+ this.clearAttachments();
+ for (let attachProp of cal.ical.propertyIterator(aComp, "ATTACH")) {
+ let attach = cal.createAttachment();
+ attach.icalProperty = attachProp;
+ this.addAttachment(attach);
+ }
+
+ // Set up summary
+ this.summary = (summaryProp ? summaryProp.value : null);
+
+ // Set up description
+ this.description = (descriptionProp ? descriptionProp.value : null);
+
+ // Set up the alarm lastack. We can't use valueAsDatetime here since
+ // the default for an X-Prop is TEXT and in older versions we didn't set
+ // VALUE=DATE-TIME.
+ this.lastAck = (lastAckProp ? cal.createDateTime(lastAckProp.valueAsIcalString) : null);
+
+ this.mProperties = new calPropertyBag();
+ this.mPropertyParams = {};
+
+ // Other properties
+ for (let prop of cal.ical.propertyIterator(aComp)) {
+ if (!this.promotedProps[prop.propertyName]) {
+ this.setProperty(prop.propertyName, prop.value);
+
+ for (let [paramName, param] of cal.ical.paramIterator(prop)) {
+ if (!(prop.propertyName in this.mPropertyParams)) {
+ this.mPropertyParams[prop.propertyName] = {};
+ }
+ this.mPropertyParams[prop.propertyName][paramName] = param;
+ }
+ }
+ }
+ return aComp;
+ },
+
+ hasProperty: function(aName) {
+ return (this.getProperty(aName.toUpperCase()) != null);
+ },
+
+ getProperty: function(aName) {
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ return this[this.promotedProps[name]];
+ } else {
+ return this.mProperties.getProperty(name);
+ }
+ },
+
+ setProperty: function(aName, aValue) {
+ this.ensureMutable();
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ this[this.promotedProps[name]] = aValue;
+ } else {
+ this.mProperties.setProperty(name, aValue);
+ }
+ return aValue;
+ },
+
+ deleteProperty: function(aName) {
+ this.ensureMutable();
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ this[this.promotedProps[name]] = null;
+ } else {
+ this.mProperties.deleteProperty(name);
+ }
+ },
+
+ get propertyEnumerator() {
+ return this.mProperties.enumerator;
+ },
+
+ toString: function(aItem) {
+ function getItemBundleStringName(aPrefix) {
+ if (!aItem || isEvent(aItem)) {
+ return aPrefix + "Event";
+ } else if (isToDo(aItem)) {
+ return aPrefix + "Task";
+ } else {
+ return aPrefix;
+ }
+ }
+
+ if (this.related == ALARM_RELATED_ABSOLUTE && this.mAbsoluteDate) {
+ // this is an absolute alarm. Use the calendar default timezone and
+ // format it.
+ let formatter = cal.getDateFormatter();
+ let formatDate = this.mAbsoluteDate.getInTimezone(cal.calendarDefaultTimezone());
+ return formatter.formatDateTime(formatDate);
+ } else if (this.related != ALARM_RELATED_ABSOLUTE && this.mOffset) {
+ // Relative alarm length
+ let alarmlen = Math.abs(this.mOffset.inSeconds / 60);
+ if (alarmlen == 0) {
+ // No need to get the other information if the alarm is at the start
+ // of the event/task.
+ if (this.related == ALARM_RELATED_START) {
+ return calGetString("calendar-alarms",
+ getItemBundleStringName("reminderTitleAtStart"));
+ } else if (this.related == ALARM_RELATED_END) {
+ return calGetString("calendar-alarms",
+ getItemBundleStringName("reminderTitleAtEnd"));
+ }
+ }
+
+ let unit;
+ if (alarmlen % 1440 == 0) {
+ // Alarm is in days
+ unit = "unitDays";
+ alarmlen /= 1440;
+ } else if (alarmlen % 60 == 0) {
+ unit = "unitHours";
+ alarmlen /= 60;
+ } else {
+ unit = "unitMinutes";
+ }
+ let localeUnitString = cal.calGetString("calendar", unit);
+ let unitString = PluralForm.get(alarmlen, localeUnitString)
+ .replace("#1", alarmlen);
+ let originStringName = "reminderCustomOrigin";
+
+ // Origin
+ switch (this.related) {
+ case ALARM_RELATED_START:
+ originStringName += "Begin";
+ break;
+ case ALARM_RELATED_END:
+ originStringName += "End";
+ break;
+ }
+
+ if (this.offset.isNegative) {
+ originStringName += "Before";
+ } else {
+ originStringName += "After";
+ }
+
+ let originString = calGetString("calendar-alarms",
+ getItemBundleStringName(originStringName));
+ return calGetString("calendar-alarms",
+ "reminderCustomTitle",
+ [unitString, originString]);
+ } else {
+ // This is an incomplete alarm, but then again we should never reach
+ // this state.
+ return "[Incomplete calIAlarm]";
+ }
+ }
+};
diff --git a/calendar/base/src/calAlarmMonitor.js b/calendar/base/src/calAlarmMonitor.js
new file mode 100644
index 000000000..9b224dc1f
--- /dev/null
+++ b/calendar/base/src/calAlarmMonitor.js
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+function peekAlarmWindow() {
+ return Services.wm.getMostRecentWindow("Calendar:AlarmWindow");
+}
+
+/**
+ * The alarm monitor takes care of playing the alarm sound and opening one copy
+ * of the calendar-alarm-dialog. Both depend on their respective prefs to be
+ * set. This monitor is only used for DISPLAY type alarms.
+ */
+function calAlarmMonitor() {
+ this.wrappedJSObject = this;
+ this.mAlarms = [];
+
+ this.mSound = Components.classes["@mozilla.org/sound;1"]
+ .createInstance(Components.interfaces.nsISound);
+}
+
+var calAlarmMonitorClassID = Components.ID("{4b7ae030-ed79-11d9-8cd6-0800200c9a66}");
+var calAlarmMonitorInterfaces = [
+ Components.interfaces.nsIObserver,
+ Components.interfaces.calIAlarmServiceObserver
+];
+calAlarmMonitor.prototype = {
+ mAlarms: null,
+
+ // This is a work-around for the fact that there is a delay between when
+ // we call openWindow and when it appears via getMostRecentWindow. If an
+ // alarm is fired in that time-frame, it will actually end up in another window.
+ mWindowOpening: null,
+
+ // nsISound instance used for playing all sounds
+ mSound: null,
+
+ classID: calAlarmMonitorClassID,
+ QueryInterface: XPCOMUtils.generateQI(calAlarmMonitorInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ contractID: "@mozilla.org/calendar/alarm-monitor;1",
+ classDescription: "Calendar Alarm Monitor",
+ classID: calAlarmMonitorClassID,
+ interfaces: calAlarmMonitorInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ /**
+ * nsIObserver
+ */
+ observe: function(aSubject, aTopic, aData) {
+ let alarmService = Components.classes["@mozilla.org/calendar/alarm-service;1"]
+ .getService(Components.interfaces.calIAlarmService);
+ switch (aTopic) {
+ case "alarm-service-startup":
+ alarmService.addObserver(this);
+ break;
+ case "alarm-service-shutdown":
+ alarmService.removeObserver(this);
+ break;
+ }
+ },
+
+ /**
+ * calIAlarmServiceObserver
+ */
+ onAlarm: function(aItem, aAlarm) {
+ if (aAlarm.action != "DISPLAY") {
+ // This monitor only looks for DISPLAY alarms.
+ return;
+ }
+
+ this.mAlarms.push([aItem, aAlarm]);
+
+ if (Preferences.get("calendar.alarms.playsound", true)) {
+ // We want to make sure the user isn't flooded with alarms so we
+ // limit this using a preference. For example, if the user has 20
+ // events that fire an alarm in the same minute, then the alarm
+ // sound will only play 5 times. All alarms will be shown in the
+ // dialog nevertheless.
+ let maxAlarmSoundCount = Preferences.get("calendar.alarms.maxsoundsperminute", 5);
+ let now = new Date();
+
+ if (!this.mLastAlarmSoundDate ||
+ (now - this.mLastAlarmSoundDate >= 60000)) {
+ // Last alarm was long enough ago, reset counters. Note
+ // subtracting JSDate results in microseconds.
+ this.mAlarmSoundCount = 0;
+ this.mLastAlarmSoundDate = now;
+ } else {
+ // Otherwise increase the counter
+ this.mAlarmSoundCount++;
+ }
+
+ if (maxAlarmSoundCount > this.mAlarmSoundCount) {
+ // Only ring the alarm sound if we haven't hit the max count.
+ try {
+ let soundURL = Preferences.get("calendar.alarms.soundURL", null);
+ if (soundURL && soundURL.length > 0) {
+ soundURL = makeURL(soundURL);
+ this.mSound.play(soundURL);
+ } else {
+ this.mSound.beep();
+ }
+ } catch (exc) {
+ cal.ERROR("Error playing alarm sound: " + exc);
+ }
+ }
+ }
+
+ if (!Preferences.get("calendar.alarms.show", true)) {
+ return;
+ }
+
+ let calAlarmWindow = peekAlarmWindow();
+ if (!calAlarmWindow && (!this.mWindowOpening ||
+ this.mWindowOpening.closed)) {
+ this.mWindowOpening = Services.ww.openWindow(
+ null,
+ "chrome://calendar/content/calendar-alarm-dialog.xul",
+ "_blank",
+ "chrome,dialog=yes,all,resizable",
+ this);
+ }
+ if (!this.mWindowOpening) {
+ calAlarmWindow.addWidgetFor(aItem, aAlarm);
+ }
+ },
+
+ window_onLoad: function() {
+ let calAlarmWindow = this.mWindowOpening;
+ this.mWindowOpening = null;
+ if (this.mAlarms.length > 0) {
+ for (let [item, alarm] of this.mAlarms) {
+ calAlarmWindow.addWidgetFor(item, alarm);
+ }
+ } else {
+ // Uh oh, it seems the alarms were removed even before the window
+ // finished loading. Looks like we can close it again
+ calAlarmWindow.closeIfEmpty();
+ }
+ },
+
+ onRemoveAlarmsByItem: function(aItem) {
+ let calAlarmWindow = peekAlarmWindow();
+ this.mAlarms = this.mAlarms.filter(([thisItem, alarm]) => {
+ let ret = (aItem.hashId != thisItem.hashId);
+ if (!ret && calAlarmWindow) { // window is open
+ calAlarmWindow.removeWidgetFor(thisItem, alarm);
+ }
+ return ret;
+ });
+ },
+
+ onRemoveAlarmsByCalendar: function(calendar) {
+ let calAlarmWindow = peekAlarmWindow();
+ this.mAlarms = this.mAlarms.filter(([thisItem, alarm]) => {
+ let ret = (calendar.id != thisItem.calendar.id);
+
+ if (!ret && calAlarmWindow) { // window is open
+ calAlarmWindow.removeWidgetFor(thisItem, alarm);
+ }
+ return ret;
+ });
+ },
+
+ onAlarmsLoaded: function(aCalendar) {
+ // the alarm dialog won't close while alarms are loading, check again now
+ let calAlarmWindow = peekAlarmWindow();
+ if (calAlarmWindow && this.mAlarms.length == 0) {
+ calAlarmWindow.closeIfEmpty();
+ }
+ }
+};
diff --git a/calendar/base/src/calAlarmService.js b/calendar/base/src/calAlarmService.js
new file mode 100644
index 000000000..c00195f88
--- /dev/null
+++ b/calendar/base/src/calAlarmService.js
@@ -0,0 +1,581 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
+
+var kHoursBetweenUpdates = 6;
+
+function nowUTC() {
+ return cal.jsDateToDateTime(new Date()).getInTimezone(cal.UTC());
+}
+
+function newTimerWithCallback(aCallback, aDelay, aRepeating) {
+ let timer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+
+ timer.initWithCallback(aCallback,
+ aDelay,
+ (aRepeating ? timer.TYPE_REPEATING_PRECISE : timer.TYPE_ONE_SHOT));
+ return timer;
+}
+
+function calAlarmService() {
+ this.wrappedJSObject = this;
+
+ this.mLoadedCalendars = {};
+ this.mTimerMap = {};
+ this.mObservers = new calListenerBag(Components.interfaces.calIAlarmServiceObserver);
+
+ this.calendarObserver = {
+ alarmService: this,
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIObserver]),
+
+ // calIObserver:
+ onStartBatch: function() { },
+ onEndBatch: function() { },
+ onLoad: function(calendar) {
+ // ignore any onLoad events until initial getItems() call of startup has finished:
+ if (calendar && this.alarmService.mLoadedCalendars[calendar.id]) {
+ // a refreshed calendar signals that it has been reloaded
+ // (and cannot notify detailed changes), thus reget all alarms of it:
+ this.alarmService.initAlarms([calendar]);
+ }
+ },
+
+ onAddItem: function(aItem) {
+ this.alarmService.addAlarmsForOccurrences(aItem);
+ },
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (!aNewItem.recurrenceId) {
+ // deleting an occurrence currently calls modifyItem(newParent, *oldOccurrence*)
+ aOldItem = aOldItem.parentItem;
+ }
+
+ this.onDeleteItem(aOldItem);
+ this.onAddItem(aNewItem);
+ },
+ onDeleteItem: function(aDeletedItem) {
+ this.alarmService.removeAlarmsForOccurrences(aDeletedItem);
+ },
+ onError: function(aCalendar, aErrNo, aMessage) {},
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "suppressAlarms":
+ case "disabled":
+ this.alarmService.initAlarms([aCalendar]);
+ break;
+ }
+ },
+ onPropertyDeleting: function(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName);
+ }
+ };
+
+ this.calendarManagerObserver = {
+ alarmService: this,
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICalendarManagerObserver]),
+
+ onCalendarRegistered: function(aCalendar) {
+ this.alarmService.observeCalendar(aCalendar);
+ // initial refresh of alarms for new calendar:
+ this.alarmService.initAlarms([aCalendar]);
+ },
+ onCalendarUnregistering: function(aCalendar) {
+ // XXX todo: we need to think about calendar unregistration;
+ // there may still be dangling items (-> alarm dialog),
+ // dismissing those alarms may write data...
+ this.alarmService.unobserveCalendar(aCalendar);
+ },
+ onCalendarDeleting: function(aCalendar) {}
+ };
+}
+
+var calAlarmServiceClassID = Components.ID("{7a9200dd-6a64-4fff-a798-c5802186e2cc}");
+var calAlarmServiceInterfaces = [
+ Components.interfaces.calIAlarmService,
+ Components.interfaces.nsIObserver
+];
+calAlarmService.prototype = {
+ mRangeStart: null,
+ mRangeEnd: null,
+ mUpdateTimer: null,
+ mStarted: false,
+ mTimerMap: null,
+ mObservers: null,
+ mTimezone: null,
+
+ classID: calAlarmServiceClassID,
+ QueryInterface: XPCOMUtils.generateQI(calAlarmServiceInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calAlarmServiceClassID,
+ contractID: "@mozilla.org/calendar/alarm-service;1",
+ classDescription: "Calendar Alarm Service",
+ interfaces: calAlarmServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ /**
+ * nsIObserver
+ */
+ observe: function(aSubject, aTopic, aData) {
+ // This will also be called on app-startup, but nothing is done yet, to
+ // prevent unwanted dialogs etc. See bug 325476 and 413296
+ if (aTopic == "profile-after-change" || aTopic == "wake_notification") {
+ this.shutdown();
+ this.startup();
+ }
+ if (aTopic == "xpcom-shutdown") {
+ this.shutdown();
+ }
+ },
+
+ /**
+ * calIAlarmService APIs
+ */
+ get timezone() {
+ // TODO Do we really need this? Do we ever set the timezone to something
+ // different than the default timezone?
+ return this.mTimezone || calendarDefaultTimezone();
+ },
+
+ set timezone(aTimezone) {
+ return (this.mTimezone = aTimezone);
+ },
+
+ snoozeAlarm: function(aItem, aAlarm, aDuration) {
+ // Right now we only support snoozing all alarms for the given item for
+ // aDuration.
+
+ // Make sure we're working with the parent, otherwise we'll accidentally
+ // create an exception
+ let newEvent = aItem.parentItem.clone();
+ let alarmTime = nowUTC();
+
+ // Set the last acknowledged time to now.
+ newEvent.alarmLastAck = alarmTime;
+
+ alarmTime = alarmTime.clone();
+ alarmTime.addDuration(aDuration);
+
+ if (aItem.parentItem == aItem) {
+ newEvent.setProperty("X-MOZ-SNOOZE-TIME", alarmTime.icalString);
+ } else {
+ // This is the *really* hard case where we've snoozed a single
+ // instance of a recurring event. We need to not only know that
+ // there was a snooze, but also which occurrence was snoozed. Part
+ // of me just wants to create a local db of snoozes here...
+ newEvent.setProperty("X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime,
+ alarmTime.icalString);
+ }
+ // calling modifyItem will cause us to get the right callback
+ // and update the alarm properly
+ return newEvent.calendar.modifyItem(newEvent, aItem.parentItem, null);
+ },
+
+ dismissAlarm: function(aItem, aAlarm) {
+ let now = nowUTC();
+ // We want the parent item, otherwise we're going to accidentally create an
+ // exception. We've relnoted (for 0.1) the slightly odd behavior this can
+ // cause if you move an event after dismissing an alarm
+ let oldParent = aItem.parentItem;
+ let newParent = oldParent.clone();
+ newParent.alarmLastAck = now;
+ // Make sure to clear out any snoozes that were here.
+ if (aItem.recurrenceId) {
+ newParent.deleteProperty("X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime);
+ } else {
+ newParent.deleteProperty("X-MOZ-SNOOZE-TIME");
+ }
+ return newParent.calendar.modifyItem(newParent, oldParent, null);
+ },
+
+ addObserver: function(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+
+ removeObserver: function(aObserver) {
+ this.mObservers.remove(aObserver);
+ },
+
+ startup: function() {
+ if (this.mStarted) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "profile-after-change", false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ Services.obs.addObserver(this, "wake_notification", false);
+
+ /* Tell people that we're alive so they can start monitoring alarms.
+ */
+ let notifier = Components.classes["@mozilla.org/embedcomp/appstartup-notifier;1"]
+ .getService(Components.interfaces.nsIObserver);
+ notifier.observe(null, "alarm-service-startup", null);
+
+ getCalendarManager().addObserver(this.calendarManagerObserver);
+
+ for (let calendar of getCalendarManager().getCalendars({})) {
+ this.observeCalendar(calendar);
+ }
+
+ /* set up a timer to update alarms every N hours */
+ let timerCallback = {
+ alarmService: this,
+ notify: function() {
+ let now = nowUTC();
+ let start;
+ if (this.alarmService.mRangeEnd) {
+ // This is a subsequent search, so we got all the past alarms before
+ start = this.alarmService.mRangeEnd.clone();
+ } else {
+ // This is our first search for alarms. We're going to look for
+ // alarms +/- 1 month from now. If someone sets an alarm more than
+ // a month ahead of an event, or doesn't start Lightning
+ // for a month, they'll miss some, but that's a slim chance
+ start = now.clone();
+ start.month -= Components.interfaces.calIAlarmService.MAX_SNOOZE_MONTHS;
+ this.alarmService.mRangeStart = start.clone();
+ }
+ let until = now.clone();
+ until.month += Components.interfaces.calIAlarmService.MAX_SNOOZE_MONTHS;
+
+ // We don't set timers for every future alarm, only those within 6 hours
+ let end = now.clone();
+ end.hour += kHoursBetweenUpdates;
+ this.alarmService.mRangeEnd = end.getInTimezone(UTC());
+
+ this.alarmService.findAlarms(getCalendarManager().getCalendars({}),
+ start, until);
+ }
+ };
+ timerCallback.notify();
+
+ this.mUpdateTimer = newTimerWithCallback(timerCallback, kHoursBetweenUpdates * 3600000, true);
+
+ this.mStarted = true;
+ },
+
+ shutdown: function() {
+ if (!this.mStarted) {
+ return;
+ }
+
+ /* tell people that we're no longer running */
+ let notifier = Components.classes["@mozilla.org/embedcomp/appstartup-notifier;1"]
+ .getService(Components.interfaces.nsIObserver);
+ notifier.observe(null, "alarm-service-shutdown", null);
+
+ if (this.mUpdateTimer) {
+ this.mUpdateTimer.cancel();
+ this.mUpdateTimer = null;
+ }
+
+ let calmgr = cal.getCalendarManager();
+ calmgr.removeObserver(this.calendarManagerObserver);
+
+ // Stop observing all calendars. This will also clear the timers.
+ for (let calendar of calmgr.getCalendars({})) {
+ this.unobserveCalendar(calendar);
+ }
+
+ this.mRangeEnd = null;
+
+ Services.obs.removeObserver(this, "profile-after-change");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.obs.removeObserver(this, "wake_notification");
+
+ this.mStarted = false;
+ },
+
+ observeCalendar: function(calendar) {
+ calendar.addObserver(this.calendarObserver);
+ },
+
+ unobserveCalendar: function(calendar) {
+ calendar.removeObserver(this.calendarObserver);
+ this.disposeCalendarTimers([calendar]);
+ this.mObservers.notify("onRemoveAlarmsByCalendar", [calendar]);
+ },
+
+ addAlarmsForItem: function(aItem) {
+ if (cal.isToDo(aItem) && aItem.isCompleted) {
+ // If this is a task and it is completed, don't add the alarm.
+ return;
+ }
+
+ let showMissed = Preferences.get("calendar.alarms.showmissed", true);
+
+ let alarms = aItem.getAlarms({});
+ for (let alarm of alarms) {
+ let alarmDate = cal.alarms.calculateAlarmDate(aItem, alarm);
+
+ if (!alarmDate || alarm.action != "DISPLAY") {
+ // Only take care of DISPLAY alarms with an alarm date.
+ continue;
+ }
+
+ // Handle all day events. This is kinda weird, because they don't have
+ // a well defined startTime. We just consider the start/end to be
+ // midnight in the user's timezone.
+ if (alarmDate.isDate) {
+ alarmDate = alarmDate.getInTimezone(this.timezone);
+ alarmDate.isDate = false;
+ }
+ alarmDate = alarmDate.getInTimezone(UTC());
+
+ // Check for snooze
+ let snoozeDate;
+ if (aItem.parentItem == aItem) {
+ snoozeDate = aItem.getProperty("X-MOZ-SNOOZE-TIME");
+ } else {
+ snoozeDate = aItem.parentItem.getProperty("X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime);
+ }
+
+ if (snoozeDate && !(snoozeDate instanceof Components.interfaces.calIDateTime)) {
+ snoozeDate = cal.createDateTime(snoozeDate);
+ }
+
+ // an alarm can only be snoozed to a later time, if earlier it's from another alarm.
+ if (snoozeDate && snoozeDate.compare(alarmDate) > 0) {
+ // If the alarm was snoozed, the snooze time is more important.
+ alarmDate = snoozeDate;
+ }
+
+ let now = nowUTC();
+ if (alarmDate.timezone.isFloating) {
+ now = cal.now();
+ now.timezone = floating();
+ }
+
+ if (alarmDate.compare(now) >= 0) {
+ // We assume that future alarms haven't been acknowledged
+ // Delay is in msec, so don't forget to multiply
+ let timeout = alarmDate.subtractDate(now).inSeconds * 1000;
+
+ // No sense in keeping an extra timeout for an alarm thats past
+ // our range.
+ let timeUntilRefresh = this.mRangeEnd.subtractDate(now).inSeconds * 1000;
+ if (timeUntilRefresh < timeout) {
+ continue;
+ }
+
+ this.addTimer(aItem, alarm, timeout);
+ } else if (showMissed) {
+ // This alarm is in the past. See if it has been previously ack'd.
+ let lastAck = aItem.parentItem.alarmLastAck;
+ if (lastAck && lastAck.compare(alarmDate) >= 0) {
+ // The alarm was previously dismissed or snoozed, no further
+ // action required.
+ continue;
+ } else {
+ // The alarm was not snoozed or dismissed, fire it now.
+ this.alarmFired(aItem, alarm);
+ }
+ }
+ }
+ },
+
+ removeAlarmsForItem: function(aItem) {
+ // make sure already fired alarms are purged out of the alarm window:
+ this.mObservers.notify("onRemoveAlarmsByItem", [aItem]);
+ // Purge alarms specifically for this item (i.e exception)
+ for (let alarm of aItem.getAlarms({})) {
+ this.removeTimer(aItem, alarm);
+ }
+ },
+
+ getOccurrencesInRange: function(aItem) {
+ // We search 1 month in each direction for alarms. Therefore,
+ // we need occurrences between initial start date and 1 month from now
+ let until = nowUTC();
+ until.month += 1;
+
+ if (aItem && aItem.recurrenceInfo) {
+ return aItem.recurrenceInfo.getOccurrences(this.mRangeStart, until, 0, {});
+ } else {
+ return cal.checkIfInRange(aItem, this.mRangeStart, until) ? [aItem] : [];
+ }
+ },
+
+ addAlarmsForOccurrences: function(aParentItem) {
+ let occs = this.getOccurrencesInRange(aParentItem);
+
+ // Add an alarm for each occurrence
+ occs.forEach(this.addAlarmsForItem, this);
+ },
+
+ removeAlarmsForOccurrences: function(aParentItem) {
+ let occs = this.getOccurrencesInRange(aParentItem);
+
+ // Remove alarm for each occurrence
+ occs.forEach(this.removeAlarmsForItem, this);
+ },
+
+ addTimer: function(aItem, aAlarm, aTimeout) {
+ this.mTimerMap[aItem.calendar.id] =
+ this.mTimerMap[aItem.calendar.id] || {};
+ this.mTimerMap[aItem.calendar.id][aItem.hashId] =
+ this.mTimerMap[aItem.calendar.id][aItem.hashId] || {};
+
+ let self = this;
+ let alarmTimerCallback = {
+ notify: function() {
+ self.alarmFired(aItem, aAlarm);
+ }
+ };
+
+ let timer = newTimerWithCallback(alarmTimerCallback, aTimeout, false);
+ this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString] = timer;
+ },
+
+ removeTimer: function(aItem, aAlarm) {
+ /* Is the calendar in the timer map */
+ if (aItem.calendar.id in this.mTimerMap &&
+ /* ...and is the item in the calendar map */
+ aItem.hashId in this.mTimerMap[aItem.calendar.id] &&
+ /* ...and is the alarm in the item map ? */
+ aAlarm.icalString in this.mTimerMap[aItem.calendar.id][aItem.hashId]) {
+ // First cancel the existing timer
+ let timer = this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString];
+ timer.cancel();
+
+ // Remove the alarm from the item map
+ delete this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString];
+
+ // If the item map is empty, remove it from the calendar map
+ if (this.mTimerMap[aItem.calendar.id][aItem.hashId].toSource() == "({})") {
+ delete this.mTimerMap[aItem.calendar.id][aItem.hashId];
+ }
+
+ // If the calendar map is empty, remove it from the timer map
+ if (this.mTimerMap[aItem.calendar.id].toSource() == "({})") {
+ delete this.mTimerMap[aItem.calendar.id];
+ }
+ }
+ },
+
+ disposeCalendarTimers: function(aCalendars) {
+ for (let calendar of aCalendars) {
+ if (calendar.id in this.mTimerMap) {
+ for (let hashId in this.mTimerMap[calendar.id]) {
+ let itemTimerMap = this.mTimerMap[calendar.id][hashId];
+ for (let icalString in itemTimerMap) {
+ let timer = itemTimerMap[icalString];
+ timer.cancel();
+ }
+ }
+ delete this.mTimerMap[calendar.id];
+ }
+ }
+ },
+
+ findAlarms: function(aCalendars, aStart, aUntil) {
+ let getListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ alarmService: this,
+ addRemovePromise: PromiseUtils.defer(),
+ batchCount: 0,
+ results: false,
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ this.addRemovePromise.promise.then((aValue) => {
+ // calendar has been loaded, so until now, onLoad events can be ignored:
+ this.alarmService.mLoadedCalendars[aCalendar.id] = true;
+
+ // notify observers that the alarms for the calendar have been loaded
+ this.alarmService.mObservers.notify("onAlarmsLoaded", [aCalendar]);
+ }, (aReason) => {
+ Components.utils.reportError("Promise was rejected: " + aReason);
+ this.alarmService.mLoadedCalendars[aCalendar.id] = true;
+ this.alarmService.mObservers.notify("onAlarmsLoaded", [aCalendar]);
+ });
+
+ // if no results were returned we still need to resolve the promise
+ if (!this.results) {
+ this.addRemovePromise.resolve();
+ }
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ let promise = this.addRemovePromise;
+ this.batchCount++;
+ this.results = true;
+
+ cal.forEach(aItems, (item) => {
+ try {
+ this.alarmService.removeAlarmsForItem(item);
+ this.alarmService.addAlarmsForItem(item);
+ } catch (ex) {
+ promise.reject(ex);
+ }
+ }, () => {
+ if (--this.batchCount <= 0) {
+ promise.resolve();
+ }
+ });
+ }
+ };
+
+ const calICalendar = Components.interfaces.calICalendar;
+ let filter = calICalendar.ITEM_FILTER_COMPLETED_ALL |
+ calICalendar.ITEM_FILTER_CLASS_OCCURRENCES |
+ calICalendar.ITEM_FILTER_TYPE_ALL;
+
+ for (let calendar of aCalendars) {
+ // assuming that suppressAlarms does not change anymore until refresh:
+ if (!calendar.getProperty("suppressAlarms") &&
+ !calendar.getProperty("disabled")) {
+ this.mLoadedCalendars[calendar.id] = false;
+ calendar.getItems(filter, 0, aStart, aUntil, getListener);
+ } else {
+ this.mLoadedCalendars[calendar.id] = true;
+ this.mObservers.notify("onAlarmsLoaded", [calendar]);
+ }
+ }
+ },
+
+ initAlarms: function(aCalendars) {
+ // Purge out all alarm timers belonging to the refreshed/loaded calendars
+ this.disposeCalendarTimers(aCalendars);
+
+ // Purge out all alarms from dialog belonging to the refreshed/loaded calendars
+ for (let calendar of aCalendars) {
+ this.mLoadedCalendars[calendar.id] = false;
+ this.mObservers.notify("onRemoveAlarmsByCalendar", [calendar]);
+ }
+
+ // Total refresh similar to startup. We're going to look for
+ // alarms +/- 1 month from now. If someone sets an alarm more than
+ // a month ahead of an event, or doesn't start Lightning
+ // for a month, they'll miss some, but that's a slim chance
+ let start = nowUTC();
+ let until = start.clone();
+ start.month -= Components.interfaces.calIAlarmService.MAX_SNOOZE_MONTHS;
+ until.month += Components.interfaces.calIAlarmService.MAX_SNOOZE_MONTHS;
+ this.findAlarms(aCalendars, start, until);
+ },
+
+ alarmFired: function(aItem, aAlarm) {
+ if (!aItem.calendar.getProperty("suppressAlarms") &&
+ !aItem.calendar.getProperty("disabled") &&
+ aItem.getProperty("STATUS") != "CANCELLED") {
+ this.mObservers.notify("onAlarm", [aItem, aAlarm]);
+ }
+ },
+
+ get isLoading() {
+ for (let calId in this.mLoadedCalendars) {
+ if (!this.mLoadedCalendars[calId]) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
diff --git a/calendar/base/src/calApplicationUtils.js b/calendar/base/src/calApplicationUtils.js
new file mode 100644
index 000000000..033d4b165
--- /dev/null
+++ b/calendar/base/src/calApplicationUtils.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported launchBrowser */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Launch the given url (string) in the external browser. If an event is passed,
+ * then this is only done on left click and the event propagation is stopped.
+ *
+ * @param url The URL to open, as a string
+ * @param event (optional) The event that caused the URL to open
+ */
+function launchBrowser(url, event) {
+ // Bail out if there is no url set, or an event was passed without left-click
+ if (!url || (event && event.button != 0)) {
+ return;
+ }
+
+ // 0. Prevent people from trying to launch URLs such as javascript:foo();
+ // by only allowing URLs starting with http or https.
+ // XXX: We likely will want to do this using nsIURLs in the future to
+ // prevent sneaky nasty escaping issues, but this is fine for now.
+ if (!url.startsWith("http")) {
+ Components.utils.reportError("launchBrowser: " +
+ "Invalid URL provided: " + url +
+ " Only http:// and https:// URLs are valid.");
+ return;
+ }
+
+ Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Components.interfaces.nsIExternalProtocolService)
+ .loadUrl(Services.io.newURI(url, null, null));
+
+ // Make sure that any default click handlers don't do anything, we have taken
+ // care of all processing
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+}
diff --git a/calendar/base/src/calAttachment.js b/calendar/base/src/calAttachment.js
new file mode 100644
index 000000000..fcd30cfc2
--- /dev/null
+++ b/calendar/base/src/calAttachment.js
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+//
+// calAttachment.js
+//
+function calAttachment() {
+ this.wrappedJSObject = this;
+ this.mProperties = new cal.calPropertyBag();
+}
+
+var calAttachmentClassID = Components.ID("{5f76b352-ab75-4c2b-82c9-9206dbbf8571}");
+var calAttachmentInterfaces = [Components.interfaces.calIAttachment];
+calAttachment.prototype = {
+ mData: null,
+ mHashId: null,
+
+ classID: calAttachmentClassID,
+ QueryInterface: XPCOMUtils.generateQI(calAttachmentInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calAttachmentClassID,
+ contractID: "@mozilla.org/calendar/attachment;1",
+ classDescription: "Calendar Item Attachment",
+ interfaces: calAttachmentInterfaces
+ }),
+
+ get hashId() {
+ if (!this.mHashId) {
+ let cryptoHash = Components.classes["@mozilla.org/security/hash;1"]
+ .createInstance(Components.interfaces.nsICryptoHash);
+
+ let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let data = converter.convertToByteArray(this.rawData, {});
+
+ cryptoHash.init(cryptoHash.MD5);
+ cryptoHash.update(data, data.length);
+ this.mHashId = cryptoHash.finish(true);
+ }
+ return this.mHashId;
+ },
+
+ /**
+ * calIAttachment
+ */
+
+ get uri() {
+ let uri = null;
+ if (this.getParameter("VALUE") != "BINARY") {
+ // If this is not binary data, its likely an uri. Attempt to convert
+ // and throw otherwise.
+ try {
+ uri = makeURL(this.mData);
+ } catch (e) {
+ // Its possible that the uri contains malformed data. Often
+ // callers don't expect an exception here, so we just catch
+ // it and return null.
+ }
+ }
+
+ return uri;
+ },
+ set uri(aUri) {
+ // An uri is the default format, remove any value type parameters
+ this.deleteParameter("VALUE");
+ this.setData(aUri.spec);
+ return aUri;
+ },
+
+ get rawData() {
+ return this.mData;
+ },
+ set rawData(aData) {
+ // Setting the raw data lets us assume this is binary data. Make sure
+ // the value parameter is set
+ this.setParameter("VALUE", "BINARY");
+ return this.setData(aData);
+ },
+
+ get formatType() {
+ return this.getParameter("FMTTYPE");
+ },
+ set formatType(aType) {
+ return this.setParameter("FMTTYPE", aType);
+ },
+
+ get encoding() {
+ return this.getParameter("ENCODING");
+ },
+ set encoding(aValue) {
+ return this.setParameter("ENCODING", aValue);
+ },
+
+ get icalProperty() {
+ let icalatt = cal.getIcsService().createIcalProperty("ATTACH");
+
+ for (let [key, value] of this.mProperties) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attachment parameter value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ if (this.mData) {
+ icalatt.value = this.mData;
+ }
+ return icalatt;
+ },
+
+ set icalProperty(attProp) {
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new cal.calPropertyBag();
+ this.setData(attProp.value);
+
+ for (let [name, value] of cal.ical.paramIterator(attProp)) {
+ this.setParameter(name, value);
+ }
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return (comp ? comp.icalString : "");
+ },
+ set icalString(val) {
+ let prop = cal.getIcsService().createIcalPropertyFromString(val);
+ if (prop.propertyName != "ATTACH") {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+ this.icalProperty = prop;
+ return val;
+ },
+
+ getParameter: function(aName) {
+ return this.mProperties.getProperty(aName);
+ },
+
+ setParameter: function(aName, aValue) {
+ if (aValue || aValue === 0) {
+ return this.mProperties.setProperty(aName, aValue);
+ } else {
+ return this.mProperties.deleteProperty(aName);
+ }
+ },
+
+ deleteParameter: function(aName) {
+ this.mProperties.deleteProperty(aName);
+ },
+
+ clone: function() {
+ let newAttachment = new calAttachment();
+ newAttachment.mData = this.mData;
+ newAttachment.mHashId = this.mHashId;
+ for (let [name, value] of this.mProperties) {
+ newAttachment.mProperties.setProperty(name, value);
+ }
+ return newAttachment;
+ },
+
+ setData: function(aData) {
+ // Sets the data and invalidates the hash so it will be recalculated
+ this.mHashId = null;
+ this.mData = aData;
+ return this.mData;
+ }
+};
diff --git a/calendar/base/src/calAttendee.js b/calendar/base/src/calAttendee.js
new file mode 100644
index 000000000..b3b9e8673
--- /dev/null
+++ b/calendar/base/src/calAttendee.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function calAttendee() {
+ this.wrappedJSObject = this;
+ this.mProperties = new calPropertyBag();
+}
+
+var calAttendeeClassID = Components.ID("{5c8dcaa3-170c-4a73-8142-d531156f664d}");
+var calAttendeeInterfaces = [Components.interfaces.calIAttendee];
+calAttendee.prototype = {
+ classID: calAttendeeClassID,
+ QueryInterface: XPCOMUtils.generateQI(calAttendeeInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calAttendeeClassID,
+ contractID: "@mozilla.org/calendar/attendee;1",
+ classDescription: "Calendar Attendee",
+ interfaces: calAttendeeInterfaces
+ }),
+
+ mImmutable: false,
+ get isMutable() { return !this.mImmutable; },
+
+ modify: function() {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ },
+
+ makeImmutable: function() {
+ this.mImmutable = true;
+ },
+
+ clone: function() {
+ let a = new calAttendee();
+
+ if (this.mIsOrganizer) {
+ a.isOrganizer = true;
+ }
+
+ const allProps = ["id", "commonName", "rsvp", "role",
+ "participationStatus", "userType"];
+ for (let prop of allProps) {
+ a[prop] = this[prop];
+ }
+
+ for (let [key, value] of this.mProperties) {
+ a.setProperty(key, value);
+ }
+
+ return a;
+ },
+ // XXX enforce legal values for our properties;
+
+ icalAttendeePropMap: [
+ { cal: "rsvp", ics: "RSVP" },
+ { cal: "commonName", ics: "CN" },
+ { cal: "participationStatus", ics: "PARTSTAT" },
+ { cal: "userType", ics: "CUTYPE" },
+ { cal: "role", ics: "ROLE" }
+ ],
+
+ mIsOrganizer: false,
+ get isOrganizer() { return this.mIsOrganizer; },
+ set isOrganizer(bool) { this.mIsOrganizer = bool; },
+
+ // icalatt is a calIcalProperty of type attendee
+ set icalProperty(icalatt) {
+ this.modify();
+ this.id = icalatt.valueAsIcalString;
+ this.mIsOrganizer = (icalatt.propertyName == "ORGANIZER");
+
+ let promotedProps = { };
+ for (let prop of this.icalAttendeePropMap) {
+ this[prop.cal] = icalatt.getParameter(prop.ics);
+ // Don't copy these to the property bag.
+ promotedProps[prop.ics] = true;
+ }
+
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new calPropertyBag();
+
+ for (let [name, value] of cal.ical.paramIterator(icalatt)) {
+ if (!promotedProps[name]) {
+ this.setProperty(name, value);
+ }
+ }
+ },
+
+ get icalProperty() {
+ let icssvc = cal.getIcsService();
+ let icalatt;
+ if (this.mIsOrganizer) {
+ icalatt = icssvc.createIcalProperty("ORGANIZER");
+ } else {
+ icalatt = icssvc.createIcalProperty("ATTENDEE");
+ }
+
+ if (!this.id) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ icalatt.valueAsIcalString = this.id;
+ for (let i = 0; i < this.icalAttendeePropMap.length; i++) {
+ let prop = this.icalAttendeePropMap[i];
+ if (this[prop.cal]) {
+ try {
+ icalatt.setParameter(prop.ics, this[prop.cal]);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attendee parameter value " + prop.ics + "=" + this[prop.cal]);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ for (let [key, value] of this.mProperties) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attendee parameter value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+ return icalatt;
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return (comp ? comp.icalString : "");
+ },
+ set icalString(val) {
+ let prop = cal.getIcsService().createIcalPropertyFromString(val);
+ if (prop.propertyName != "ORGANIZER" && prop.propertyName != "ATTENDEE") {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+ this.icalProperty = prop;
+ return val;
+ },
+
+ get propertyEnumerator() { return this.mProperties.enumerator; },
+
+ // The has/get/set/deleteProperty methods are case-insensitive.
+ getProperty: function(aName) {
+ return this.mProperties.getProperty(aName.toUpperCase());
+ },
+ setProperty: function(aName, aValue) {
+ this.modify();
+ if (aValue || !isNaN(parseInt(aValue, 10))) {
+ this.mProperties.setProperty(aName.toUpperCase(), aValue);
+ } else {
+ this.mProperties.deleteProperty(aName.toUpperCase());
+ }
+ },
+ deleteProperty: function(aName) {
+ this.modify();
+ this.mProperties.deleteProperty(aName.toUpperCase());
+ },
+
+ mId: null,
+ get id() {
+ return this.mId;
+ },
+ set id(aId) {
+ this.modify();
+ // RFC 1738 para 2.1 says we should be using lowercase mailto: urls
+ // we enforce prepending the mailto prefix for email type ids as migration code bug 1199942
+ return (this.mId = (aId ? cal.prependMailTo(aId) : null));
+ },
+
+ toString: function() {
+ const emailRE = new RegExp("^mailto:", "i");
+ let stringRep = (this.id || "").replace(emailRE, "");
+ let commonName = this.commonName;
+
+ if (commonName) {
+ stringRep = commonName + " <" + stringRep + ">";
+ }
+
+ return stringRep;
+ }
+};
+
+var makeMemberAttr;
+if (makeMemberAttr) {
+ makeMemberAttr(calAttendee, "mCommonName", null, "commonName");
+ makeMemberAttr(calAttendee, "mRsvp", null, "rsvp");
+ makeMemberAttr(calAttendee, "mRole", null, "role");
+ makeMemberAttr(calAttendee, "mParticipationStatus", "NEEDS-ACTION",
+ "participationStatus");
+ makeMemberAttr(calAttendee, "mUserType", "INDIVIDUAL", "userType");
+}
diff --git a/calendar/base/src/calCachedCalendar.js b/calendar/base/src/calCachedCalendar.js
new file mode 100644
index 000000000..f9eb2d858
--- /dev/null
+++ b/calendar/base/src/calCachedCalendar.js
@@ -0,0 +1,884 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var calICalendar = Components.interfaces.calICalendar;
+var cICL = Components.interfaces.calIChangeLog;
+var cIOL = Components.interfaces.calIOperationListener;
+
+var gNoOpListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ },
+
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ }
+};
+
+/**
+ * Returns true if the exception passed is one that should cause the cache
+ * layer to retry the operation. This is usually a network error or other
+ * temporary error.
+ *
+ * @param result The result code to check.
+ * @return True, if the result code means server unavailability.
+ */
+function isUnavailableCode(result) {
+ // Stolen from nserror.h
+ const NS_ERROR_MODULE_NETWORK = 6;
+ function NS_ERROR_GET_MODULE(code) {
+ return (((code >> 16) - 0x45) & 0x1fff);
+ }
+
+ if (NS_ERROR_GET_MODULE(result) == NS_ERROR_MODULE_NETWORK &&
+ !Components.isSuccessCode(result)) {
+ // This is a network error, which most likely means we should
+ // retry it some time.
+ return true;
+ }
+
+ // Other potential errors we want to retry with
+ switch (result) {
+ case Components.results.NS_ERROR_NOT_AVAILABLE:
+ return true;
+ default:
+ return false;
+ }
+}
+
+function calCachedCalendarObserverHelper(home, isCachedObserver) {
+ this.home = home;
+ this.isCachedObserver = isCachedObserver;
+}
+calCachedCalendarObserverHelper.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIObserver]),
+ isCachedObserver: false,
+
+ onStartBatch: function() {
+ this.home.mObservers.notify("onStartBatch");
+ },
+
+ onEndBatch: function() {
+ this.home.mObservers.notify("onEndBatch");
+ },
+
+ onLoad: function(calendar) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onLoad", [this.home]);
+ } else {
+ // start sync action after uncached calendar has been loaded.
+ // xxx todo, think about:
+ // although onAddItem et al have been called, we need to fire
+ // an additional onLoad completing the refresh call (->composite)
+ let home = this.home;
+ home.synchronize((status) => {
+ home.mObservers.notify("onLoad", [home]);
+ });
+ }
+ },
+
+ onAddItem: function(aItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onAddItem", arguments);
+ }
+ },
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onModifyItem", arguments);
+ }
+ },
+
+ onDeleteItem: function(aItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onDeleteItem", arguments);
+ }
+ },
+
+ onError: function(aCalendar, aErrNo, aMessage) {
+ this.home.mObservers.notify("onError", arguments);
+ },
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ if (!this.isCachedObserver) {
+ this.home.mObservers.notify("onPropertyChanged", [this.home, aName, aValue, aOldValue]);
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ if (!this.isCachedObserver) {
+ this.home.mObservers.notify("onPropertyDeleting", [this.home, aName]);
+ }
+ }
+};
+
+function calCachedCalendar(uncachedCalendar) {
+ this.wrappedJSObject = this;
+ this.mSyncQueue = [];
+ this.mObservers = new cal.ObserverBag(Components.interfaces.calIObserver);
+ uncachedCalendar.superCalendar = this;
+ uncachedCalendar.addObserver(new calCachedCalendarObserverHelper(this, false));
+ this.mUncachedCalendar = uncachedCalendar;
+ this.setupCachedCalendar();
+ if (this.supportsChangeLog) {
+ uncachedCalendar.offlineStorage = this.mCachedCalendar;
+ }
+ this.offlineCachedItems = {};
+ this.offlineCachedItemFlags = {};
+}
+calCachedCalendar.prototype = {
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Components.interfaces.calISchedulingSupport) &&
+ this.mUncachedCalendar.QueryInterface(aIID)) {
+ // check whether uncached calendar supports it:
+ return this;
+ } else if (aIID.equals(Components.interfaces.calICalendar) ||
+ aIID.equals(Components.interfaces.nsISupports)) {
+ return this;
+ } else {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ mCachedCalendar: null,
+ mCachedObserver: null,
+ mUncachedCalendar: null,
+ mObservers: null,
+ mSuperCalendar: null,
+ offlineCachedItems: null,
+ offlineCachedItemFlags: null,
+
+ onCalendarUnregistering: function() {
+ if (this.mCachedCalendar) {
+ let self = this;
+ this.mCachedCalendar.removeObserver(this.mCachedObserver);
+ // TODO put changes into a different calendar and delete
+ // afterwards.
+
+ let listener = {
+ onDeleteCalendar: function(aCalendar, aStatus, aDetail) {
+ self.mCachedCalendar = null;
+ }
+ };
+
+ this.mCachedCalendar.QueryInterface(Components.interfaces.calICalendarProvider)
+ .deleteCalendar(this.mCachedCalendar, listener);
+ }
+ },
+
+ setupCachedCalendar: function() {
+ try {
+ if (this.mCachedCalendar) { // this is actually a resetupCachedCalendar:
+ // Although this doesn't really follow the spec, we know the
+ // storage calendar's deleteCalendar method is synchronous.
+ // TODO put changes into a different calendar and delete
+ // afterwards.
+ this.mCachedCalendar.QueryInterface(Components.interfaces.calICalendarProvider)
+ .deleteCalendar(this.mCachedCalendar, null);
+ if (this.supportsChangeLog) {
+ // start with full sync:
+ this.mUncachedCalendar.resetLog();
+ }
+ } else {
+ let calType = Preferences.get("calendar.cache.type", "storage");
+ // While technically, the above deleteCalendar should delete the
+ // whole calendar, this is nothing more than deleting all events
+ // todos and properties. Therefore the initialization can be
+ // skipped.
+ let cachedCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=" + calType]
+ .createInstance(Components.interfaces.calICalendar);
+ switch (calType) {
+ case "memory": {
+ if (this.supportsChangeLog) {
+ // start with full sync:
+ this.mUncachedCalendar.resetLog();
+ }
+ break;
+ }
+ case "storage": {
+ let file = getCalendarDirectory();
+ file.append("cache.sqlite");
+ cachedCalendar.uri = Services.io.newFileURI(file);
+ cachedCalendar.id = this.id;
+ break;
+ }
+ default: {
+ throw new Error("unsupported cache calendar type: " + calType);
+ }
+ }
+ cachedCalendar.transientProperties = true;
+ cachedCalendar.setProperty("relaxedMode", true);
+ cachedCalendar.superCalendar = this;
+ if (!this.mCachedObserver) {
+ this.mCachedObserver = new calCachedCalendarObserverHelper(this, true);
+ }
+ cachedCalendar.addObserver(this.mCachedObserver);
+ this.mCachedCalendar = cachedCalendar;
+ }
+ } catch (exc) {
+ Components.utils.reportError(exc);
+ }
+ },
+
+ getOfflineAddedItems: function(callbackFunc) {
+ let self = this;
+ self.offlineCachedItems = {};
+ let getListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ self.offlineCachedItems[item.hashId] = item;
+ self.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_CREATED_RECORD;
+ }
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ self.getOfflineModifiedItems(callbackFunc);
+ }
+ };
+ this.mCachedCalendar.getItems(calICalendar.ITEM_FILTER_ALL_ITEMS | calICalendar.ITEM_FILTER_OFFLINE_CREATED,
+ 0, null, null, getListener);
+ },
+
+ getOfflineModifiedItems: function(callbackFunc) {
+ let self = this;
+ let getListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ self.offlineCachedItems[item.hashId] = item;
+ self.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_MODIFIED_RECORD;
+ }
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ self.getOfflineDeletedItems(callbackFunc);
+ }
+ };
+ this.mCachedCalendar.getItems(calICalendar.ITEM_FILTER_OFFLINE_MODIFIED | calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0, null, null, getListener);
+ },
+
+ getOfflineDeletedItems: function(callbackFunc) {
+ let self = this;
+ let getListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ self.offlineCachedItems[item.hashId] = item;
+ self.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_DELETED_RECORD;
+ }
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ if (callbackFunc) {
+ callbackFunc();
+ }
+ }
+ };
+ this.mCachedCalendar.getItems(calICalendar.ITEM_FILTER_OFFLINE_DELETED | calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0, null, null, getListener);
+ },
+
+ mPendingSync: null,
+ mSyncQueue: null,
+ synchronize: function(respFunc) {
+ let self = this;
+ if (this.getProperty("disabled")) {
+ return emptyQueue(Components.results.NS_OK);
+ }
+
+ this.mSyncQueue.push(respFunc);
+ if (this.mSyncQueue.length > 1) { // don't use mPendingSync here
+ cal.LOG("[calCachedCalendar] sync in action/pending.");
+ return this.mPendingSync;
+ }
+
+ function emptyQueue(status) {
+ let queue = self.mSyncQueue;
+ self.mSyncQueue = [];
+ function execResponseFunc(func) {
+ try {
+ func(status);
+ } catch (exc) {
+ cal.ASSERT(false, exc);
+ }
+ }
+ queue.forEach(execResponseFunc);
+ cal.LOG("[calCachedCalendar] sync queue empty.");
+ let operation = self.mPendingSync;
+ self.mPendingSync = null;
+ return operation;
+ }
+
+ if (this.offline) {
+ return emptyQueue(Components.results.NS_OK);
+ }
+
+ if (this.supportsChangeLog) {
+ cal.LOG("[calCachedCalendar] Doing changelog based sync for calendar " + this.uri.spec);
+ let opListener = {
+ onResult: function(operation, result) {
+ if (!operation || !operation.isPending) {
+ let status = (operation ? operation.status : Components.results.NS_OK);
+ if (!Components.isSuccessCode(status)) {
+ cal.ERROR("[calCachedCalendar] replay action failed: " +
+ (operation ? operation.id : "<unknown>") + ", uri=" +
+ self.uri.spec + ", result=" +
+ result + ", operation=" + operation);
+ }
+ cal.LOG("[calCachedCalendar] replayChangesOn finished.");
+ emptyQueue(status);
+ }
+ }
+ };
+ this.mPendingSync = this.mUncachedCalendar.replayChangesOn(opListener);
+ return this.mPendingSync;
+ }
+
+ cal.LOG("[calCachedCalendar] Doing full sync for calendar " + this.uri.spec);
+ let completeListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ modifiedTimes: {},
+ hasRenewedCalendar: false,
+ getsCompleted: 0,
+ getsReceived: 0,
+ opCompleted: false,
+
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (Components.isSuccessCode(aStatus)) {
+ if (!this.hasRenewedCalendar) {
+ // TODO instead of deleting the calendar and creating a new
+ // one, maybe we want to do a "real" sync between the
+ // existing local calendar and the remote calendar.
+ self.setupCachedCalendar();
+ this.hasRenewedCalendar = true;
+ }
+
+ this.getsReceived++;
+ cal.forEach(aItems, (item) => {
+ // Adding items recd from the Memory Calendar
+ // These may be different than what the cache has
+ completeListener.modifiedTimes[item.id] = item.lastModifiedTime;
+ self.mCachedCalendar.addItem(item, null);
+ }, () => {
+ completeListener.getsCompleted++;
+ if (completeListener.opCompleted) {
+ // onOperationComplete was called, but we were not ready yet. call it now.
+ completeListener.onOperationComplete(...completeListener.opCompleted);
+ completeListener.opCompleted = false;
+ }
+ });
+ }
+ },
+
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ if (this.getsCompleted < this.getsReceived) {
+ // If not all of our gets have been processed, then save the
+ // arguments and finish processing later.
+ this.opCompleted = Array.slice(arguments);
+ return;
+ }
+
+ if (Components.isSuccessCode(aStatus)) {
+ cal.forEach(self.offlineCachedItems, (item) => {
+ switch (self.offlineCachedItemFlags[item.hashId]) {
+ case cICL.OFFLINE_FLAG_CREATED_RECORD:
+ // Created items are not present on the server, so its safe to adopt them
+ self.adoptOfflineItem(item.clone(), null);
+ break;
+ case cICL.OFFLINE_FLAG_MODIFIED_RECORD:
+ // Two Cases Here:
+ if (item.id in completeListener.modifiedTimes) {
+ // The item is still on the server, we just retrieved it in the listener above.
+ if (item.lastModifiedTime.compare(completeListener.modifiedTimes[item.id]) < 0) {
+ // The item on the server has been modified, ask to overwrite
+ cal.WARN("[calCachedCalendar] Item '" + item.title + "' at the server seems to be modified recently.");
+ self.promptOverwrite("modify", item, null, null);
+ } else {
+ // Our item is newer, just modify the item
+ self.modifyOfflineItem(item, null, null);
+ }
+ } else {
+ // The item has been deleted from the server, ask if it should be added again
+ cal.WARN("[calCachedCalendar] Item '" + item.title + "' has been deleted from the server");
+ if (cal.promptOverwrite("modify", item, null, null)) {
+ self.adoptOfflineItem(item.clone(), null);
+ }
+ }
+ break;
+ case cICL.OFFLINE_FLAG_DELETED_RECORD:
+ if (item.id in completeListener.modifiedTimes) {
+ // The item seems to exist on the server...
+ if (item.lastModifiedTime.compare(completeListener.modifiedTimes[item.id]) < 0) {
+ // ...and has been modified on the server. Ask to overwrite
+ cal.WARN("[calCachedCalendar] Item '" + item.title + "' at the server seems to be modified recently.");
+ self.promptOverwrite("delete", item, null, null);
+ } else {
+ // ...and has not been modified. Delete it now.
+ self.deleteOfflineItem(item, null);
+ }
+ } else {
+ // Item has already been deleted from the server, no need to change anything.
+ }
+ break;
+ }
+ }, () => {
+ self.offlineCachedItems = {};
+ self.offlineCachedItemFlags = {};
+ self.playbackOfflineItems(() => emptyQueue(aStatus));
+ });
+ } else {
+ self.playbackOfflineItems(() => self.mCachedObserver.onLoad(self.mCachedCalendar));
+ emptyQueue(aStatus);
+ }
+ }
+ };
+
+ this.getOfflineAddedItems(() => {
+ this.mPendingSync = this.mUncachedCalendar.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0, null, null, completeListener);
+ });
+ return this.mPendingSync;
+ },
+
+ onOfflineStatusChanged: function(aNewState) {
+ if (aNewState) {
+ // Going offline: (XXX get items before going offline?) => we may ask the user to stay online a bit longer
+ } else {
+ // Going online (start replaying changes to the remote calendar)
+ this.refresh();
+ }
+ },
+
+ // aOldItem is already in the cache
+ promptOverwrite: function(aMethod, aItem, aListener, aOldItem) {
+ let overwrite = cal.promptOverwrite(aMethod, aItem, aListener, aOldItem);
+ if (overwrite) {
+ if (aMethod == "modify") {
+ this.modifyOfflineItem(aItem, aOldItem, aListener);
+ } else {
+ this.deleteOfflineItem(aItem, aListener);
+ }
+ }
+ },
+
+ /*
+ * Asynchronously performs playback operations of items added, modified, or deleted offline
+ *
+ * @param aCallback (optional) The function to be callled when playback is complete.
+ * @param aPlaybackType (optional) The starting operation type. This function will be
+ * called recursively through playback operations in the order of
+ * add, modify, delete. By default playback will start with the add
+ * operation. Valid values for this parameter are defined as
+ * OFFLINE_FLAG_XXX constants in the calIChangeLog interface.
+ */
+ playbackOfflineItems: function(aCallback, aPlaybackType) {
+ let self = this;
+ let storage = this.mCachedCalendar.QueryInterface(Components.interfaces.calIOfflineStorage);
+
+ let resetListener = gNoOpListener;
+ let itemQueue = [];
+ let debugOp;
+ let nextCallback;
+ let uncachedOp;
+ let listenerOp;
+ let filter;
+
+ aPlaybackType = aPlaybackType || cICL.OFFLINE_FLAG_CREATED_RECORD;
+ switch (aPlaybackType) {
+ case cICL.OFFLINE_FLAG_CREATED_RECORD:
+ debugOp = "add";
+ nextCallback = this.playbackOfflineItems.bind(this, aCallback, cICL.OFFLINE_FLAG_MODIFIED_RECORD);
+ uncachedOp = this.mUncachedCalendar.addItem.bind(this.mUncachedCalendar);
+ listenerOp = cIOL.ADD;
+ filter = calICalendar.ITEM_FILTER_OFFLINE_CREATED;
+ break;
+ case cICL.OFFLINE_FLAG_MODIFIED_RECORD:
+ debugOp = "modify";
+ nextCallback = this.playbackOfflineItems.bind(this, aCallback, cICL.OFFLINE_FLAG_DELETED_RECORD);
+ uncachedOp = function(item, listener) { self.mUncachedCalendar.modifyItem(item, item, listener); };
+ listenerOp = cIOL.MODIFY;
+ filter = calICalendar.ITEM_FILTER_OFFLINE_MODIFIED;
+ break;
+ case cICL.OFFLINE_FLAG_DELETED_RECORD:
+ debugOp = "delete";
+ nextCallback = aCallback;
+ uncachedOp = this.mUncachedCalendar.deleteItem.bind(this.mUncachedCalendar);
+ listenerOp = cIOL.MODIFY;
+ filter = calICalendar.ITEM_FILTER_OFFLINE_DELETED;
+ break;
+ default:
+ cal.ERROR("[calCachedCalendar] Invalid playback type: " + aPlaybackType);
+ return;
+ }
+
+ let opListener = {
+ onGetResult: function(calendar, status, itemType, detail, count, items) {},
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (Components.isSuccessCode(status)) {
+ if (aPlaybackType == cICL.OFFLINE_FLAG_DELETED_RECORD) {
+ self.mCachedCalendar.deleteItem(detail, resetListener);
+ } else {
+ storage.resetItemOfflineFlag(detail, resetListener);
+ }
+ } else {
+ // If the playback action could not be performed, then there
+ // is no need for further action. The item still has the
+ // offline flag, so it will be taken care of next time.
+ cal.WARN("[calCachedCalendar] Unable to perform playback action " + debugOp +
+ " to the server, will try again next time (" + id + "," + detail + ")");
+ }
+
+ // move on to the next item in the queue
+ popItemQueue();
+ }
+ };
+
+ function popItemQueue() {
+ if (!itemQueue || itemQueue.length == 0) {
+ // no items left in the queue, move on to the next operation
+ if (nextCallback) {
+ nextCallback();
+ }
+ } else {
+ // perform operation on the next offline item in the queue
+ let item = itemQueue.pop();
+ try {
+ uncachedOp(item, opListener);
+ } catch (e) {
+ cal.ERROR("[calCachedCalendar] Could not perform playback operation " + debugOp +
+ " for item " + (item.title || " (none) ") + ": " + e);
+ opListener.onOperationComplete(self, e.result, listenerOp, item.id, e.message);
+ }
+ }
+ }
+
+ let getListener = {
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ itemQueue = itemQueue.concat(items);
+ },
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (self.offline) {
+ cal.LOG("[calCachedCalendar] back to offline mode, reconciliation aborted");
+ if (aCallback) {
+ aCallback();
+ }
+ } else {
+ cal.LOG("[calCachedCalendar] Performing playback operation " + debugOp +
+ " on " + itemQueue.length + " items to " + self.name);
+
+ // start the first operation
+ popItemQueue();
+ }
+ }
+ };
+
+ this.mCachedCalendar.getItems(calICalendar.ITEM_FILTER_ALL_ITEMS | filter,
+ 0, null, null, getListener);
+ },
+
+ get superCalendar() {
+ return (this.mSuperCalendar && this.mSuperCalendar.superCalendar) || this;
+ },
+ set superCalendar(val) {
+ return (this.mSuperCalendar = val);
+ },
+
+ get offline() {
+ return Services.io.offline;
+ },
+ get supportsChangeLog() {
+ return (cal.wrapInstance(this.mUncachedCalendar, Components.interfaces.calIChangeLog) != null);
+ },
+
+ get canRefresh() { // enable triggering sync using the reload button
+ return true;
+ },
+
+ getProperty: function(aName) {
+ switch (aName) {
+ case "cache.enabled":
+ if (this.mUncachedCalendar.getProperty("cache.always")) {
+ return true;
+ }
+ break;
+ }
+
+ return this.mUncachedCalendar.getProperty(aName);
+ },
+ refresh: function() {
+ if (this.offline) {
+ this.downstreamRefresh();
+ } else if (this.supportsChangeLog) {
+ /* we first ensure that any remaining offline items are reconciled with the calendar server */
+ this.playbackOfflineItems(this.downstreamRefresh.bind(this));
+ } else {
+ this.downstreamRefresh();
+ }
+ },
+ downstreamRefresh: function() {
+ if (this.mUncachedCalendar.canRefresh && !this.offline) {
+ return this.mUncachedCalendar.refresh(); // will trigger synchronize once the calendar is loaded
+ } else {
+ return this.synchronize((status) => { // fire completing onLoad for this refresh call
+ this.mCachedObserver.onLoad(this.mCachedCalendar);
+ });
+ }
+ },
+
+ addObserver: function(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+ removeObserver: function(aObserver) {
+ this.mObservers.remove(aObserver);
+ },
+
+ addItem: function(item, listener) {
+ return this.adoptItem(item.clone(), listener);
+ },
+ adoptItem: function(item, listener) {
+ // Forwarding add/modify/delete to the cached calendar using the calIObserver
+ // callbacks would be advantageous, because the uncached provider could implement
+ // a true push mechanism firing without being triggered from within the program.
+ // But this would mean the uncached provider fires on the passed
+ // calIOperationListener, e.g. *before* it fires on calIObservers
+ // (because that order is undefined). Firing onOperationComplete before onAddItem et al
+ // would result in this facade firing onOperationComplete even though the modification
+ // hasn't yet been performed on the cached calendar (which happens in onAddItem et al).
+ // Result is that we currently stick to firing onOperationComplete if the cached calendar
+ // has performed the modification, see below:
+ let self = this;
+ let cacheListener = {
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ cal.ASSERT(false, "unexpected!");
+ },
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (isUnavailableCode(status)) {
+ // The item couldn't be added to the (remote) location,
+ // this is like being offline. Add the item to the cached
+ // calendar instead.
+ cal.LOG("[calCachedCalendar] Calendar " + calendar.name + " is unavailable, adding item offline");
+ self.adoptOfflineItem(item, listener);
+ } else if (Components.isSuccessCode(status)) {
+ // On success, add the item to the cache.
+ self.mCachedCalendar.addItem(detail, listener);
+ } else if (listener) {
+ // Either an error occurred or this is a successful add
+ // to a cached calendar. Forward the call to the listener
+ listener.onOperationComplete(self, status, opType, id, detail);
+ }
+ }
+ };
+
+ if (this.offline) {
+ // If we are offline, don't even try to add the item
+ this.adoptOfflineItem(item, listener);
+ } else {
+ // Otherwise ask the provider to add the item now.
+ this.mUncachedCalendar.adoptItem(item, cacheListener);
+ }
+ },
+ adoptOfflineItem: function(item, listener) {
+ let self = this;
+ let opListener = {
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ cal.ASSERT(false, "unexpected!");
+ },
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (Components.isSuccessCode(status)) {
+ let storage = self.mCachedCalendar.QueryInterface(Components.interfaces.calIOfflineStorage);
+ storage.addOfflineItem(detail, listener);
+ } else if (listener) {
+ listener.onOperationComplete(self, status, opType, id, detail);
+ }
+ }
+ };
+ this.mCachedCalendar.adoptItem(item, opListener);
+ },
+
+ modifyItem: function(newItem, oldItem, listener) {
+ let self = this;
+
+ // First of all, we should find out if the item to modify is
+ // already an offline item or not.
+ let flagListener = {
+ onGetResult: function() {},
+ onOperationComplete: function(calendar, status, opType, id, offline_flag) {
+ if (offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+ offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD) {
+ // The item is already offline, just modify it in the cache
+ self.modifyOfflineItem(newItem, oldItem, listener);
+ } else {
+ // Not an offline item, attempt to modify using provider
+ self.mUncachedCalendar.modifyItem(newItem, oldItem, cacheListener);
+ }
+ }
+ };
+
+ /* Forwarding add/modify/delete to the cached calendar using the calIObserver
+ * callbacks would be advantageous, because the uncached provider could implement
+ * a true push mechanism firing without being triggered from within the program.
+ * But this would mean the uncached provider fires on the passed
+ * calIOperationListener, e.g. *before* it fires on calIObservers
+ * (because that order is undefined). Firing onOperationComplete before onAddItem et al
+ * would result in this facade firing onOperationComplete even though the modification
+ * hasn't yet been performed on the cached calendar (which happens in onAddItem et al).
+ * Result is that we currently stick to firing onOperationComplete if the cached calendar
+ * has performed the modification, see below: */
+ let cacheListener = {
+ onGetResult: function() {},
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (isUnavailableCode(status)) {
+ // The item couldn't be modified at the (remote) location,
+ // this is like being offline. Add the item to the cache
+ // instead.
+ cal.LOG("[calCachedCalendar] Calendar " + calendar.name + " is unavailable, modifying item offline");
+ self.modifyOfflineItem(newItem, oldItem, listener);
+ } else if (Components.isSuccessCode(status)) {
+ // On success, modify the item in the cache
+ self.mCachedCalendar.modifyItem(detail, oldItem, listener);
+ } else if (listener) {
+ // This happens on error, forward the error through the listener
+ listener.onOperationComplete(self, status, opType, id, detail);
+ }
+ }
+ };
+
+ if (this.offline) {
+ // If we are offline, don't even try to modify the item
+ this.modifyOfflineItem(newItem, oldItem, listener);
+ } else {
+ // Otherwise, get the item flags, the listener will further
+ // process the item.
+ this.mCachedCalendar.getItemOfflineFlag(oldItem, flagListener);
+ }
+ },
+
+ modifyOfflineItem: function(newItem, oldItem, listener) {
+ let self = this;
+ let opListener = {
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ cal.ASSERT(false, "unexpected!");
+ },
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (Components.isSuccessCode(status)) {
+ // Modify the offline item in the storage, passing the
+ // listener will make sure its notified
+ let storage = self.mCachedCalendar.QueryInterface(Components.interfaces.calIOfflineStorage);
+ storage.modifyOfflineItem(detail, listener);
+ } else if (listener) {
+ // If there was not a success, then we need to notify the
+ // listener ourselves
+ listener.onOperationComplete(self, status, opType, id, detail);
+ }
+ }
+ };
+
+ this.mCachedCalendar.modifyItem(newItem, oldItem, opListener);
+ },
+
+ deleteItem: function(item, listener) {
+ let self = this;
+
+ // First of all, we should find out if the item to delete is
+ // already an offline item or not.
+ let flagListener = {
+ onGetResult: function() {},
+ onOperationComplete: function(calendar, status, opType, id, offline_flag) {
+ if (offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+ offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD) {
+ // The item is already offline, just mark it deleted it in
+ // the cache
+ self.deleteOfflineItem(item, listener);
+ } else {
+ // Not an offline item, attempt to delete using provider
+ self.mUncachedCalendar.deleteItem(item, cacheListener);
+ }
+ }
+ };
+ // Forwarding add/modify/delete to the cached calendar using the calIObserver
+ // callbacks would be advantageous, because the uncached provider could implement
+ // a true push mechanism firing without being triggered from within the program.
+ // But this would mean the uncached provider fires on the passed
+ // calIOperationListener, e.g. *before* it fires on calIObservers
+ // (because that order is undefined). Firing onOperationComplete before onAddItem et al
+ // would result in this facade firing onOperationComplete even though the modification
+ // hasn't yet been performed on the cached calendar (which happens in onAddItem et al).
+ // Result is that we currently stick to firing onOperationComplete if the cached calendar
+ // has performed the modification, see below:
+ let cacheListener = {
+ onGetResult: function() {},
+ onOperationComplete: function(calendar, status, opType, id, detail) {
+ if (isUnavailableCode(status)) {
+ // The item couldn't be deleted at the (remote) location,
+ // this is like being offline. Mark the item deleted in the
+ // cache instead.
+ cal.LOG("[calCachedCalendar] Calendar " + calendar.name + " is unavailable, deleting item offline");
+ self.deleteOfflineItem(item, listener);
+ } else if (Components.isSuccessCode(status)) {
+ // On success, delete the item from the cache
+ self.mCachedCalendar.deleteItem(item, listener);
+
+ // Also, remove any meta data associated with the item
+ try {
+ let storage = self.mCachedCalendar.QueryInterface(Components.interfaces.calISyncWriteCalendar);
+ storage.deleteMetaData(item.id);
+ } catch (e) {
+ cal.LOG("[calCachedCalendar] Offline storage doesn't support metadata");
+ }
+ } else if (listener) {
+ // This happens on error, forward the error through the listener
+ listener.onOperationComplete(self, status, opType, id, detail);
+ }
+ }
+ };
+
+ if (this.offline) {
+ // If we are offline, don't even try to delete the item
+ this.deleteOfflineItem(item, listener);
+ } else {
+ // Otherwise, get the item flags, the listener will further
+ // process the item.
+ this.mCachedCalendar.getItemOfflineFlag(item, flagListener);
+ }
+ },
+ deleteOfflineItem: function(item, listener) {
+ /* We do not delete the item from the cache, as we will need it when reconciling the cache content and the server content. */
+ let storage = this.mCachedCalendar.QueryInterface(Components.interfaces.calIOfflineStorage);
+ storage.deleteOfflineItem(item, listener);
+ }
+};
+(function() {
+ function defineForwards(proto, targetName, functions, getters, gettersAndSetters) {
+ function defineForwardGetter(attr) {
+ proto.__defineGetter__(attr, function() { return this[targetName][attr]; });
+ }
+ function defineForwardGetterAndSetter(attr) {
+ defineForwardGetter(attr);
+ proto.__defineSetter__(attr, function(value) { return (this[targetName][attr] = value); });
+ }
+ function defineForwardFunction(funcName) {
+ proto[funcName] = function(...args) {
+ let obj = this[targetName];
+ return obj[funcName](...args);
+ };
+ }
+ functions.forEach(defineForwardFunction);
+ getters.forEach(defineForwardGetter);
+ gettersAndSetters.forEach(defineForwardGetterAndSetter);
+ }
+
+ defineForwards(calCachedCalendar.prototype, "mUncachedCalendar",
+ ["setProperty", "deleteProperty",
+ "isInvitation", "getInvitedAttendee", "canNotify"],
+ ["type", "aclManager", "aclEntry"],
+ ["id", "name", "uri", "readOnly"]);
+ defineForwards(calCachedCalendar.prototype, "mCachedCalendar",
+ ["getItem", "getItems", "startBatch", "endBatch"], [], []);
+})();
diff --git a/calendar/base/src/calCalendarManager.js b/calendar/base/src/calCalendarManager.js
new file mode 100644
index 000000000..6800da45a
--- /dev/null
+++ b/calendar/base/src/calCalendarManager.js
@@ -0,0 +1,1123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
+
+var REGISTRY_BRANCH = "calendar.registry.";
+var DB_SCHEMA_VERSION = 10;
+var MAX_INT = Math.pow(2, 31) - 1;
+var MIN_INT = -MAX_INT;
+
+function calCalendarManager() {
+ this.wrappedJSObject = this;
+ this.mObservers = new calListenerBag(Components.interfaces.calICalendarManagerObserver);
+ this.mCalendarObservers = new calListenerBag(Components.interfaces.calIObserver);
+}
+
+var calCalendarManagerClassID = Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}");
+var calCalendarManagerInterfaces = [
+ Components.interfaces.calICalendarManager,
+ Components.interfaces.calIStartupService,
+ Components.interfaces.nsIObserver,
+];
+calCalendarManager.prototype = {
+ classID: calCalendarManagerClassID,
+ QueryInterface: XPCOMUtils.generateQI(calCalendarManagerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calCalendarManagerClassID,
+ contractID: "@mozilla.org/calendar/manager;1",
+ classDescription: "Calendar Manager",
+ interfaces: calCalendarManagerInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ get networkCalendarCount() { return this.mNetworkCalendarCount; },
+ get readOnlyCalendarCount() { return this.mReadonlyCalendarCount; },
+ get calendarCount() { return this.mCalendarCount; },
+
+ // calIStartupService:
+ startup: function(aCompleteListener) {
+ AddonManager.addAddonListener(gCalendarManagerAddonListener);
+ this.checkAndMigrateDB();
+ this.mCache = null;
+ this.mCalObservers = null;
+ this.mRefreshTimer = {};
+ this.setupOfflineObservers();
+ this.mNetworkCalendarCount = 0;
+ this.mReadonlyCalendarCount = 0;
+ this.mCalendarCount = 0;
+
+ Services.obs.addObserver(this, "http-on-modify-request", false);
+
+ // We only add the observer if the pref is set and only check for the
+ // pref on startup to avoid checking for every http request
+ if (Preferences.get("calendar.network.multirealm", false)) {
+ Services.obs.addObserver(this, "http-on-examine-response", false);
+ }
+
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ },
+
+ shutdown: function(aCompleteListener) {
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ calendar.removeObserver(this.mCalObservers[calendar.id]);
+ }
+
+ this.cleanupOfflineObservers();
+
+ Services.obs.removeObserver(this, "http-on-modify-request");
+
+ AddonManager.removeAddonListener(gCalendarManagerAddonListener);
+
+ // Remove the observer if the pref is set. This might fail when the
+ // user flips the pref, but we assume he is going to restart anyway
+ // afterwards.
+ if (Preferences.get("calendar.network.multirealm", false)) {
+ Services.obs.removeObserver(this, "http-on-examine-response");
+ }
+
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ },
+
+
+ setupOfflineObservers: function() {
+ Services.obs.addObserver(this, "network:offline-status-changed", false);
+ },
+
+ cleanupOfflineObservers: function() {
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "timer-callback": {
+ // Refresh all the calendars that can be refreshed.
+ for (let calendar of this.getCalendars({})) {
+ if (!calendar.getProperty("disabled") && calendar.canRefresh) {
+ calendar.refresh();
+ }
+ }
+ break;
+ }
+ case "network:offline-status-changed": {
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ if (calendar instanceof calCachedCalendar) {
+ calendar.onOfflineStatusChanged(aData == "offline");
+ }
+ }
+ break;
+ }
+ case "http-on-examine-response": {
+ try {
+ let channel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
+ if (channel.notificationCallbacks) {
+ // We use the notification callbacks to get the calendar interface,
+ // which likely works for our requests since getInterface is called
+ // from the calendar provider context.
+ let authHeader = channel.getResponseHeader("WWW-Authenticate");
+ let calendar = channel.notificationCallbacks
+ .getInterface(Components.interfaces.calICalendar);
+ if (calendar && !calendar.getProperty("capabilities.realmrewrite.disabled")) {
+ // The provider may choose to explicitly disable the
+ // rewriting, for example if all calendars on a
+ // domain have the same credentials
+ let escapedName = calendar.name.replace(/\\/g, "\\\\")
+ .replace(/"/g, '\\"');
+ authHeader = appendToRealm(authHeader, "(" + escapedName + ")");
+ channel.setResponseHeader("WWW-Authenticate", authHeader, false);
+ }
+ }
+ } catch (e) {
+ if (e.result != Components.results.NS_NOINTERFACE &&
+ e.result != Components.results.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ // Possible reasons we got here:
+ // - Its not a http channel (wtf? Oh well)
+ // - The owner is not a calICalendar (looks like its not our deal)
+ // - The WWW-Authenticate header is missing (thats ok)
+ }
+ break;
+ }
+ case "http-on-modify-request": {
+ // Unfortunately, the ability to do this with a general pref has
+ // been removed. Calendar servers might still want to know what
+ // client is used for access, so add our UA String to each
+ // request.
+ let httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
+ try {
+ // NOTE: For some reason, this observer call doesn't have
+ // the "cal" namespace defined
+ let userAgent = httpChannel.getRequestHeader("User-Agent");
+ let calUAString = Preferences.get("calendar.useragent.extra", "").trim();
+
+ // Don't add an empty string or an already included token.
+ if (calUAString && !userAgent.includes(calUAString)) {
+ // User-Agent is not a mergeable header. We need to
+ // merge the user agent ourselves.
+ httpChannel.setRequestHeader("User-Agent",
+ userAgent + " " + calUAString,
+ false);
+ }
+ } catch (e) {
+ if (e.result != Components.results.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ // We swallow this error since it means the User Agent
+ // header is not set. We don't want to force it to be set.
+ }
+ break;
+ }
+ }
+ },
+
+ //
+ // DB migration code begins here
+ //
+
+ upgradeDB: function(oldVersion, db) {
+ if (oldVersion < 6) {
+ dump("**** Upgrading calCalendarManager schema to 6\n");
+
+ // Schema changes in v6:
+ //
+ // - Change all STRING columns to TEXT to avoid SQLite's
+ // "feature" where it will automatically convert strings to
+ // numbers (ex: 10e4 -> 10000). See bug 333688.
+
+ // Create the new tables.
+
+ try {
+ db.executeSimpleSQL("DROP TABLE cal_calendars_v6; DROP TABLE cal_calendars_prefs_v6;");
+ } catch (e) {
+ // We should get exceptions for trying to drop tables
+ // that don't (shouldn't) exist.
+ }
+
+ db.executeSimpleSQL("CREATE TABLE cal_calendars_v6 " +
+ "(id INTEGER PRIMARY KEY," +
+ " type TEXT," +
+ " uri TEXT);");
+
+ db.executeSimpleSQL("CREATE TABLE cal_calendars_prefs_v6 " +
+ "(id INTEGER PRIMARY KEY," +
+ " calendar INTEGER," +
+ " name TEXT," +
+ " value TEXT);");
+
+ // Copy in the data.
+ let calendarCols = ["id", "type", "uri"];
+ let calendarPrefsCols = ["id", "calendar", "name", "value"];
+
+ db.executeSimpleSQL("INSERT INTO cal_calendars_v6(" + calendarCols.join(",") + ") " +
+ " SELECT " + calendarCols.join(",") +
+ " FROM cal_calendars");
+
+ db.executeSimpleSQL("INSERT INTO cal_calendars_prefs_v6(" + calendarPrefsCols.join(",") + ") " +
+ " SELECT " + calendarPrefsCols.join(",") +
+ " FROM cal_calendars_prefs");
+
+ // Delete each old table and rename the new ones to use the
+ // old tables' names.
+ let tableNames = ["cal_calendars", "cal_calendars_prefs"];
+
+ for (let i in tableNames) {
+ db.executeSimpleSQL("DROP TABLE " + tableNames[i] + ";" +
+ "ALTER TABLE " + tableNames[i] + "_v6 " +
+ " RENAME TO " + tableNames[i] + ";");
+ }
+
+ oldVersion = 8;
+ }
+
+ if (oldVersion < DB_SCHEMA_VERSION) {
+ dump("**** Upgrading calCalendarManager schema to 9/10\n");
+
+ if (db.tableExists("cal_calmgr_schema_version")) {
+ // Set only once the last time to v10, so the version check works in calendar 0.8.
+ // In calendar 0.9 and following, the provider itself will check its version
+ // on initialization and notify the calendar whether it's usable or not.
+ db.executeSimpleSQL("UPDATE cal_calmgr_schema_version SET version = " + DB_SCHEMA_VERSION + ";");
+ } else {
+ // Schema changes in v9:
+ //
+ // - Decouple schema version from storage calendar
+ // Create the new tables.
+ db.executeSimpleSQL("CREATE TABLE cal_calmgr_schema_version (version INTEGER);");
+ db.executeSimpleSQL("INSERT INTO cal_calmgr_schema_version VALUES(" + DB_SCHEMA_VERSION + ")");
+ }
+ }
+ },
+
+ migrateDB: function(db) {
+ let selectCalendars = db.createStatement("SELECT * FROM cal_calendars");
+ let selectPrefs = db.createStatement("SELECT name, value FROM cal_calendars_prefs WHERE calendar = :calendar");
+ try {
+ let sortOrder = {};
+
+ while (selectCalendars.executeStep()) {
+ let id = cal.getUUID(); // use fresh uuids
+ Preferences.set(getPrefBranchFor(id) + "type", selectCalendars.row.type);
+ Preferences.set(getPrefBranchFor(id) + "uri", selectCalendars.row.uri);
+ // the former id served as sort position:
+ sortOrder[selectCalendars.row.id] = id;
+ // move over prefs:
+ selectPrefs.params.calendar = selectCalendars.row.id;
+ while (selectPrefs.executeStep()) {
+ let name = selectPrefs.row.name.toLowerCase(); // may come lower case, so force it to be
+ let value = selectPrefs.row.value;
+ switch (name) {
+ case "readonly":
+ Preferences.set(getPrefBranchFor(id) + "readOnly", value == "true");
+ break;
+ case "relaxedmode":
+ Preferences.set(getPrefBranchFor(id) + "relaxedMode", value == "true");
+ break;
+ case "suppressalarms":
+ Preferences.set(getPrefBranchFor(id) + "suppressAlarms", value == "true");
+ break;
+ case "disabled":
+ case "cache.supported":
+ case "auto-enabled":
+ case "cache.enabled":
+ case "lightning-main-in-composite":
+ case "calendar-main-in-composite":
+ case "lightning-main-default":
+ case "calendar-main-default":
+ Preferences.set(getPrefBranchFor(id) + name, value == "true");
+ break;
+ case "backup-time":
+ case "uniquenum":
+ // These preference names were migrated due to bug 979262.
+ Preferences.set(getPrefBranchFor(id) + name + "2", "bignum:" + value);
+ break;
+ default: // keep as string
+ Preferences.set(getPrefBranchFor(id) + name, value);
+ break;
+ }
+ }
+ selectPrefs.reset();
+ }
+
+ let sortOrderAr = [];
+ for (let id in sortOrder) {
+ sortOrderAr.push(sortOrder[id]);
+ }
+ Preferences.set("calendar.list.sortOrder", sortOrderAr.join(" "));
+ flushPrefs();
+ } finally {
+ selectPrefs.reset();
+ selectCalendars.reset();
+ }
+ },
+
+ checkAndMigrateDB: function() {
+ let storageSdb = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile);
+ storageSdb.append("storage.sdb");
+ let db = Services.storage.openDatabase(storageSdb);
+
+ db.beginTransactionAs(Components.interfaces.mozIStorageConnection.TRANSACTION_EXCLUSIVE);
+ try {
+ if (db.tableExists("cal_calendars_prefs")) {
+ // Check if we need to upgrade:
+ let version = this.getSchemaVersion(db);
+ if (version < DB_SCHEMA_VERSION) {
+ this.upgradeDB(version, db);
+ }
+
+ this.migrateDB(db);
+
+ db.executeSimpleSQL("DROP TABLE cal_calendars; " +
+ "DROP TABLE cal_calendars_prefs; " +
+ "DROP TABLE cal_calmgr_schema_version;");
+ }
+
+ if (db.tableExists("cal_calendars")) {
+ db.rollbackTransaction();
+ } else {
+ // create dummy cal_calendars, so previous versions (pre 1.0pre) run into the schema check:
+ db.createTable("cal_calendars", "id INTEGER");
+ // let schema checks always fail, we cannot take the shared cal_calendar_schema_version:
+ db.createTable("cal_calmgr_schema_version", "version INTEGER");
+ db.executeSimpleSQL("INSERT INTO cal_calmgr_schema_version VALUES(" + (DB_SCHEMA_VERSION + 1) + ")");
+ db.commitTransaction();
+ }
+ } catch (exc) {
+ db.rollbackTransaction();
+ throw exc;
+ } finally {
+ db.close();
+ }
+ },
+
+ /**
+ * @return db schema version
+ * @exception various, depending on error
+ */
+ getSchemaVersion: function(db) {
+ let stmt;
+ let version = null;
+
+ let table;
+ if (db.tableExists("cal_calmgr_schema_version")) {
+ table = "cal_calmgr_schema_version";
+ } else {
+ // Fall back to the old schema table
+ table = "cal_calendar_schema_version";
+ }
+
+ try {
+ stmt = db.createStatement("SELECT version FROM " + table + " LIMIT 1");
+ if (stmt.executeStep()) {
+ version = stmt.row.version;
+ }
+ stmt.reset();
+
+ if (version !== null) {
+ // This is the only place to leave this function gracefully.
+ return version;
+ }
+ } catch (e) {
+ if (stmt) {
+ stmt.reset();
+ }
+ cal.ERROR("++++++++++++ calMgrGetSchemaVersion() error: " + db.lastErrorString);
+ Components.utils.reportError("Error getting calendar schema version! DB Error: " + db.lastErrorString);
+ throw e;
+ }
+
+ throw table + " SELECT returned no results";
+ },
+
+ //
+ // / DB migration code ends here
+ //
+
+ alertAndQuit: function() {
+ // We want to include the extension name in the error message rather
+ // than blaming Thunderbird.
+ let hostAppName = calGetString("brand", "brandShortName", null, "branding");
+ let calAppName = calGetString("lightning", "brandShortName", null, "lightning");
+ let errorBoxTitle = calGetString("calendar", "tooNewSchemaErrorBoxTitle", [calAppName]);
+ let errorBoxText = calGetString("calendar", "tooNewSchemaErrorBoxTextLightning", [calAppName, hostAppName]);
+ let errorBoxButtonLabel = calGetString("calendar", "tooNewSchemaButtonRestart", [hostAppName]);
+
+ let promptSvc = Services.prompt;
+
+ let errorBoxButtonFlags = promptSvc.BUTTON_POS_0 *
+ promptSvc.BUTTON_TITLE_IS_STRING +
+ promptSvc.BUTTON_POS_0_DEFAULT;
+
+ promptSvc.confirmEx(null,
+ errorBoxTitle,
+ errorBoxText,
+ errorBoxButtonFlags,
+ errorBoxButtonLabel,
+ null, // No second button text
+ null, // No third button text
+ null, // No checkbox
+ { value: false }); // Unnecessary checkbox state
+
+ // Disable Lightning
+ AddonManager.getAddonByID("{e2fda1a4-762b-4020-b5ad-a41df1933103}", (aAddon) => {
+ aAddon.userDisabled = true;
+ Services.startup.quit(Components.interfaces.nsIAppStartup.eRestart |
+ Components.interfaces.nsIAppStartup.eForceQuit);
+ });
+ },
+
+ /**
+ * calICalendarManager interface
+ */
+ createCalendar: function(type, uri) {
+ try {
+ if (!Components.classes["@mozilla.org/calendar/calendar;1?type=" + type]) {
+ // Don't notify the user with an extra dialog if the provider
+ // interface is missing.
+ return null;
+ }
+ let calendar = Components.classes["@mozilla.org/calendar/calendar;1?type=" + type]
+ .createInstance(Components.interfaces.calICalendar);
+ calendar.uri = uri;
+ return calendar;
+ } catch (ex) {
+ let rc = ex;
+ let uiMessage = ex;
+ if (ex instanceof Components.interfaces.nsIException) {
+ rc = ex.result;
+ uiMessage = ex.message;
+ }
+ switch (rc) {
+ case Components.interfaces.calIErrors.STORAGE_UNKNOWN_SCHEMA_ERROR:
+ // For now we alert and quit on schema errors like we've done before:
+ this.alertAndQuit();
+ return null;
+ case Components.interfaces.calIErrors.STORAGE_UNKNOWN_TIMEZONES_ERROR:
+ uiMessage = calGetString("calendar", "unknownTimezonesError", [uri.spec]);
+ break;
+ default:
+ uiMessage = calGetString("calendar", "unableToCreateProvider", [uri.spec]);
+ break;
+ }
+ // Log the original exception via error console to provide more debug info
+ cal.ERROR(ex);
+
+ // Log the possibly translated message via the UI.
+ let paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Components.interfaces.nsIDialogParamBlock);
+ paramBlock.SetNumberStrings(3);
+ paramBlock.SetString(0, uiMessage);
+ paramBlock.SetString(1, "0x" + rc.toString(0x10));
+ paramBlock.SetString(2, ex);
+ Services.ww.openWindow(null,
+ "chrome://calendar/content/calendar-error-prompt.xul",
+ "_blank",
+ "chrome,dialog=yes,alwaysRaised=yes",
+ paramBlock);
+ return null;
+ }
+ },
+
+ registerCalendar: function(calendar) {
+ this.assureCache();
+
+ // If the calendar is already registered, bail out
+ cal.ASSERT(!calendar.id || !(calendar.id in this.mCache),
+ "[calCalendarManager::registerCalendar] calendar already registered!",
+ true);
+
+ if (!calendar.id) {
+ calendar.id = cal.getUUID();
+ }
+
+ Preferences.set(getPrefBranchFor(calendar.id) + "type", calendar.type);
+ Preferences.set(getPrefBranchFor(calendar.id) + "uri", calendar.uri.spec);
+
+ if ((calendar.getProperty("cache.supported") !== false) &&
+ (calendar.getProperty("cache.enabled") ||
+ calendar.getProperty("cache.always"))) {
+ calendar = new calCachedCalendar(calendar);
+ }
+
+ this.setupCalendar(calendar);
+ flushPrefs();
+
+ if (!calendar.getProperty("disabled") && calendar.canRefresh) {
+ calendar.refresh();
+ }
+
+ this.notifyObservers("onCalendarRegistered", [calendar]);
+ },
+
+ setupCalendar: function(calendar) {
+ this.mCache[calendar.id] = calendar;
+
+ // Add an observer to track readonly-mode triggers
+ let newObserver = new calMgrCalendarObserver(calendar, this);
+ calendar.addObserver(newObserver);
+ this.mCalObservers[calendar.id] = newObserver;
+
+ // Set up statistics
+ if (calendar.getProperty("requiresNetwork") !== false) {
+ this.mNetworkCalendarCount++;
+ }
+ if (calendar.readOnly) {
+ this.mReadonlyCalendarCount++;
+ }
+ this.mCalendarCount++;
+
+ // Set up the refresh timer
+ this.setupRefreshTimer(calendar);
+ },
+
+ setupRefreshTimer: function(aCalendar) {
+ // Add the refresh timer for this calendar
+ let refreshInterval = aCalendar.getProperty("refreshInterval");
+ if (refreshInterval === null) {
+ // Default to 30 minutes, in case the value is missing
+ refreshInterval = 30;
+ }
+
+ this.clearRefreshTimer(aCalendar);
+
+ if (refreshInterval > 0) {
+ this.mRefreshTimer[aCalendar.id] =
+ Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+
+ this.mRefreshTimer[aCalendar.id]
+ .initWithCallback(new timerCallback(aCalendar),
+ refreshInterval * 60000,
+ Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+ }
+ },
+
+ clearRefreshTimer: function(aCalendar) {
+ if (aCalendar.id in this.mRefreshTimer &&
+ this.mRefreshTimer[aCalendar.id]) {
+ this.mRefreshTimer[aCalendar.id].cancel();
+ delete this.mRefreshTimer[aCalendar.id];
+ }
+ },
+
+ unregisterCalendar: function(calendar) {
+ this.notifyObservers("onCalendarUnregistering", [calendar]);
+
+ // calendar may be a calICalendar wrapper:
+ if (calendar.wrappedJSObject instanceof calCachedCalendar) {
+ calendar.wrappedJSObject.onCalendarUnregistering();
+ }
+
+ calendar.removeObserver(this.mCalObservers[calendar.id]);
+ Services.prefs.deleteBranch(getPrefBranchFor(calendar.id));
+ flushPrefs();
+
+ if (this.mCache) {
+ delete this.mCache[calendar.id];
+ }
+
+ if (calendar.readOnly) {
+ this.mReadonlyCalendarCount--;
+ }
+
+ if (calendar.getProperty("requiresNetwork") !== false) {
+ this.mNetworkCalendarCount--;
+ }
+ this.mCalendarCount--;
+
+ this.clearRefreshTimer(calendar);
+ },
+
+ // Delete this method for Lightning 4.7 at latest
+ deleteCalendar: function(calendar) {
+ if (!this.deleteCalendar.warningIssued) {
+ cal.WARN("Use of calICalendarManager::deleteCalendar is deprecated" +
+ " and will be removed with the next release. Use" +
+ " ::removeCalendar instead.\n" + cal.STACK(10));
+ this.deleteCalendar.warningIssued = true;
+ }
+
+ const cICM = Components.interfaces.calICalendarManager;
+ this.removeCalendar(calendar, cICM.REMOVE_NO_UNREGISTER);
+ },
+
+ removeCalendar: function(calendar, mode=0) {
+ const cICM = Components.interfaces.calICalendarManager;
+
+ let removeModes = new Set(calendar.getProperty("capabilities.removeModes") || ["unsubscribe"]);
+ if (!removeModes.has("unsubscribe") && !removeModes.has("delete")) {
+ // Removing is not allowed
+ return;
+ }
+
+ if ((mode & cICM.REMOVE_NO_UNREGISTER) && this.mCache &&
+ (calendar.id in this.mCache)) {
+ throw new Components.Exception("Can't remove a registered calendar");
+ } else if (!(mode & cICM.REMOVE_NO_UNREGISTER)) {
+ this.unregisterCalendar(calendar);
+ }
+
+ // This observer notification needs to be fired for both unsubscribe
+ // and delete, we don't differ this at the moment.
+ this.notifyObservers("onCalendarDeleting", [calendar]);
+
+ // For deleting, we also call the deleteCalendar method from the provider.
+ if (removeModes.has("delete") && (mode & cICM.REMOVE_NO_DELETE) == 0) {
+ let wrappedCalendar = cal.wrapInstance(calendar, Components.interfaces.calICalendarProvider);
+ if (!wrappedCalendar) {
+ throw new Components.Exception("Calendar is missing a provider implementation for delete");
+ }
+
+ wrappedCalendar.deleteCalendar(calendar, null);
+ }
+ },
+
+ getCalendarById: function(aId) {
+ if (aId in this.mCache) {
+ return this.mCache[aId];
+ } else {
+ return null;
+ }
+ },
+
+ getCalendars: function(count) {
+ this.assureCache();
+ let calendars = [];
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ calendars.push(calendar);
+ }
+ count.value = calendars.length;
+ return calendars;
+ },
+
+ assureCache: function() {
+ if (!this.mCache) {
+ this.mCache = {};
+ this.mCalObservers = {};
+
+ let allCals = {};
+ for (let key of Services.prefs.getChildList(REGISTRY_BRANCH)) { // merge down all keys
+ allCals[key.substring(0, key.indexOf(".", REGISTRY_BRANCH.length))] = true;
+ }
+
+ for (let calBranch in allCals) {
+ let id = calBranch.substring(REGISTRY_BRANCH.length);
+ let ctype = Preferences.get(calBranch + ".type", null);
+ let curi = Preferences.get(calBranch + ".uri", null);
+
+ try {
+ if (!ctype || !curi) { // sanity check
+ Services.prefs.deleteBranch(calBranch + ".");
+ continue;
+ }
+
+ let uri = cal.makeURL(curi);
+ let calendar = this.createCalendar(ctype, uri);
+ if (calendar) {
+ calendar.id = id;
+ if (calendar.getProperty("auto-enabled")) {
+ calendar.deleteProperty("disabled");
+ calendar.deleteProperty("auto-enabled");
+ }
+
+ if ((calendar.getProperty("cache.supported") !== false) &&
+ (calendar.getProperty("cache.enabled") ||
+ calendar.getProperty("cache.always"))) {
+ calendar = new calCachedCalendar(calendar);
+ }
+ } else { // create dummy calendar that stays disabled for this run:
+ calendar = new calDummyCalendar(ctype);
+ calendar.id = id;
+ calendar.uri = uri;
+ // try to enable on next startup if calendar has been enabled:
+ if (!calendar.getProperty("disabled")) {
+ calendar.setProperty("auto-enabled", true);
+ }
+ calendar.setProperty("disabled", true);
+ }
+
+ this.setupCalendar(calendar);
+ } catch (exc) {
+ cal.ERROR("Can't create calendar for " + id + " (" + ctype + ", " + curi + "): " + exc);
+ }
+ }
+
+ // do refreshing in a second step, when *all* calendars are already available
+ // via getCalendars():
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ if (!calendar.getProperty("disabled") && calendar.canRefresh) {
+ calendar.refresh();
+ }
+ }
+ }
+ },
+
+ getCalendarPref_: function(calendar, name) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+
+ let branch = getPrefBranchFor(calendar.id) + name;
+ let value = Preferences.get(branch, null);
+
+ if (typeof value == "string" && value.startsWith("bignum:")) {
+ let converted = Number(value.substr(7));
+ if (!isNaN(converted)) {
+ value = converted;
+ }
+ }
+ return value;
+ },
+
+ setCalendarPref_: function(calendar, name, value) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+
+ let branch = getPrefBranchFor(calendar.id) + name;
+
+ if (typeof value == "number" && (value > MAX_INT || value < MIN_INT || !Number.isInteger(value))) {
+ // This is something the preferences service can't store directly.
+ // Convert to string and tag it so we know how to handle it.
+ value = "bignum:" + value;
+ }
+
+ // Delete before to allow pref-type changes, then set the pref.
+ Services.prefs.deleteBranch(branch);
+ if (value !== null && value !== undefined) {
+ Preferences.set(branch, value);
+ }
+ },
+
+ deleteCalendarPref_: function(calendar, name) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+ Services.prefs.deleteBranch(getPrefBranchFor(calendar.id) + name);
+ },
+
+ mObservers: null,
+ addObserver: function(aObserver) { return this.mObservers.add(aObserver); },
+ removeObserver: function(aObserver) { return this.mObservers.remove(aObserver); },
+ notifyObservers: function(functionName, args) { return this.mObservers.notify(functionName, args); },
+
+ mCalendarObservers: null,
+ addCalendarObserver: function(aObserver) { return this.mCalendarObservers.add(aObserver); },
+ removeCalendarObserver: function(aObserver) { return this.mCalendarObservers.remove(aObserver); },
+ notifyCalendarObservers: function(functionName, args) { return this.mCalendarObservers.notify(functionName, args); }
+};
+
+function equalMessage(msg1, msg2) {
+ if (msg1.GetString(0) == msg2.GetString(0) &&
+ msg1.GetString(1) == msg2.GetString(1) &&
+ msg1.GetString(2) == msg2.GetString(2)) {
+ return true;
+ }
+ return false;
+}
+
+function calMgrCalendarObserver(calendar, calMgr) {
+ this.calendar = calendar;
+ // We compare this to determine if the state actually changed.
+ this.storedReadOnly = calendar.readOnly;
+ this.announcedMessages = [];
+ this.calMgr = calMgr;
+}
+
+calMgrCalendarObserver.prototype = {
+ calendar: null,
+ storedReadOnly: null,
+ calMgr: null,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.nsIWindowMediatorListener,
+ Components.interfaces.calIObserver
+ ]),
+
+ // calIObserver:
+ onStartBatch: function() { return this.calMgr.notifyCalendarObservers("onStartBatch", arguments); },
+ onEndBatch: function() { return this.calMgr.notifyCalendarObservers("onEndBatch", arguments); },
+ onLoad: function(calendar) { return this.calMgr.notifyCalendarObservers("onLoad", arguments); },
+ onAddItem: function(aItem) { return this.calMgr.notifyCalendarObservers("onAddItem", arguments); },
+ onModifyItem: function(aNewItem, aOldItem) { return this.calMgr.notifyCalendarObservers("onModifyItem", arguments); },
+ onDeleteItem: function(aDeletedItem) { return this.calMgr.notifyCalendarObservers("onDeleteItem", arguments); },
+ onError: function(aCalendar, aErrNo, aMessage) {
+ this.calMgr.notifyCalendarObservers("onError", arguments);
+ this.announceError(aCalendar, aErrNo, aMessage);
+ },
+
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ this.calMgr.notifyCalendarObservers("onPropertyChanged", arguments);
+ switch (aName) {
+ case "requiresNetwork":
+ this.calMgr.mNetworkCalendarCount += (aValue ? 1 : -1);
+ break;
+ case "readOnly":
+ this.calMgr.mReadonlyCalendarCount += (aValue ? 1 : -1);
+ break;
+ case "refreshInterval":
+ this.calMgr.setupRefreshTimer(aCalendar);
+ break;
+ case "cache.enabled":
+ this.changeCalendarCache(...arguments);
+ break;
+ case "disabled":
+ if (!aValue && aCalendar.canRefresh) {
+ aCalendar.refresh();
+ }
+ break;
+ }
+ },
+
+ changeCalendarCache: function(aCalendar, aName, aValue, aOldValue) {
+ const cICM = Components.interfaces.calICalendarManager;
+ aOldValue = aOldValue || false;
+ aValue = aValue || false;
+
+ // hack for bug 1182264 to deal with calendars, which have set cache.enabled, but in fact do
+ // not support caching (like storage calendars) - this also prevents enabling cache again
+ if (aCalendar.getProperty("cache.supported") === false) {
+ if (aCalendar.getProperty("cache.enabled") === true) {
+ aCalendar.deleteProperty("cache.enabled");
+ }
+ return;
+ }
+
+ if (aOldValue != aValue) {
+ // Try to find the current sort order
+ let sortOrderPref = Preferences.get("calendar.list.sortOrder", "").split(" ");
+ let initialSortOrderPos = null;
+ for (let i = 0; i < sortOrderPref.length; ++i) {
+ if (sortOrderPref[i] == aCalendar.id) {
+ initialSortOrderPos = i;
+ }
+ }
+ // Enabling or disabling cache on a calendar re-creates
+ // it so the registerCalendar call can wrap/unwrap the
+ // calCachedCalendar facade saving the user the need to
+ // restart Thunderbird and making sure a new Id is used.
+ this.calMgr.removeCalendar(aCalendar, cICM.REMOVE_NO_DELETE);
+ let newCal = this.calMgr.createCalendar(aCalendar.type, aCalendar.uri);
+ newCal.name = aCalendar.name;
+
+ // TODO: if properties get added this list will need to be adjusted,
+ // ideally we should add a "getProperties" method to calICalendar.idl
+ // to retrieve all non-transient properties for a calendar.
+ let propsToCopy = [
+ "color",
+ "disabled",
+ "auto-enabled",
+ "cache.enabled",
+ "refreshInterval",
+ "suppressAlarms",
+ "calendar-main-in-composite",
+ "calendar-main-default",
+ "readOnly",
+ "imip.identity.key"
+ ];
+ for (let prop of propsToCopy) {
+ newCal.setProperty(prop, aCalendar.getProperty(prop));
+ }
+
+ if (initialSortOrderPos != null) {
+ newCal.setProperty("initialSortOrderPos",
+ initialSortOrderPos);
+ }
+ this.calMgr.registerCalendar(newCal);
+ } else if (aCalendar.wrappedJSObject instanceof calCachedCalendar) {
+ // any attempt to switch this flag will reset the cached calendar;
+ // could be useful for users in case the cache may be corrupted.
+ aCalendar.wrappedJSObject.setupCachedCalendar();
+ }
+ },
+
+ onPropertyDeleting: function(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, false, true);
+ },
+
+ // Error announcer specific functions
+ announceError: function(aCalendar, aErrNo, aMessage) {
+ let paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Components.interfaces.nsIDialogParamBlock);
+ let props = Services.strings.createBundle("chrome://calendar/locale/calendar.properties");
+ let errMsg;
+ paramBlock.SetNumberStrings(3);
+ if (!this.storedReadOnly && this.calendar.readOnly) {
+ // Major errors change the calendar to readOnly
+ errMsg = props.formatStringFromName("readOnlyMode", [this.calendar.name], 1);
+ } else if (!this.storedReadOnly && !this.calendar.readOnly) {
+ // Minor errors don't, but still tell the user something went wrong
+ errMsg = props.formatStringFromName("minorError", [this.calendar.name], 1);
+ } else {
+ // The calendar was already in readOnly mode, but still tell the user
+ errMsg = props.formatStringFromName("stillReadOnlyError", [this.calendar.name], 1);
+ }
+
+ // When possible, change the error number into its name, to
+ // make it slightly more readable.
+ let errCode = "0x" + aErrNo.toString(16);
+ const calIErrors = Components.interfaces.calIErrors;
+ // Check if it is worth enumerating all the error codes.
+ if (aErrNo & calIErrors.ERROR_BASE) {
+ for (let err in calIErrors) {
+ if (calIErrors[err] == aErrNo) {
+ errCode = err;
+ }
+ }
+ }
+
+ let message;
+ switch (aErrNo) {
+ case calIErrors.CAL_UTF8_DECODING_FAILED:
+ message = props.GetStringFromName("utf8DecodeError");
+ break;
+ case calIErrors.ICS_MALFORMEDDATA:
+ message = props.GetStringFromName("icsMalformedError");
+ break;
+ case calIErrors.MODIFICATION_FAILED:
+ errMsg = calGetString("calendar", "errorWriting", [aCalendar.name]);
+ // falls through
+ default:
+ message = aMessage;
+ }
+
+
+ paramBlock.SetString(0, errMsg);
+ paramBlock.SetString(1, errCode);
+ paramBlock.SetString(2, message);
+
+ this.storedReadOnly = this.calendar.readOnly;
+ let errorCode = calGetString("calendar", "errorCode", [errCode]);
+ let errorDescription = calGetString("calendar", "errorDescription", [message]);
+ let summary = errMsg + " " + errorCode + ". " + errorDescription;
+
+ // Log warnings in error console.
+ // Report serious errors in both error console and in prompt window.
+ if (aErrNo == calIErrors.MODIFICATION_FAILED) {
+ Components.utils.reportError(summary);
+ this.announceParamBlock(paramBlock);
+ } else {
+ cal.WARN(summary);
+ }
+ },
+
+ announceParamBlock: function(paramBlock) {
+ function awaitLoad(event) {
+ promptWindow.removeEventListener("load", awaitLoad, false);
+ promptWindow.addEventListener("unload", awaitUnload, false);
+ }
+ let awaitUnload = (event) => {
+ promptWindow.removeEventListener("unload", awaitUnload, false);
+ // unloaded (user closed prompt window),
+ // remove paramBlock and unload listener.
+ try {
+ // remove the message that has been shown from
+ // the list of all announced messages.
+ this.announcedMessages = this.announcedMessages.filter((msg) => {
+ return !equalMessage(msg, paramBlock);
+ });
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ };
+
+ // silently don't do anything if this message already has been
+ // announced without being acknowledged.
+ if (this.announcedMessages.some(equalMessage.bind(null, paramBlock))) {
+ return;
+ }
+
+ // this message hasn't been announced recently, remember the details of
+ // the message for future reference.
+ this.announcedMessages.push(paramBlock);
+
+ // Will remove paramBlock from announced messages when promptWindow is
+ // closed. (Closing fires unloaded event, but promptWindow is also
+ // unloaded [to clean it?] before loading, so wait for detected load
+ // event before detecting unload event that signifies user closed this
+ // prompt window.)
+ let promptUrl = "chrome://calendar/content/calendar-error-prompt.xul";
+ let features = "chrome,dialog=yes,alwaysRaised=yes";
+ let promptWindow = Services.ww.openWindow(null, promptUrl, "_blank", features, paramBlock);
+ promptWindow.addEventListener("load", awaitLoad, false);
+ }
+};
+
+function calDummyCalendar(type) {
+ this.initProviderBase();
+ this.type = type;
+}
+calDummyCalendar.prototype = {
+ __proto__: cal.ProviderBase.prototype,
+
+ getProperty: function(aName) {
+ switch (aName) {
+ case "force-disabled":
+ return true;
+ default:
+ return this.__proto__.__proto__.getProperty.apply(this, arguments);
+ }
+ }
+};
+
+function getPrefBranchFor(id) {
+ return REGISTRY_BRANCH + id + ".";
+}
+
+/**
+ * Helper function to flush the preferences file. If the application crashes
+ * after a calendar has been created using the prefs registry, then the calendar
+ * won't show up. Writing the prefs helps counteract.
+ */
+function flushPrefs() {
+ Services.prefs.savePrefFile(null);
+}
+
+/**
+ * Callback object for the refresh timer. Should be called as an object, i.e
+ * let foo = new timerCallback(calendar);
+ *
+ * @param aCalendar The calendar to refresh on notification
+ */
+function timerCallback(aCalendar) {
+ this.notify = function(aTimer) {
+ if (!aCalendar.getProperty("disabled") && aCalendar.canRefresh) {
+ aCalendar.refresh();
+ }
+ };
+}
+
+var gCalendarManagerAddonListener = {
+ onDisabling: function(aAddon, aNeedsRestart) {
+ if (!this.queryUninstallProvider(aAddon)) {
+ // If the addon should not be disabled, then re-enable it.
+ aAddon.userDisabled = false;
+ }
+ },
+
+ onUninstalling: function(aAddon, aNeedsRestart) {
+ if (!this.queryUninstallProvider(aAddon)) {
+ // If the addon should not be uninstalled, then cancel the uninstall.
+ aAddon.cancelUninstall();
+ }
+ },
+
+ queryUninstallProvider: function(aAddon) {
+ const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xul";
+ const features = "chrome,titlebar,resizable,modal";
+ let calMgr = cal.getCalendarManager();
+ let affectedCalendars =
+ calMgr.getCalendars({}).filter(calendar => calendar.providerID == aAddon.id);
+ if (!affectedCalendars.length) {
+ // If no calendars are affected, then everything is fine.
+ return true;
+ }
+
+ let args = { shouldUninstall: false, extension: aAddon };
+
+ // Now find a window. The best choice would be the most recent
+ // addons window, otherwise the most recent calendar window, or we
+ // create a new toplevel window.
+ let win = Services.wm.getMostRecentWindow("Extension:Manager") ||
+ cal.getCalendarWindow();
+ if (win) {
+ win.openDialog(uri, "CalendarProviderUninstallDialog", features, args);
+ } else {
+ // Use the window watcher to open a parentless window.
+ Services.ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args);
+ }
+
+ // Now that we are done, check if the dialog was accepted or canceled.
+ return args.shouldUninstall;
+ }
+};
+
+function appendToRealm(authHeader, appendStr) {
+ let isEscaped = false;
+ let idx = authHeader.search(/realm="(.*?)(\\*)"/);
+ if (idx > -1) {
+ let remain = authHeader.substr(idx + 7); idx += 7;
+ while (remain.length && !isEscaped) {
+ let match = remain.match(/(.*?)(\\*)"/);
+ idx += match[0].length;
+
+ isEscaped = ((match[2].length % 2) == 0);
+ if (!isEscaped) {
+ remain = remain.substr(match[0].length);
+ }
+ }
+ return authHeader.substr(0, idx - 1) + " " +
+ appendStr + authHeader.substr(idx - 1);
+ } else {
+ return authHeader;
+ }
+}
diff --git a/calendar/base/src/calCalendarSearchService.js b/calendar/base/src/calCalendarSearchService.js
new file mode 100644
index 000000000..2abecf562
--- /dev/null
+++ b/calendar/base/src/calCalendarSearchService.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function calCalendarSearchListener(numOperations, finalListener) {
+ this.mFinalListener = finalListener;
+ this.mNumOperations = numOperations;
+ this.mResults = [];
+
+ this.opGroup = new calOperationGroup(() => {
+ this.notifyResult(null);
+ });
+}
+calCalendarSearchListener.prototype = {
+ mFinalListener: null,
+ mNumOperations: 0,
+ opGroup: null,
+
+ notifyResult: function(result) {
+ let listener = this.mFinalListener;
+ if (listener) {
+ if (!this.opGroup.isPending) {
+ this.mFinalListener = null;
+ }
+ listener.onResult(this.opGroup, result);
+ }
+ },
+
+ // calIGenericOperationListener:
+ onResult: function(aOperation, aResult) {
+ if (this.mFinalListener) {
+ if (!aOperation || !aOperation.isPending) {
+ --this.mNumOperations;
+ if (this.mNumOperations == 0) {
+ this.opGroup.notifyCompleted();
+ }
+ }
+ if (aResult) {
+ this.notifyResult(aResult);
+ }
+ }
+ }
+};
+
+function calCalendarSearchService() {
+ this.wrappedJSObject = this;
+ this.mProviders = new calInterfaceBag(Components.interfaces.calICalendarSearchProvider);
+}
+var calCalendarSearchServiceClassID = Components.ID("{f5f743cd-8997-428e-bc1b-644e73f61203}");
+var calCalendarSearchServiceInterfaces = [
+ Components.interfaces.calICalendarSearchProvider,
+ Components.interfaces.calICalendarSearchService
+];
+calCalendarSearchService.prototype = {
+ mProviders: null,
+
+ classID: calCalendarSearchServiceClassID,
+ QueryInterface: XPCOMUtils.generateQI(calCalendarSearchServiceInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calCalendarSearchServiceClassID,
+ contractID: "@mozilla.org/calendar/calendarsearch-service;1",
+ classDescription: "Calendar Search Service",
+ interfaces: calCalendarSearchServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ // calICalendarSearchProvider:
+ searchForCalendars: function(aString, aHints, aMaxResults, aListener) {
+ let groupListener = new calCalendarSearchListener(this.mProviders.size, aListener);
+ function searchForCalendars_(provider) {
+ try {
+ groupListener.opGroup.add(provider.searchForCalendars(aString,
+ aHints,
+ aMaxResults,
+ groupListener));
+ } catch (exc) {
+ Components.utils.reportError(exc);
+ groupListener.onResult(null, []); // dummy to adopt mNumOperations
+ }
+ }
+ this.mProviders.forEach(searchForCalendars_);
+ return groupListener.opGroup;
+ },
+
+ // calICalendarSearchService:
+ getProviders: function(out_aCount) {
+ let ret = this.mProviders.interfaceArray;
+ out_aCount.value = ret.length;
+ return ret;
+ },
+ addProvider: function(aProvider) {
+ this.mProviders.add(aProvider);
+ },
+ removeProvider: function(aProvider) {
+ this.mProviders.remove(aProvider);
+ }
+};
diff --git a/calendar/base/src/calDateTimeFormatter.js b/calendar/base/src/calDateTimeFormatter.js
new file mode 100644
index 000000000..23c06af3f
--- /dev/null
+++ b/calendar/base/src/calDateTimeFormatter.js
@@ -0,0 +1,296 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var nsIScriptableDateFormat = Components.interfaces.nsIScriptableDateFormat;
+
+function calDateTimeFormatter() {
+ this.wrappedJSObject = this;
+ this.mDateStringBundle = Services.strings.createBundle("chrome://calendar/locale/dateFormat.properties");
+
+ this.mDateService =
+ Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(nsIScriptableDateFormat);
+
+ // Do does the month or day come first in this locale?
+ this.mMonthFirst = false;
+
+ // If LONG FORMATTED DATE is same as short formatted date,
+ // then OS has poor extended/long date config, so use workaround.
+ this.mUseLongDateService = true;
+ let probeDate =
+ Components.classes["@mozilla.org/calendar/datetime;1"]
+ .createInstance(Components.interfaces.calIDateTime);
+ probeDate.timezone = UTC();
+ probeDate.year = 2002;
+ probeDate.month = 3;
+ probeDate.day = 5;
+ try {
+ // We're try/catching the calls to nsScriptableDateFormat since it's
+ // outside this module. We're also reusing probeDate rather than
+ // creating 3 discrete calDateTimes for performance.
+ let probeStringA = this.formatDateShort(probeDate);
+ let longProbeString = this.formatDateLong(probeDate);
+ probeDate.month = 4;
+ let probeStringB = this.formatDateShort(probeDate);
+ probeDate.month = 3;
+ probeDate.day = 6;
+ let probeStringC = this.formatDateShort(probeDate);
+
+ // Compare the index of the first differing character between
+ // probeStringA to probeStringB and probeStringA to probeStringC.
+ for (let i = 0; i < probeStringA.length; i++) {
+ if (probeStringA[i] != probeStringB[i]) {
+ this.mMonthFirst = true;
+ break;
+ } else if (probeStringA[i] != probeStringC[i]) {
+ this.mMonthFirst = false;
+ break;
+ }
+ }
+
+ // On Unix extended/long date format may be created using %Ex instead
+ // of %x. Some systems may not support it and return "Ex" or same as
+ // short string. In that case, don't use long date service, use a
+ // workaround hack instead.
+ if (longProbeString == null ||
+ longProbeString.length < 4 ||
+ longProbeString == probeStringA) {
+ this.mUseLongDateService = false;
+ }
+ } catch (e) {
+ this.mUseLongDateService = false;
+ }
+}
+var calDateTimeFormatterClassID = Components.ID("{4123da9a-f047-42da-a7d0-cc4175b9f36a}");
+var calDateTimeFormatterInterfaces = [Components.interfaces.calIDateTimeFormatter];
+calDateTimeFormatter.prototype = {
+ classID: calDateTimeFormatterClassID,
+ QueryInterface: XPCOMUtils.generateQI(calDateTimeFormatterInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calDateTimeFormatterClassID,
+ contractID: "@mozilla.org/calendar/datetime-formatter;1",
+ classDescription: "Formats Dates and Times",
+ interfaces: calDateTimeFormatterInterfaces,
+ }),
+
+ formatDate: function(aDate) {
+ // Format the date using user's format preference (long or short)
+ let format = Preferences.get("calendar.date.format", 0);
+ return (format == 0 ? this.formatDateLong(aDate) : this.formatDateShort(aDate));
+ },
+
+ formatDateShort: function(aDate) {
+ return this.mDateService.FormatDate("",
+ nsIScriptableDateFormat.dateFormatShort,
+ aDate.year,
+ aDate.month + 1,
+ aDate.day);
+ },
+
+ formatDateLong: function(aDate) {
+ let longDate;
+ if (this.mUseLongDateService) {
+ longDate = this.mDateService.FormatDate("",
+ nsIScriptableDateFormat.dateFormatLong,
+ aDate.year,
+ aDate.month + 1,
+ aDate.day);
+ // check whether weekday name appears as in Lightning localization. if not, this is
+ // probably a minority language without OS support, so we should fall back to compose
+ // longDate on our own. May be not needed anymore once bug 441167 is fixed.
+ if (!longDate.includes(this.dayName(aDate.weekday)) &&
+ !longDate.includes(this.shortDayName(aDate.weekday))) {
+ longDate = null;
+ this.mUseLongDateService = false;
+ }
+ }
+ if (longDate == null) {
+ // HACK We are probably on Linux or have a minority localization and want a string in
+ // long format. dateService.dateFormatLong on Linux may return a short string, so
+ // build our own.
+ longDate = cal.calGetString("calendar", "formatDateLong",
+ [this.shortDayName(aDate.weekday),
+ this.formatDayWithOrdinal(aDate.day),
+ this.shortMonthName(aDate.month),
+ aDate.year]);
+ }
+ return longDate;
+ },
+
+ formatDateWithoutYear: function(aDate) {
+ // Doing this the hard way, because nsIScriptableDateFormat doesn't
+ // have a way to not include the year.
+ if (this.mMonthFirst) {
+ return this.shortMonthName(aDate.month) + " " + this.formatDayWithOrdinal(aDate.day);
+ } else {
+ return this.formatDayWithOrdinal(aDate.day) + " " + this.shortMonthName(aDate.month);
+ }
+ },
+
+ formatTime: function(aDate) {
+ if (aDate.isDate) {
+ return this.mDateStringBundle.GetStringFromName("AllDay");
+ }
+
+ return this.mDateService.FormatTime("",
+ nsIScriptableDateFormat.timeFormatNoSeconds,
+ aDate.hour,
+ aDate.minute,
+ 0);
+ },
+
+ formatDateTime: function(aDate) {
+ let formattedDate = this.formatDate(aDate);
+ let formattedTime = this.formatTime(aDate);
+
+ let timeBeforeDate = Preferences.get("calendar.date.formatTimeBeforeDate", false);
+ if (timeBeforeDate) {
+ return formattedTime + " " + formattedDate;
+ } else {
+ return formattedDate + " " + formattedTime;
+ }
+ },
+
+ formatTimeInterval: function(aStartDate, aEndDate) {
+ if (!aStartDate && aEndDate) {
+ return this.formatTime(aEndDate);
+ }
+ if (!aEndDate && aStartDate) {
+ return this.formatTime(aStartDate);
+ }
+ if (!aStartDate && !aEndDate) {
+ return "";
+ }
+
+ // TODO do we need l10n for this?
+ // TODO should we check for the same day? The caller should know what
+ // he is doing...
+ return this.formatTime(aStartDate) + "\u2013" + this.formatTime(aEndDate);
+ },
+
+ formatInterval: function(aStartDate, aEndDate) {
+ // Check for tasks without start and/or due date
+ if (aEndDate == null && aStartDate == null) {
+ return calGetString("calendar", "datetimeIntervalTaskWithoutDate");
+ } else if (aEndDate == null) {
+ let startDateString = this.formatDate(aStartDate);
+ let startTime = this.formatTime(aStartDate);
+ return calGetString("calendar", "datetimeIntervalTaskWithoutDueDate", [startDateString, startTime]);
+ } else if (aStartDate == null) {
+ let endDateString = this.formatDate(aEndDate);
+ let endTime = this.formatTime(aEndDate);
+ return calGetString("calendar", "datetimeIntervalTaskWithoutStartDate", [endDateString, endTime]);
+ }
+ // Here there are only events or tasks with both start and due date.
+ // make sure start and end use the same timezone when formatting intervals:
+ let endDate = aEndDate.getInTimezone(aStartDate.timezone);
+ let testdate = aStartDate.clone();
+ testdate.isDate = true;
+ let sameDay = (testdate.compare(endDate) == 0);
+ if (aStartDate.isDate) {
+ // All-day interval, so we should leave out the time part
+ if (sameDay) {
+ return this.formatDateLong(aStartDate);
+ } else {
+ let startDay = this.formatDayWithOrdinal(aStartDate.day);
+ let startYear = aStartDate.year;
+ let endDay = this.formatDayWithOrdinal(endDate.day);
+ let endYear = endDate.year;
+ if (aStartDate.year != endDate.year) {
+ let startMonthName = cal.formatMonth(aStartDate.month + 1, "calendar", "daysIntervalBetweenYears");
+ let endMonthName = cal.formatMonth(aEndDate.month + 1, "calendar", "daysIntervalBetweenYears");
+ return cal.calGetString("calendar", "daysIntervalBetweenYears", [startMonthName, startDay, startYear, endMonthName, endDay, endYear]);
+ } else if (aStartDate.month == endDate.month) {
+ let startMonthName = cal.formatMonth(aStartDate.month + 1, "calendar", "daysIntervalInMonth");
+ return cal.calGetString("calendar", "daysIntervalInMonth", [startMonthName, startDay, endDay, endYear]);
+ } else {
+ let startMonthName = cal.formatMonth(aStartDate.month + 1, "calendar", "daysIntervalBetweenMonths");
+ let endMonthName = cal.formatMonth(aEndDate.month + 1, "calendar", "daysIntervalBetweenMonths");
+ return cal.calGetString("calendar", "daysIntervalBetweenMonths", [startMonthName, startDay, endMonthName, endDay, endYear]);
+ }
+ }
+ } else {
+ let startDateString = this.formatDate(aStartDate);
+ let startTime = this.formatTime(aStartDate);
+ let endDateString = this.formatDate(endDate);
+ let endTime = this.formatTime(endDate);
+ // non-allday, so need to return date and time
+ if (sameDay) {
+ // End is on the same day as start, so we can leave out the end date
+ if (startTime == endTime) {
+ // End time is on the same time as start, so we can leave out the end time
+ // "5 Jan 2006 13:00"
+ return calGetString("calendar", "datetimeIntervalOnSameDateTime", [startDateString, startTime]);
+ } else {
+ // still include end time
+ // "5 Jan 2006 13:00 - 17:00"
+ return calGetString("calendar", "datetimeIntervalOnSameDay", [startDateString, startTime, endTime]);
+ }
+ } else {
+ // Spanning multiple days, so need to include date and time
+ // for start and end
+ // "5 Jan 2006 13:00 - 7 Jan 2006 9:00"
+ return calGetString("calendar", "datetimeIntervalOnSeveralDays", [startDateString, startTime, endDateString, endTime]);
+ }
+ }
+ },
+
+ formatDayWithOrdinal: function(aDay) {
+ let ordinalSymbols = this.mDateStringBundle.GetStringFromName("dayOrdinalSymbol").split(",");
+ let dayOrdinalSymbol = ordinalSymbols[aDay - 1] || ordinalSymbols[0];
+ return aDay + dayOrdinalSymbol;
+ },
+
+ _getItemDates: function(aItem) {
+ let start = aItem[calGetStartDateProp(aItem)];
+ let end = aItem[calGetEndDateProp(aItem)];
+ let kDefaultTimezone = calendarDefaultTimezone();
+ // Check for tasks without start and/or due date
+ if (start) {
+ start = start.getInTimezone(kDefaultTimezone);
+ }
+ if (end) {
+ end = end.getInTimezone(kDefaultTimezone);
+ }
+ // EndDate is exclusive. For all-day events, we ened to substract one day,
+ // to get into a format that's understandable.
+ if (start && start.isDate && end) {
+ end.day -= 1;
+ }
+
+ return [start, end];
+ },
+
+ formatItemInterval: function(aItem) {
+ return this.formatInterval(...this._getItemDates(aItem));
+ },
+
+ formatItemTimeInterval: function(aItem) {
+ return this.formatTimeInterval(...this._getItemDates(aItem));
+ },
+
+ monthName: function(aMonthIndex) {
+ let oneBasedMonthIndex = aMonthIndex + 1;
+ return this.mDateStringBundle.GetStringFromName("month." + oneBasedMonthIndex + ".name");
+ },
+
+ shortMonthName: function(aMonthIndex) {
+ let oneBasedMonthIndex = aMonthIndex + 1;
+ return this.mDateStringBundle.GetStringFromName("month." + oneBasedMonthIndex + ".Mmm");
+ },
+
+ dayName: function(aDayIndex) {
+ let oneBasedDayIndex = aDayIndex + 1;
+ return this.mDateStringBundle.GetStringFromName("day." + oneBasedDayIndex + ".name");
+ },
+
+ shortDayName: function(aDayIndex) {
+ let oneBasedDayIndex = aDayIndex + 1;
+ return this.mDateStringBundle.GetStringFromName("day." + oneBasedDayIndex + ".Mmm");
+ }
+};
diff --git a/calendar/base/src/calDefaultACLManager.js b/calendar/base/src/calDefaultACLManager.js
new file mode 100644
index 000000000..dc512be54
--- /dev/null
+++ b/calendar/base/src/calDefaultACLManager.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/* calDefaultACLManager */
+function calDefaultACLManager() {
+ this.mCalendarEntries = {};
+}
+
+var calDefaultACLManagerClassID = Components.ID("{7463258c-6ef3-40a2-89a9-bb349596e927}");
+var calDefaultACLManagerInterfaces = [Components.interfaces.calICalendarACLManager];
+calDefaultACLManager.prototype = {
+ mCalendarEntries: null,
+
+ /* nsISupports, nsIClassInfo */
+ classID: calDefaultACLManagerClassID,
+ QueryInterface: XPCOMUtils.generateQI(calDefaultACLManagerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calDefaultACLManagerClassID,
+ contractID: "@mozilla.org/calendar/acl-manager;1?type=default",
+ classDescription: "Default Calendar ACL Provider",
+ interfaces: calDefaultACLManagerInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ /* calICalendarACLManager */
+ _getCalendarEntryCached: function(aCalendar) {
+ let calUri = aCalendar.uri.spec;
+ if (!(calUri in this.mCalendarEntries)) {
+ this.mCalendarEntries[calUri] = new calDefaultCalendarACLEntry(this, aCalendar);
+ }
+
+ return this.mCalendarEntries[calUri];
+ },
+ getCalendarEntry: function(aCalendar, aListener) {
+ let entry = this._getCalendarEntryCached(aCalendar);
+ aListener.onOperationComplete(aCalendar, Components.results.NS_OK,
+ Components.interfaces.calIOperationListener.GET,
+ null,
+ entry);
+ },
+ getItemEntry: function(aItem) {
+ let calEntry = this._getCalendarEntryCached(aItem.calendar);
+ return new calDefaultItemACLEntry(calEntry);
+ },
+
+};
+
+function calDefaultCalendarACLEntry(aMgr, aCalendar) {
+ this.mACLManager = aMgr;
+ this.mCalendar = aCalendar;
+}
+
+calDefaultCalendarACLEntry.prototype = {
+ mACLManager: null,
+
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICalendarACLEntry]),
+
+ /* calICalendarACLCalendarEntry */
+ get aclManager() {
+ return this.mACLManager;
+ },
+
+ hasAccessControl: false,
+ userIsOwner: true,
+ userCanAddItems: true,
+ userCanDeleteItems: true,
+
+ _getIdentities: function(aCount) {
+ let identities = [];
+ cal.calIterateEmailIdentities(id => identities.push(id));
+ aCount.value = identities.length;
+ return identities;
+ },
+
+ getUserAddresses: function(aCount) {
+ let identities = this.getUserIdentities(aCount);
+ let addresses = identities.map(id => id.email);
+ return addresses;
+ },
+
+ getUserIdentities: function(aCount) {
+ let identity = cal.getEmailIdentityOfCalendar(this.mCalendar);
+ if (identity) {
+ aCount.value = 1;
+ return [identity];
+ } else {
+ return this._getIdentities(aCount);
+ }
+ },
+ getOwnerIdentities: function(aCount) {
+ return this._getIdentities(aCount);
+ },
+
+ refresh: function() {
+ }
+};
+
+function calDefaultItemACLEntry(aCalendarEntry) {
+ this.calendarEntry = aCalendarEntry;
+}
+
+calDefaultItemACLEntry.prototype = {
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIItemACLEntry]),
+
+ /* calIItemACLEntry */
+ calendarEntry: null,
+ userCanModify: true,
+ userCanRespond: true,
+ userCanViewAll: true,
+ userCanViewDateAndTime: true,
+};
+
+/** Module Registration */
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([calDefaultACLManager]);
diff --git a/calendar/base/src/calDefaultACLManager.manifest b/calendar/base/src/calDefaultACLManager.manifest
new file mode 100644
index 000000000..038f2f72e
--- /dev/null
+++ b/calendar/base/src/calDefaultACLManager.manifest
@@ -0,0 +1,2 @@
+component {7463258c-6ef3-40a2-89a9-bb349596e927} calDefaultACLManager.js
+contract @mozilla.org/calendar/acl-manager;1?type=default {7463258c-6ef3-40a2-89a9-bb349596e927}
diff --git a/calendar/base/src/calDeletedItems.js b/calendar/base/src/calDeletedItems.js
new file mode 100644
index 000000000..73c05ae85
--- /dev/null
+++ b/calendar/base/src/calDeletedItems.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+/**
+ * Handles remembering deleted items.
+ *
+ * This is (currently) not a real trashcan. Only ids and time deleted is stored.
+ * Note also that the code doesn't strictly check the calendar of the item,
+ * except when a calendar id is passed to getDeletedDate.
+ */
+function calDeletedItems() {
+ this.wrappedJSObject = this;
+
+ this.completedNotifier = {
+ handleResult: function() {},
+ handleError: function() {},
+ handleCompletion: function() {},
+ };
+}
+
+var calDeletedItemsClassID = Components.ID("{8e6799af-e7e9-4e6c-9a82-a2413e86d8c3}");
+var calDeletedItemsInterfaces = [
+ Components.interfaces.calIDeletedItems,
+ Components.interfaces.nsIObserver,
+ Components.interfaces.calIObserver
+];
+calDeletedItems.prototype = {
+
+ classID: calDeletedItemsClassID,
+ QueryInterface: XPCOMUtils.generateQI(calDeletedItemsInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calDeletedItemsClassID,
+ contractID: "@mozilla.org/calendar/deleted-items-manager;1",
+ classDescription: "Database containing information about deleted items",
+ interfaces: calDeletedItemsInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ DB_SCHEMA_VERSION: 1,
+ STALE_TIME: 30 * 24 * 60 * 60 / 1000, /* 30 days */
+
+ // To make the tests more failsafe, we have an internal notifier function.
+ // As the deleted items store is just meant to be a hint, this should not
+ // be used in real code.
+ completedNotifier: null,
+
+ flush: function() {
+ this.ensureStatements();
+ this.stmtFlush.params.stale_time = cal.now().nativeTime - this.STALE_TIME;
+ this.stmtFlush.executeAsync(this.completedNotifier);
+ },
+
+ getDeletedDate: function(aId, aCalId) {
+ this.ensureStatements();
+ let stmt;
+ if (aCalId) {
+ stmt = this.stmtGetWithCal;
+ stmt.params.calId = aCalId;
+ } else {
+ stmt = this.stmtGet;
+ }
+
+ stmt.params.id = aId;
+ try {
+ if (stmt.executeStep()) {
+ let date = cal.createDateTime();
+ date.nativeTime = stmt.row.time_deleted;
+ return date.getInTimezone(cal.calendarDefaultTimezone());
+ }
+ } catch (e) {
+ cal.ERROR(e);
+ } finally {
+ stmt.reset();
+ }
+ return null;
+ },
+
+ markDeleted: function(aItem) {
+ this.ensureStatements();
+ this.stmtMarkDelete.params.calId = aItem.calendar.id;
+ this.stmtMarkDelete.params.id = aItem.id;
+ this.stmtMarkDelete.params.time = cal.now().nativeTime;
+ this.stmtMarkDelete.params.rid = (aItem.recurrenceId && aItem.recurrenceId.nativeTime) || "";
+ this.stmtMarkDelete.executeAsync(this.completedNotifier);
+ },
+
+ unmarkDeleted: function(aItem) {
+ this.ensureStatements();
+ this.stmtUnmarkDelete.params.id = aItem.id;
+ this.stmtUnmarkDelete.executeAsync(this.completedNotifier);
+ },
+
+ initDB: function() {
+ if (this.mDB) {
+ // Looks like we've already initialized, exit early
+ return;
+ }
+
+ let file = FileUtils.getFile("ProfD", ["calendar-data", "deleted.sqlite"]);
+ this.mDB = Services.storage.openDatabase(file);
+
+ // If this database needs changing, please start using a real schema
+ // management, i.e using PRAGMA user_version and upgrading
+ if (!this.mDB.tableExists("cal_deleted_items")) {
+ const v1_schema = "cal_id TEXT, id TEXT, time_deleted INTEGER, recurrence_id INTEGER";
+ const v1_index = "CREATE INDEX idx_deleteditems ON cal_deleted_items(id,cal_id,recurrence_id)";
+
+ this.mDB.createTable("cal_deleted_items", v1_schema);
+ this.mDB.executeSimpleSQL(v1_index);
+ this.mDB.executeSimpleSQL("PRAGMA user_version = 1");
+ }
+
+ // We will not init the statements now, we can still do that the
+ // first time this interface is used. What we should do though is
+ // to clean up at shutdown
+ cal.addShutdownObserver(this.shutdown.bind(this));
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "profile-after-change") {
+ // Make sure to observe calendar changes so we know when things are
+ // deleted. We don't initialize the statements until first use.
+ cal.getCalendarManager().addCalendarObserver(this);
+ }
+ },
+
+ ensureStatements: function() {
+ if (!this.mDB) {
+ this.initDB();
+ }
+
+ if (!this.stmtMarkDelete) {
+ let stmt = "INSERT OR REPLACE INTO cal_deleted_items (cal_id, id, time_deleted, recurrence_id) VALUES(:calId, :id, :time, :rid)";
+ this.stmtMarkDelete = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtUnmarkDelete) {
+ let stmt = "DELETE FROM cal_deleted_items WHERE id = :id";
+ this.stmtUnmarkDelete = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtGetWithCal) {
+ let stmt = "SELECT time_deleted FROM cal_deleted_items WHERE cal_id = :calId AND id = :id";
+ this.stmtGetWithCal = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtGet) {
+ let stmt = "SELECT time_deleted FROM cal_deleted_items WHERE id = :id";
+ this.stmtGet = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtFlush) {
+ let stmt = "DELETE FROM cal_deleted_items WHERE time_deleted < :stale_time";
+ this.stmtFlush = this.mDB.createStatement(stmt);
+ }
+ },
+
+ shutdown: function() {
+ try {
+ let stmts = [
+ this.stmtMarkDelete, this.stmtUnmarkDelete, this.stmtGet,
+ this.stmtGetWithCal, this.stmtFlush
+ ];
+ for (let stmt of stmts) {
+ stmt.finalize();
+ }
+
+ if (this.mDB) {
+ this.mDB.asyncClose();
+ this.mDB = null;
+ }
+ } catch (e) {
+ cal.ERROR("Error closing deleted items database: " + e);
+ }
+
+ cal.getCalendarManager().removeCalendarObserver(this);
+ },
+
+ // calIObserver
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onModifyItem: function() {},
+ onError: function() {},
+ onPropertyChanged: function() {},
+ onPropertyDeleting: function() {},
+
+ onAddItem: function(aItem) {
+ this.unmarkDeleted(aItem);
+ },
+
+ onDeleteItem: function(aItem) {
+ this.markDeleted(aItem);
+ },
+
+ onLoad: function() {
+ this.flush();
+ }
+};
diff --git a/calendar/base/src/calEvent.js b/calendar/base/src/calEvent.js
new file mode 100644
index 000000000..9d33e1c87
--- /dev/null
+++ b/calendar/base/src/calEvent.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+//
+// constructor
+//
+function calEvent() {
+ this.initItemBase();
+
+ this.eventPromotedProps = {
+ DTSTART: true,
+ DTEND: true,
+ __proto__: this.itemBasePromotedProps
+ };
+}
+var calEventClassID = Components.ID("{974339d5-ab86-4491-aaaf-2b2ca177c12b}");
+var calEventInterfaces = [
+ Components.interfaces.calIItemBase,
+ Components.interfaces.calIEvent,
+ Components.interfaces.calIInternalShallowCopy
+];
+calEvent.prototype = {
+ __proto__: calItemBase.prototype,
+
+ classID: calEventClassID,
+ QueryInterface: XPCOMUtils.generateQI(calEventInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calEventClassID,
+ contractID: "@mozilla.org/calendar/event;1",
+ classDescription: "Calendar Event",
+ interfaces: calEventInterfaces
+ }),
+
+ cloneShallow: function(aNewParent) {
+ let cloned = new calEvent();
+ this.cloneItemBaseInto(cloned, aNewParent);
+ return cloned;
+ },
+
+ createProxy: function(aRecurrenceId) {
+ cal.ASSERT(!this.mIsProxy, "Tried to create a proxy for an existing proxy!", true);
+
+ let proxy = new calEvent();
+
+ // override proxy's DTSTART/DTEND/RECURRENCE-ID
+ // before master is set (and item might get immutable):
+ let endDate = aRecurrenceId.clone();
+ endDate.addDuration(this.duration);
+ proxy.endDate = endDate;
+ proxy.startDate = aRecurrenceId;
+
+ proxy.initializeProxy(this, aRecurrenceId);
+ proxy.mDirty = false;
+
+ return proxy;
+ },
+
+ makeImmutable: function() {
+ this.makeItemBaseImmutable();
+ },
+
+ get duration() {
+ if (this.endDate && this.startDate) {
+ return this.endDate.subtractDate(this.startDate);
+ } else {
+ // Return a null-duration if we don't have an end date
+ return cal.createDuration();
+ }
+ },
+
+ get recurrenceStartDate() {
+ return this.startDate;
+ },
+
+ icsEventPropMap: [
+ { cal: "DTSTART", ics: "startTime" },
+ { cal: "DTEND", ics: "endTime" }],
+
+ set icalString(value) {
+ this.icalComponent = getIcsService().parseICS(value, null);
+ },
+
+ get icalString() {
+ let calcomp = getIcsService().createIcalComponent("VCALENDAR");
+ calSetProdidVersion(calcomp);
+ calcomp.addSubcomponent(this.icalComponent);
+ return calcomp.serializeToICS();
+ },
+
+ get icalComponent() {
+ let icssvc = getIcsService();
+ let icalcomp = icssvc.createIcalComponent("VEVENT");
+ this.fillIcalComponentFromBase(icalcomp);
+ this.mapPropsToICS(icalcomp, this.icsEventPropMap);
+
+ let bagenum = this.propertyEnumerator;
+ while (bagenum.hasMoreElements()) {
+ let iprop = bagenum.getNext()
+ .QueryInterface(Components.interfaces.nsIProperty);
+ try {
+ if (!this.eventPromotedProps[iprop.name]) {
+ let icalprop = icssvc.createIcalProperty(iprop.name);
+ icalprop.value = iprop.value;
+ let propBucket = this.mPropertyParams[iprop.name];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName, propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid event parameter value " + paramName + "=" + propBucket[paramName]);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ icalcomp.addProperty(icalprop);
+ }
+ } catch (e) {
+ cal.ERROR("failed to set " + iprop.name + " to " + iprop.value + ": " + e + "\n");
+ }
+ }
+ return icalcomp;
+ },
+
+ eventPromotedProps: null,
+
+ set icalComponent(event) {
+ this.modify();
+ if (event.componentType != "VEVENT") {
+ event = event.getFirstSubcomponent("VEVENT");
+ if (!event) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ this.mEndDate = undefined;
+ this.setItemBaseFromICS(event);
+ this.mapPropsFromICS(event, this.icsEventPropMap);
+
+ this.importUnpromotedProperties(event, this.eventPromotedProps);
+
+ // Importing didn't really change anything
+ this.mDirty = false;
+ },
+
+ isPropertyPromoted: function(name) {
+ // avoid strict undefined property warning
+ return this.eventPromotedProps[name] || false;
+ },
+
+ set startDate(value) {
+ this.modify();
+
+ // We're about to change the start date of an item which probably
+ // could break the associated calIRecurrenceInfo. We're calling
+ // the appropriate method here to adjust the internal structure in
+ // order to free clients from worrying about such details.
+ if (this.parentItem == this) {
+ let rec = this.recurrenceInfo;
+ if (rec) {
+ rec.onStartDateChange(value, this.startDate);
+ }
+ }
+
+ return this.setProperty("DTSTART", value);
+ },
+
+ get startDate() {
+ return this.getProperty("DTSTART");
+ },
+
+ mEndDate: undefined,
+ get endDate() {
+ let endDate = this.mEndDate;
+ if (endDate === undefined) {
+ endDate = this.getProperty("DTEND");
+ if (!endDate && this.startDate) {
+ endDate = this.startDate.clone();
+ let dur = this.getProperty("DURATION");
+ if (dur) {
+ // If there is a duration set on the event, calculate the right end time.
+ endDate.addDuration(cal.createDuration(dur));
+ } else if (endDate.isDate) {
+ // If the start time is a date-time the event ends on the same calendar
+ // date and time of day. If the start time is a date the events
+ // non-inclusive end is the end of the calendar date.
+ endDate.day += 1;
+ }
+ }
+ this.mEndDate = endDate;
+ }
+ return endDate;
+ },
+
+ set endDate(value) {
+ this.deleteProperty("DURATION"); // setting endDate once removes DURATION
+ this.setProperty("DTEND", value);
+ return (this.mEndDate = value);
+ }
+};
diff --git a/calendar/base/src/calFilter.js b/calendar/base/src/calFilter.js
new file mode 100644
index 000000000..e06c8ca50
--- /dev/null
+++ b/calendar/base/src/calFilter.js
@@ -0,0 +1,911 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/**
+ * Object that contains a set of filter properties that may be used by a calFilter object
+ * to filter a set of items.
+ * Supported filter properties:
+ * start, end: Specifies the relative date range to use when calculating the filter date
+ * range. The relative date range may relative to the current date and time, the
+ * currently selected date, or the dates range of the current view. The actual
+ * date range used to filter items will be calculated by the calFilter object
+ * by using the updateFilterDates function, which may be called multiple times
+ * to reflect changes in the current date and time, and changes to the view.
+ *
+ *
+ * The properties may be set to one of the folowing values:
+ * - FILTER_DATE_ALL: An unbound date range.
+ * - FILTER_DATE_XXX: One of the defined relative date ranges.
+ * - A string that may be converted to a calIDuration object that will be used
+ * as an offset to the current date and time.
+ *
+ * The start and end properties may have values representing different relative
+ * date ranges, in which case the filter start date will be calculated as the start
+ * of the relative range specified by the start property, while the filter end date
+ * will be calculated as the end of the relative range specified by the end
+ * property.
+ *
+ * due: Specifies the filter property for the due date of tasks. This filter has no
+ * effect when filtering events.
+ *
+ * The property has a bit field value, with the FILTER_DUE_XXX bit flags set
+ * to indicate that tasks with the corresponding due property value should match
+ * the filter.
+ *
+ * If the value is set to null the due date will not be considered when filtering.
+ *
+ * status: Specifies the filter property for the status of tasks. This filter has no
+ * effect when filtering events.
+ *
+ * The property has a bit field value, with the FILTER_STATUS_XXX bit flags set
+ * to indicate that tasks with the corresponding status property value should match
+ * the filter.
+ *
+ * If the value is set to null the status will not be considered when filtering.
+ *
+ * category: Specifies the filter property for the item category.
+ *
+ * The property may be set to one of the folowing values:
+ * - null: The item category will not be considered when filtering.
+ * - A string: The item will match the filter if any of it's categories match the
+ * category specified by the property.
+ * - An array: The item will match the filter if any of it's categories match any
+ * of the categories contained in the Array specified by the property.
+ *
+ * occurrences: Specifies the filter property for returning occurrences of repeating items.
+ *
+ * The property may be set to one of the folowing values:
+ * - null, FILTER_OCCURRENCES_BOUND: The default occurrence handling. Occurrences
+ * will be returned only for date ranges with a bound end date.
+ * - FILTER_OCCURRENCES_NONE: Only the parent items will be returned.
+ * - FILTER_OCCURRENCES_PAST_AND_NEXT: Returns past occurrences and the next future
+ * matching occurrence if one is found.
+ *
+ * onfilter: A callback function that may be used to apply additional custom filter
+ * constraints. If specified, the callback function will be called after any other
+ * specified filter properties are tested.
+ *
+ * The callback function will be called with the following parameters:
+ * - function(aItem, aResults, aFilterProperties, aFilter)
+ * @param aItem The item being tested.
+ * @param aResults The results of the test of the other specified
+ * filter properties.
+ * @param aFilterProperties The current filter properties being tested.
+ * @param aFilter The calFilter object performing the filter test.
+ *
+ * If specified, the callback function is responsible for returning a value that
+ * can be converted to true if the item should match the filter, or a value that
+ * can be converted to false otherwise. The return value will override the results
+ * of the testing of any other specified filter properties.
+ */
+function calFilterProperties() {
+ this.wrappedJSObject = this;
+}
+
+calFilterProperties.prototype = {
+ FILTER_DATE_ALL: 0,
+ FILTER_DATE_VIEW: 1,
+ FILTER_DATE_SELECTED: 2,
+ FILTER_DATE_SELECTED_OR_NOW: 3,
+ FILTER_DATE_NOW: 4,
+ FILTER_DATE_TODAY: 5,
+ FILTER_DATE_CURRENT_WEEK: 6,
+ FILTER_DATE_CURRENT_MONTH: 7,
+ FILTER_DATE_CURRENT_YEAR: 8,
+
+ FILTER_STATUS_INCOMPLETE: 1,
+ FILTER_STATUS_IN_PROGRESS: 2,
+ FILTER_STATUS_COMPLETED_TODAY: 4,
+ FILTER_STATUS_COMPLETED_BEFORE: 8,
+ FILTER_STATUS_ALL: 15,
+
+ FILTER_DUE_PAST: 1,
+ FILTER_DUE_TODAY: 2,
+ FILTER_DUE_FUTURE: 4,
+ FILTER_DUE_NONE: 8,
+ FILTER_DUE_ALL: 15,
+
+ FILTER_OCCURRENCES_BOUND: 0,
+ FILTER_OCCURRENCES_NONE: 1,
+ FILTER_OCCURRENCES_PAST_AND_NEXT: 2,
+
+ start: null,
+ end: null,
+ due: null,
+ status: null,
+ category: null,
+ occurrences: null,
+
+ onfilter: null,
+
+ equals: function(aFilterProps) {
+ if (!(aFilterProps instanceof calFilterProperties)) {
+ return false;
+ }
+ let props = ["start", "end", "due", "status", "category", "occurrences", "onfilter"];
+ return props.every(function(prop) {
+ return (this[prop] == aFilterProps[prop]);
+ }, this);
+ },
+
+ clone: function() {
+ let cloned = new calFilterProperties();
+ let props = ["start", "end", "due", "status", "category", "occurrences", "onfilter"];
+ props.forEach(function(prop) {
+ cloned[prop] = this[prop];
+ }, this);
+
+ return cloned;
+ },
+
+ LOG: function(aString) {
+ cal.LOG("[calFilterProperties] " +
+ (aString || "") +
+ " start=" + this.start +
+ " end=" + this.end +
+ " status=" + this.status +
+ " due=" + this.due +
+ " category=" + this.category);
+ }
+};
+
+/**
+ * Object that allows filtering of a set of items using a set of filter properties. A set
+ * of property filters may be defined by a filter name, which may then be used to apply
+ * the defined filter properties. A set of commonly used property filters are predefined.
+ */
+function calFilter() {
+ this.wrappedJSObject = this;
+ this.mFilterProperties = new calFilterProperties();
+ this.initDefinedFilters();
+ this.mMaxIterations = Preferences.get("calendar.filter.maxiterations", 50);
+}
+
+calFilter.prototype = {
+ mStartDate: null,
+ mEndDate: null,
+ mSelectedDate: null,
+ mFilterText: "",
+ mDefinedFilters: {},
+ mFilterProperties: null,
+ mToday: null,
+ mTomorrow: null,
+ mMaxIterations: 50,
+
+ /**
+ * Initializes the predefined filters.
+ */
+ initDefinedFilters: function() {
+ let filters = ["all", "notstarted", "overdue", "open", "completed", "throughcurrent",
+ "throughtoday", "throughsevendays", "today", "thisCalendarMonth",
+ "future", "current", "currentview"];
+ filters.forEach(function(filter) {
+ if (!(filter in this.mDefinedFilters)) {
+ this.defineFilter(filter, this.getPreDefinedFilterProperties(filter));
+ }
+ }, this);
+ },
+
+ /**
+ * Gets the filter properties for a predefined filter.
+ *
+ * @param aFilter The name of the filter to retrieve the filter properties for.
+ * @result The filter properties for the specified filter, or null if the filter
+ * not predefined.
+ */
+ getPreDefinedFilterProperties: function(aFilter) {
+ let props = new calFilterProperties();
+
+ if (!aFilter) {
+ return props;
+ }
+
+ switch (aFilter) {
+
+ // Predefined Task filters
+ case "notstarted":
+ props.status = props.FILTER_STATUS_INCOMPLETE;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_SELECTED_OR_NOW;
+ break;
+ case "overdue":
+ props.status = props.FILTER_STATUS_INCOMPLETE | props.FILTER_STATUS_IN_PROGRESS;
+ props.due = props.FILTER_DUE_PAST;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_SELECTED_OR_NOW;
+ break;
+ case "open":
+ props.status = props.FILTER_STATUS_INCOMPLETE | props.FILTER_STATUS_IN_PROGRESS;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_ALL;
+ props.occurrences = props.FILTER_OCCURRENCES_PAST_AND_NEXT;
+ break;
+ case "completed":
+ props.status = props.FILTER_STATUS_COMPLETED_TODAY | props.FILTER_STATUS_COMPLETED_BEFORE;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_SELECTED_OR_NOW;
+ break;
+ case "throughcurrent":
+ props.status = props.FILTER_STATUS_INCOMPLETE | props.FILTER_STATUS_IN_PROGRESS |
+ props.FILTER_STATUS_COMPLETED_TODAY;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_SELECTED_OR_NOW;
+ break;
+ case "throughtoday":
+ props.status = props.FILTER_STATUS_INCOMPLETE | props.FILTER_STATUS_IN_PROGRESS |
+ props.FILTER_STATUS_COMPLETED_TODAY;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_TODAY;
+ break;
+ case "throughsevendays":
+ props.status = props.FILTER_STATUS_INCOMPLETE | props.FILTER_STATUS_IN_PROGRESS |
+ props.FILTER_STATUS_COMPLETED_TODAY;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = "P7D";
+ break;
+
+ // Predefined Event filters
+ case "today":
+ props.start = props.FILTER_DATE_TODAY;
+ props.end = props.FILTER_DATE_TODAY;
+ break;
+ case "thisCalendarMonth":
+ props.start = props.FILTER_DATE_CURRENT_MONTH;
+ props.end = props.FILTER_DATE_CURRENT_MONTH;
+ break;
+ case "future":
+ props.start = props.FILTER_DATE_NOW;
+ props.end = props.FILTER_DATE_ALL;
+ break;
+ case "current":
+ props.start = props.FILTER_DATE_SELECTED;
+ props.end = props.FILTER_DATE_SELECTED;
+ break;
+ case "currentview":
+ props.start = props.FILTER_DATE_VIEW;
+ props.end = props.FILTER_DATE_VIEW;
+ break;
+
+ case "all":
+ default:
+ props.status = props.FILTER_STATUS_ALL;
+ props.due = props.FILTER_DUE_ALL;
+ props.start = props.FILTER_DATE_ALL;
+ props.end = props.FILTER_DATE_ALL;
+ }
+
+ return props;
+ },
+
+ /**
+ * Defines a set of filter properties so that they may be applied by the filter name. If
+ * the specified filter name is already defined, it's associated filter properties will be
+ * replaced.
+ *
+ * @param aFilterName The name to define the filter properties as.
+ * @param aFilterProperties The filter properties to define.
+ */
+ defineFilter: function(aFilterName, aFilterProperties) {
+ if (!(aFilterProperties instanceof calFilterProperties)) {
+ return;
+ }
+
+ this.mDefinedFilters[aFilterName] = aFilterProperties;
+ },
+
+ /**
+ * Returns the set of filter properties that were previously defined by a filter name.
+ *
+ * @param aFilter The filter name of the defined filter properties.
+ * @return The properties defined by the filter name, or null if
+ * the filter name was not previously defined.
+ */
+ getDefinedFilterProperties: function(aFilter) {
+ if (aFilter in this.mDefinedFilters) {
+ return this.mDefinedFilters[aFilter].clone();
+ } else {
+ return null;
+ }
+ },
+
+ /**
+ * Returns the filter name that a set of filter properties were previously defined as.
+ *
+ * @param aFilterProperties The filter properties previously defined.
+ * @return The name of the first filter name that the properties
+ * were defined as, or null if the filter properties were
+ * not previously defined.
+ */
+ getDefinedFilterName: function(aFilterProperties) {
+ for (filter in this.mDefinedFilters) {
+ if (this.mDefinedFilters[filter].equals(aFilterProperties)) {
+ return filter;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Checks if the item matches the current filter text
+ *
+ * @param aItem The item to check.
+ * @return Returns true if the item matches the filter text or no
+ * filter text has been set, false otherwise.
+ */
+ textFilter: function(aItem) {
+ if (!this.mFilterText) {
+ return true;
+ }
+
+ let searchText = this.mFilterText.toLowerCase();
+
+ if (!searchText.length || searchText.match(/^\s*$/)) {
+ return true;
+ }
+
+ // TODO: Support specifying which fields to search on
+ for (let field of ["SUMMARY", "DESCRIPTION", "LOCATION", "URL"]) {
+ let val = aItem.getProperty(field);
+ if (val && val.toLowerCase().includes(searchText)) {
+ return true;
+ }
+ }
+
+ return aItem.getCategories({}).some(cat => cat.toLowerCase().includes(searchText));
+ },
+
+ /**
+ * Checks if the item matches the current filter date range.
+ *
+ * @param aItem The item to check.
+ * @return Returns true if the item falls within the date range
+ * specified by mStartDate and mEndDate, false otherwise.
+ */
+ dateRangeFilter: function(aItem) {
+ return checkIfInRange(aItem, this.mStartDate, this.mEndDate);
+ },
+
+ /**
+ * Checks if the item matches the currently applied filter properties. Filter properties
+ * with a value of null or that are not applicable to the item's type are not tested.
+ *
+ * @param aItem The item to check.
+ * @return Returns true if the item matches the filter properties
+ * currently applied, false otherwise.
+ */
+ propertyFilter: function(aItem) {
+ let result;
+ let props = this.mFilterProperties;
+ if (!props) {
+ return false;
+ }
+
+ // the today and tomorrow properties are precalculated in the updateFilterDates function
+ // for better performance when filtering batches of items.
+ let today = this.mToday;
+ if (!today) {
+ today = cal.now();
+ today.isDate = true;
+ }
+
+ let tomorrow = this.mTomorrow;
+ if (!tomorrow) {
+ tomorrow = today.clone();
+ tomorrow.day++;
+ }
+
+ // test the date range of the applied filter.
+ result = this.dateRangeFilter(aItem);
+
+ // test the category property. If the property value is an array, only one category must
+ // match.
+ if (result && props.category) {
+ let cats = [];
+
+ if (typeof props.category == "string") {
+ cats.push(props.category);
+ } else if (Array.isArray(props.category)) {
+ cats = props.category;
+ }
+ result = cats.some(cat => aItem.getCategories({}).includes(cat));
+ }
+
+ // test the status property. Only applies to tasks.
+ if (result && props.status != null && cal.isToDo(aItem)) {
+ let completed = aItem.isCompleted;
+ let current = !aItem.completedDate || today.compare(aItem.completedDate) <= 0;
+ let percent = aItem.percentComplete || 0;
+
+ result = ((props.status & props.FILTER_STATUS_INCOMPLETE) ||
+ !(!completed && (percent == 0))) &&
+ ((props.status & props.FILTER_STATUS_IN_PROGRESS) ||
+ !(!completed && (percent > 0))) &&
+ ((props.status & props.FILTER_STATUS_COMPLETED_TODAY) ||
+ !(completed && current)) &&
+ ((props.status & props.FILTER_STATUS_COMPLETED_BEFORE) ||
+ !(completed && !current));
+ }
+
+ // test the due property. Only applies to tasks.
+ if (result && props.due != null && cal.isToDo(aItem)) {
+ let due = aItem.dueDate;
+ let now = cal.now();
+
+ result = ((props.due & props.FILTER_DUE_PAST) ||
+ !(due && (due.compare(now) < 0))) &&
+ ((props.due & props.FILTER_DUE_TODAY) ||
+ !(due && (due.compare(now) >= 0) && (due.compare(tomorrow) < 0))) &&
+ ((props.due & props.FILTER_DUE_FUTURE) ||
+ !(due && (due.compare(tomorrow) >= 0))) &&
+ ((props.due & props.FILTER_DUE_NONE) ||
+ !(due == null));
+ }
+
+ // Call the filter properties onfilter callback if set. The return value of the
+ // callback function will override the result of this function.
+ if (props.onfilter && typeof props.onfilter == "function") {
+ return props.onfilter(aItem, result, props, this);
+ }
+
+ return result;
+ },
+
+ /**
+ * Calculates the date from a date filter property.
+ *
+ * @param prop The value of the date filter property to calculate for. May
+ * be a constant specifying a relative date range, or a string
+ * representing a duration offset from the current date time.
+ * @param start If true, the function will return the date value for the
+ * start of the relative date range, otherwise it will return the
+ * date value for the end of the date range.
+ * @return The calculated date for the property.
+ */
+ getDateForProperty: function(prop, start) {
+ let props = this.mFilterProperties || new calFilterProperties();
+ let result = null;
+ let selectedDate = this.mSelectedDate || currentView().selectedDay || cal.now();
+ let nowDate = cal.now();
+
+ if (typeof prop == "string") {
+ let duration = cal.createDuration(prop);
+ if (duration) {
+ result = nowDate;
+ result.addDuration(duration);
+ }
+ } else {
+ switch (prop) {
+ case props.FILTER_DATE_ALL:
+ result = null;
+ break;
+ case props.FILTER_DATE_VIEW:
+ result = start ? currentView().startDay.clone()
+ : currentView().endDay.clone();
+ break;
+ case props.FILTER_DATE_SELECTED:
+ result = selectedDate.clone();
+ result.isDate = true;
+ break;
+ case props.FILTER_DATE_SELECTED_OR_NOW: {
+ result = selectedDate.clone();
+ let resultJSDate = cal.dateTimeToJsDate(result);
+ let nowJSDate = cal.dateTimeToJsDate(nowDate);
+ if ((start && resultJSDate > nowJSDate) ||
+ (!start && resultJSDate < nowJSDate)) {
+ result = nowDate;
+ }
+ result.isDate = true;
+ break;
+ }
+ case props.FILTER_DATE_NOW:
+ result = nowDate;
+ break;
+ case props.FILTER_DATE_TODAY:
+ result = nowDate;
+ result.isDate = true;
+ break;
+ case props.FILTER_DATE_CURRENT_WEEK:
+ result = start ? nowDate.startOfWeek : nowDate.endOfWeek;
+ break;
+ case props.FILTER_DATE_CURRENT_MONTH:
+ result = start ? nowDate.startOfMonth : nowDate.endOfMonth;
+ break;
+ case props.FILTER_DATE_CURRENT_YEAR:
+ result = start ? nowDate.startOfYear : nowDate.endOfYear;
+ break;
+ }
+
+ // date ranges are inclusive, so we need to include the day for the end date
+ if (!start && result && prop != props.FILTER_DATE_NOW) {
+ result.day++;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Calculates the current start and end dates for the currently applied filter.
+ *
+ * @return The current [startDate, endDate] for the applied filter.
+ */
+ getDatesForFilter: function() {
+ let startDate = null;
+ let endDate = null;
+
+ if (this.mFilterProperties) {
+ startDate = this.getDateForProperty(this.mFilterProperties.start, true);
+ endDate = this.getDateForProperty(this.mFilterProperties.end, false);
+
+ // swap the start and end dates if necessary
+ if (startDate && endDate && startDate.compare(endDate) > 0) {
+ let swap = startDate;
+ endDate = startDate;
+ startDate = swap;
+ }
+ }
+
+ return [startDate, endDate];
+ },
+
+ /**
+ * Gets the start date for the current filter date range.
+ *
+ * @return: The start date of the current filter date range, or null if
+ * the date range has an unbound start date.
+ */
+ get startDate() {
+ return this.mStartDate;
+ },
+
+ /**
+ * Sets the start date for the current filter date range. This will override the date range
+ * calculated from the filter properties by the getDatesForFilter function.
+ */
+ set startDate(aStartDate) {
+ return (this.mStartDate = aStartDate);
+ },
+
+ /**
+ * Gets the end date for the current filter date range.
+ *
+ * @return: The end date of the current filter date range, or null if
+ * the date range has an unbound end date.
+ */
+ get endDate() {
+ return this.mEndDate;
+ },
+
+ /**
+ * Sets the end date for the current filter date range. This will override the date range
+ * calculated from the filter properties by the getDatesForFilter function.
+ */
+ set endDate(aEndDate) {
+ return (this.mEndDate = aEndDate);
+ },
+
+ /**
+ * Gets the value used to perform the text filter.
+ */
+ get filterText() {
+ return this.mFilterText;
+ },
+
+ /**
+ * Sets the value used to perform the text filter.
+ *
+ * @param aValue The string value to use for the text filter.
+ */
+ set filterText(aValue) {
+ return (this.mFilterText = aValue);
+ },
+
+ /**
+ * Gets the selected date used by the getDatesForFilter function to calculate date ranges
+ * that are relative to the selected date.
+ */
+ get selectedDate() {
+ return this.mSelectedDate;
+ },
+
+ /**
+ * Sets the selected date used by the getDatesForFilter function to calculate date ranges
+ * that are relative to the selected date.
+ */
+ set selectedDate(aSelectedDate) {
+ return (this.mSelectedDate = aSelectedDate);
+ },
+
+ /**
+ * Gets the currently applied filter properties.
+ *
+ * @return The currently applied filter properties.
+ */
+ get filterProperties() {
+ return this.mFilterProperties ? this.mFilterProperties.clone() : null;
+ },
+
+ /**
+ * Gets the name of the currently applied filter.
+ *
+ * @return The current defined name of the currently applied filter
+ * properties, or null if the current properties were not
+ * previously defined.
+ */
+ get filterName() {
+ if (!this.mFilterProperties) {
+ return null;
+ }
+
+ return this.getDefinedFilterName(this.mFilterProperties);
+ },
+
+ /**
+ * Applies the specified filter.
+ *
+ * @param aFilter The filter to apply. May be one of the following types:
+ * - a calFilterProperties object specifying the filter properties
+ * - a String representing a previously defined filter name
+ * - a String representing a duration offset from now
+ * - a Function to use for the onfilter callback for a custom filter
+ */
+ applyFilter: function(aFilter) {
+ this.mFilterProperties = null;
+
+ if (typeof aFilter == "string") {
+ if (aFilter in this.mDefinedFilters) {
+ this.mFilterProperties = this.getDefinedFilterProperties(aFilter);
+ } else {
+ let dur = cal.createDuration(aFilter);
+ if (dur.inSeconds > 0) {
+ this.mFilterProperties = new calFilterProperties();
+ this.mFilterProperties.start = this.mFilterProperties.FILTER_DATE_NOW;
+ this.mFilterProperties.end = aFilter;
+ }
+ }
+ } else if (typeof aFilter == "object" && (aFilter instanceof calFilterProperties)) {
+ this.mFilterProperties = aFilter;
+ } else if (typeof aFilter == "function") {
+ this.mFilterProperties = new calFilterProperties();
+ this.mFilterProperties.onfilter = aFilter;
+ } else {
+ this.mFilterProperties = new calFilterProperties();
+ }
+
+ if (this.mFilterProperties) {
+ this.updateFilterDates();
+ // this.mFilterProperties.LOG("Applying filter:");
+ } else {
+ cal.WARN("[calFilter] Unable to apply filter " + aFilter);
+ }
+ },
+
+ /**
+ * Calculates the current start and end dates for the currently applied filter, and updates
+ * the current filter start and end dates. This function can be used to update the date range
+ * for date range filters that are relative to the selected date or current date and time.
+ *
+ * @return The current [startDate, endDate] for the applied filter.
+ */
+ updateFilterDates: function() {
+ let [startDate, endDate] = this.getDatesForFilter();
+ this.mStartDate = startDate;
+ this.mEndDate = endDate;
+
+ // the today and tomorrow properties are precalculated here
+ // for better performance when filtering batches of items.
+ this.mToday = cal.now();
+ this.mToday.isDate = true;
+
+ this.mTomorrow = this.mToday.clone();
+ this.mTomorrow.day++;
+
+ return [startDate, endDate];
+ },
+
+ /**
+ * Filters an array of items, returning a new array containing the items that match
+ * the currently applied filter properties and text filter.
+ *
+ * @param aItems The array of items to check.
+ * @param aCallback An optional callback function to be called with each item and
+ * the result of it's filter test.
+ * @return A new array containing the items that match the filters, or
+ * null if no filter has been applied.
+ */
+ filterItems: function(aItems, aCallback) {
+ if (!this.mFilterProperties) {
+ return null;
+ }
+
+ return aItems.filter(function(aItem) {
+ let result = this.propertyFilter(aItem) && this.textFilter(aItem);
+
+ if (aCallback && typeof aCallback == "function") {
+ aCallback(aItem, result, this.mFilterProperties, this);
+ }
+
+ return result;
+ }, this);
+ },
+
+ /**
+ * Checks if the item matches the currently applied filter properties and text filter.
+ *
+ * @param aItem The item to check.
+ * @return Returns true if the item matches the filters,
+ * false otherwise.
+ */
+ isItemInFilters: function(aItem) {
+ return this.propertyFilter(aItem) && this.textFilter(aItem);
+ },
+
+ /**
+ * Finds the next occurrence of a repeating item that matches the currently applied
+ * filter properties.
+ *
+ * @param aItem The parent item to find the next occurrence of.
+ * @return Returns the next occurrence that matches the filters,
+ * or null if no match is found.
+ */
+ getNextOccurrence: function(aItem) {
+ if (!aItem.recurrenceInfo) {
+ return this.isItemInFilters(aItem) ? aItem : null;
+ }
+
+ let count = 0;
+ let start = cal.now();
+
+ // If the base item matches the filter, we need to check each future occurrence.
+ // Otherwise, we only need to check the exceptions.
+ if (this.isItemInFilters(aItem)) {
+ while (count++ < this.mMaxIterations) {
+ let next = aItem.recurrenceInfo.getNextOccurrence(start);
+ if (!next) {
+ // there are no more occurrences
+ return null;
+ }
+ if (this.isItemInFilters(next)) {
+ return next;
+ }
+ start = next.startDate || next.entryDate;
+ }
+
+ // we've hit the maximum number of iterations without finding a match
+ cal.WARN("[calFilter] getNextOccurrence: reached maximum iterations for " + aItem.title);
+ return null;
+ } else {
+ // the parent item doesn't match the filter, we can return the first future exception
+ // that matches the filter
+ let exMatch = null;
+ aItem.recurrenceInfo.getExceptionIds({}).forEach(function(rID) {
+ let ex = aItem.recurrenceInfo.getExceptionFor(rID);
+ if (ex && cal.now().compare(ex.startDate || ex.entryDate) < 0 &&
+ this.isItemInFilters(ex)) {
+ exMatch = ex;
+ }
+ }, this);
+ return exMatch;
+ }
+ },
+
+ /**
+ * Gets the occurrences of a repeating item that match the currently applied
+ * filter properties and date range.
+ *
+ * @param aItem The parent item to find occurrence of.
+ * @return Returns an array containing the occurrences that
+ * match the filters, an empty array if there are no
+ * matches, or null if the filter is not initialized.
+ */
+ getOccurrences: function(aItem) {
+ if (!this.mFilterProperties) {
+ return null;
+ }
+ let props = this.mFilterProperties;
+ let occs;
+
+ if (!aItem.recurrenceInfo || (!props.occurrences && !this.mEndDate) ||
+ props.occurrences == props.FILTER_OCCURRENCES_NONE) {
+ // either this isn't a repeating item, the occurrence filter specifies that
+ // we don't want occurrences, or we have a default occurrence filter with an
+ // unbound date range, so we return just the unexpanded item.
+ occs = [aItem];
+ } else {
+ occs = aItem.getOccurrencesBetween(this.mStartDate || cal.createDateTime(),
+ this.mEndDate || cal.now(), {});
+ if ((props.occurrences == props.FILTER_OCCURRENCES_PAST_AND_NEXT) &&
+ !this.mEndDate) {
+ // we have an unbound date range and the occurrence filter specifies
+ // that we also want the next matching occurrence if available.
+ let next = this.getNextOccurrence(aItem);
+ if (next) {
+ occs.push(next);
+ }
+ }
+ }
+
+ return this.filterItems(occs);
+ },
+
+ /**
+ * Gets the items matching the currently applied filter properties from a calendar.
+ * This function is asynchronous, and returns results to a calIOperationListener object.
+ *
+ * @param aCalendar The calendar to get items from.
+ * @param aItemType The type of items to get, as defined by the calICalendar
+ * interface ITEM_FILTER_TYPE_XXX constants.
+ * @param aListener The calIOperationListener object to return results to.
+ * @return the calIOperation handle to track the operation.
+ */
+ getItems: function(aCalendar, aItemType, aListener) {
+ if (!this.mFilterProperties) {
+ return null;
+ }
+ let props = this.mFilterProperties;
+
+ // we use a local proxy listener for the calICalendar.getItems() call, and use it
+ // to handle occurrence expansion and filter the results before forwarding them to
+ // the listener passed in the aListener argument.
+ let self = this;
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: aListener.onOperationComplete.bind(aListener),
+
+ onGetResult: function(aOpCalendar, aStatus, aOpItemType, aDetail, aCount, aItems) {
+ let items;
+ if (props.occurrences == props.FILTER_OCCURRENCES_PAST_AND_NEXT) {
+ // with the FILTER_OCCURRENCES_PAST_AND_NEXT occurrence filter we will
+ // get parent items returned here, so we need to let the getOccurrences
+ // function handle occurrence expansion.
+ items = [];
+ for (let item of aItems) {
+ items = items.concat(self.getOccurrences(item));
+ }
+ } else {
+ // with other occurrence filters the calICalendar.getItems() function will
+ // return expanded occurrences appropriately, we only need to filter them.
+ items = self.filterItems(aItems);
+ }
+
+ aListener.onGetResult(aOpCalendar, aStatus, aOpItemType, aDetail, items.length, items);
+ }
+ };
+
+ // build the filter argument for calICalendar.getItems() from the filter properties
+ let filter = aItemType || aCalendar.FILTER_TYPE_ALL;
+ if (!props.status || (props.status & (props.FILTER_STATUS_COMPLETED_TODAY |
+ props.FILTER_STATUS_COMPLETED_BEFORE))) {
+ filter |= aCalendar.ITEM_FILTER_COMPLETED_YES;
+ }
+ if (!props.status || (props.status & (props.FILTER_STATUS_INCOMPLETE |
+ props.FILTER_STATUS_IN_PROGRESS))) {
+ filter |= aCalendar.ITEM_FILTER_COMPLETED_NO;
+ }
+
+ let startDate = this.startDate;
+ let endDate = this.endDate;
+
+ // we only want occurrences returned from calICalendar.getItems() with a default
+ // occurence filter property and a bound date range, otherwise the local listener
+ // will handle occurrence expansion.
+ if (!props.occurrences && this.endDate) {
+ filter |= aCalendar.ITEM_FILTER_CLASS_OCCURRENCES;
+ startDate = startDate || cal.createDateTime();
+ endDate = endDate || cal.now();
+ }
+
+ return aCalendar.getItems(filter, 0, startDate, endDate, listener);
+ }
+};
diff --git a/calendar/base/src/calFreeBusyService.js b/calendar/base/src/calFreeBusyService.js
new file mode 100644
index 000000000..5e40b01f3
--- /dev/null
+++ b/calendar/base/src/calFreeBusyService.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function calFreeBusyListener(numOperations, finalListener) {
+ this.mFinalListener = finalListener;
+ this.mNumOperations = numOperations;
+
+ this.opGroup = new calOperationGroup(() => {
+ this.notifyResult(null);
+ });
+}
+calFreeBusyListener.prototype = {
+ mFinalListener: null,
+ mNumOperations: 0,
+ opGroup: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIGenericOperationListener]),
+
+ notifyResult: function(result) {
+ let listener = this.mFinalListener;
+ if (listener) {
+ if (!this.opGroup.isPending) {
+ this.mFinalListener = null;
+ }
+ listener.onResult(this.opGroup, result);
+ }
+ },
+
+ // calIGenericOperationListener:
+ onResult: function(aOperation, aResult) {
+ if (this.mFinalListener) {
+ if (!aOperation || !aOperation.isPending) {
+ --this.mNumOperations;
+ if (this.mNumOperations == 0) {
+ this.opGroup.notifyCompleted();
+ }
+ }
+ let opStatus = aOperation ? aOperation.status : Components.results.NS_OK;
+ if (Components.isSuccessCode(opStatus) &&
+ aResult && Array.isArray(aResult)) {
+ this.notifyResult(aResult);
+ } else {
+ this.notifyResult([]);
+ }
+ }
+ }
+};
+
+function calFreeBusyService() {
+ this.wrappedJSObject = this;
+ this.mProviders = new calInterfaceBag(Components.interfaces.calIFreeBusyProvider);
+}
+var calFreeBusyServiceClassID = Components.ID("{29c56cd5-d36e-453a-acde-0083bd4fe6d3}");
+var calFreeBusyServiceInterfaces = [
+ Components.interfaces.calIFreeBusyProvider,
+ Components.interfaces.calIFreeBusyService
+];
+calFreeBusyService.prototype = {
+ mProviders: null,
+
+ classID: calFreeBusyServiceClassID,
+ QueryInterface: XPCOMUtils.generateQI(calFreeBusyServiceInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calFreeBusyServiceClassID,
+ contractID: "@mozilla.org/calendar/freebusy-service;1",
+ classDescription: "Calendar FreeBusy Service",
+ interfaces: calFreeBusyServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ // calIFreeBusyProvider:
+ getFreeBusyIntervals: function(aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) {
+ let groupListener = new calFreeBusyListener(this.mProviders.size, aListener);
+ for (let provider of this.mProviders) {
+ let operation = provider.getFreeBusyIntervals(aCalId, aRangeStart,
+ aRangeEnd,
+ aBusyTypes,
+ groupListener);
+ groupListener.opGroup.add(operation);
+ }
+ return groupListener.opGroup;
+ },
+
+ // calIFreeBusyService:
+ addProvider: function(aProvider) {
+ this.mProviders.add(aProvider);
+ },
+ removeProvider: function(aProvider) {
+ this.mProviders.remove(aProvider);
+ }
+};
diff --git a/calendar/base/src/calIcsParser.js b/calendar/base/src/calIcsParser.js
new file mode 100644
index 000000000..b72aea3b5
--- /dev/null
+++ b/calendar/base/src/calIcsParser.js
@@ -0,0 +1,342 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function calIcsParser() {
+ this.wrappedJSObject = this;
+ this.mItems = [];
+ this.mParentlessItems = [];
+ this.mComponents = [];
+ this.mProperties = [];
+}
+var calIcsParserClassID = Components.ID("{6fe88047-75b6-4874-80e8-5f5800f14984}");
+var calIcsParserInterfaces = [Components.interfaces.calIIcsParser];
+calIcsParser.prototype = {
+ classID: calIcsParserClassID,
+ QueryInterface: XPCOMUtils.generateQI(calIcsParserInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calIcsParserClassID,
+ contractID: "@mozilla.org/calendar/ics-parser;1",
+ classDescription: "Calendar ICS Parser",
+ interfaces: calIcsParserInterfaces,
+ flags: Components.interfaces.nsIClassInfo.THREADSAFE
+ }),
+
+ processIcalComponent: function(rootComp, aAsyncParsing) {
+ let calComp;
+ // libical returns the vcalendar component if there is just one vcalendar.
+ // If there are multiple vcalendars, it returns an xroot component, with
+ // vcalendar children. We need to handle both cases.
+ if (rootComp) {
+ if (rootComp.componentType == "VCALENDAR") {
+ calComp = rootComp;
+ } else {
+ calComp = rootComp.getFirstSubcomponent("VCALENDAR");
+ }
+ }
+
+ if (!calComp) {
+ let message = "Parser Error. Could not find 'VCALENDAR' component.\n";
+ try {
+ // we try to also provide the parsed component - if that fails due to an error in
+ // libical, we append the error message of the caught exception, which includes
+ // already a stack trace.
+ cal.ERROR(message + rootComp + "\n" + cal.STACK(10));
+ } catch (e) {
+ cal.ERROR(message + e);
+ }
+ }
+
+ let self = this;
+ let state = new parserState(this, aAsyncParsing);
+
+ while (calComp) {
+ // Get unknown properties from the VCALENDAR
+ for (let prop of cal.ical.propertyIterator(calComp)) {
+ if (prop.propertyName != "VERSION" && prop.propertyName != "PRODID") {
+ this.mProperties.push(prop);
+ }
+ }
+
+ for (let subComp of cal.ical.subcomponentIterator(calComp)) {
+ state.submit(subComp);
+ }
+ calComp = rootComp.getNextSubcomponent("VCALENDAR");
+ }
+
+ state.join(() => {
+ let fakedParents = {};
+ // tag "exceptions", i.e. items with rid:
+ for (let item of state.excItems) {
+ let parent = state.uid2parent[item.id];
+
+ if (!parent) { // a parentless one, fake a master and override it's occurrence
+ parent = isEvent(item) ? createEvent() : createTodo();
+ parent.id = item.id;
+ parent.setProperty("DTSTART", item.recurrenceId);
+ parent.setProperty("X-MOZ-FAKED-MASTER", "1"); // this tag might be useful in the future
+ parent.recurrenceInfo = cal.createRecurrenceInfo(parent);
+ fakedParents[item.id] = true;
+ state.uid2parent[item.id] = parent;
+ state.items.push(parent);
+ }
+ if (item.id in fakedParents) {
+ let rdate = Components.classes["@mozilla.org/calendar/recurrence-date;1"]
+ .createInstance(Components.interfaces.calIRecurrenceDate);
+ rdate.date = item.recurrenceId;
+ parent.recurrenceInfo.appendRecurrenceItem(rdate);
+ // we'll keep the parentless-API until we switch over using itip-process for import (e.g. in dnd code)
+ self.mParentlessItems.push(item);
+ }
+
+ parent.recurrenceInfo.modifyException(item, true);
+ }
+
+ if (Object.keys(state.tzErrors).length > 0) {
+ // Use an alert rather than a prompt because problems may appear in
+ // remote subscribed calendars the user cannot change.
+ if (Components.classes["@mozilla.org/alerts-service;1"]) {
+ let notifier = Components.classes["@mozilla.org/alerts-service;1"]
+ .getService(Components.interfaces.nsIAlertsService);
+ let title = calGetString("calendar", "TimezoneErrorsAlertTitle");
+ let text = calGetString("calendar", "TimezoneErrorsSeeConsole");
+ try {
+ notifier.showAlertNotification("", title, text, false, null, null, title);
+ } catch (e) {
+ // The notifier may not be available, e.g. on xpcshell tests
+ }
+ }
+ }
+
+ // We are done, push the items to the parser and notify the listener
+ self.mItems = self.mItems.concat(state.items);
+ self.mComponents = self.mComponents.concat(state.extraComponents);
+
+ if (aAsyncParsing) {
+ aAsyncParsing.onParsingComplete(Components.results.NS_OK, self);
+ }
+ });
+ },
+
+ parseString: function(aICSString, aTzProvider, aAsyncParsing) {
+ if (aAsyncParsing) {
+ let self = this;
+
+ // We are using two types of very similar listeners here:
+ // aAsyncParsing is a calIcsParsingListener that returns the ics
+ // parser containing the processed items.
+ // The listener passed to parseICSAsync is a calICsComponentParsingListener
+ // required by the ics service, that receives the parsed root component.
+ cal.getIcsService().parseICSAsync(aICSString, aTzProvider, {
+ onParsingComplete: function(rc, rootComp) {
+ if (Components.isSuccessCode(rc)) {
+ self.processIcalComponent(rootComp, aAsyncParsing);
+ } else {
+ cal.ERROR("Error Parsing ICS: " + rc);
+ aAsyncParsing.onParsingComplete(rc, self);
+ }
+ }
+ });
+ } else {
+ this.processIcalComponent(cal.getIcsService().parseICS(aICSString, aTzProvider));
+ }
+ },
+
+ parseFromStream: function(aStream, aTzProvider, aAsyncParsing) {
+ // Read in the string. Note that it isn't a real string at this point,
+ // because likely, the file is utf8. The multibyte chars show up as multiple
+ // 'chars' in this string. So call it an array of octets for now.
+
+ let octetArray = [];
+ let binaryIS = Components.classes["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Components.interfaces.nsIBinaryInputStream);
+ binaryIS.setInputStream(aStream);
+ octetArray = binaryIS.readByteArray(binaryIS.available());
+
+ // Some other apps (most notably, sunbird 0.2) happily splits an UTF8
+ // character between the octets, and adds a newline and space between them,
+ // for ICS folding. Unfold manually before parsing the file as utf8.This is
+ // UTF8 safe, because octets with the first bit 0 are always one-octet
+ // characters. So the space or the newline never can be part of a multi-byte
+ // char.
+ for (let i = octetArray.length - 2; i >= 0; i--) {
+ if (octetArray[i] == "\n" && octetArray[i + 1] == " ") {
+ octetArray = octetArray.splice(i, 2);
+ }
+ }
+
+ // Interpret the byte-array as a UTF8-string, and convert into a
+ // javascript string.
+ let unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ // ICS files are always UTF8
+ unicodeConverter.charset = "UTF-8";
+ let stringData = unicodeConverter.convertFromByteArray(octetArray, octetArray.length);
+
+ this.parseString(stringData, aTzProvider, aAsyncParsing);
+ },
+
+ getItems: function(aCount) {
+ aCount.value = this.mItems.length;
+ return this.mItems.concat([]);
+ },
+
+ getParentlessItems: function(aCount) {
+ aCount.value = this.mParentlessItems.length;
+ return this.mParentlessItems.concat([]);
+ },
+
+ getProperties: function(aCount) {
+ aCount.value = this.mProperties.length;
+ return this.mProperties.concat([]);
+ },
+
+ getComponents: function(aCount) {
+ aCount.value = this.mComponents.length;
+ return this.mComponents.concat([]);
+ }
+};
+
+/**
+ * The parser state, which helps process ical components without clogging up the
+ * event queue.
+ *
+ * @param aParser The parser that is using this state
+ */
+function parserState(aParser, aListener) {
+ this.parser = aParser;
+ this.listener = aListener;
+
+ this.extraComponents = [];
+ this.items = [];
+ this.uid2parent = {};
+ this.excItems = [];
+ this.tzErrors = {};
+}
+
+parserState.prototype = {
+ parser: null,
+ joinFunc: null,
+ threadCount: 0,
+
+ extraComponents: null,
+ items: null,
+ uid2parent: null,
+ excItems: null,
+ tzErrors: null,
+ listener: null,
+
+ /**
+ * Checks if the timezones are missing and notifies the user via error console
+ *
+ * @param item The item to check for
+ * @param date The datetime object to check with
+ */
+ checkTimezone: function(item, date) {
+ if (date && cal.isPhantomTimezone(date.timezone)) {
+ let tzid = date.timezone.tzid;
+ let hid = item.hashId + "#" + tzid;
+ if (!(hid in this.tzErrors)) {
+ // For now, publish errors to console and alert user.
+ // In future, maybe make them available through an interface method
+ // so this UI code can be removed from the parser, and caller can
+ // choose whether to alert, or show user the problem items and ask
+ // for fixes, or something else.
+ let msgArgs = [tzid, item.title, cal.getDateFormatter().formatDateTime(date)];
+ let msg = calGetString("calendar", "unknownTimezoneInItem", msgArgs);
+
+ cal.ERROR(msg + "\n" + item.icalString);
+ this.tzErrors[hid] = true;
+ }
+ }
+ },
+
+ /**
+ * Submit processing of a subcomponent to the event queue
+ *
+ * @param subComp The component to process
+ */
+ submit: function(subComp) {
+ let self = this;
+ let runner = {
+ run: function() {
+ let item = null;
+ switch (subComp.componentType) {
+ case "VEVENT":
+ item = cal.createEvent();
+ item.icalComponent = subComp;
+ self.checkTimezone(item, item.startDate);
+ self.checkTimezone(item, item.endDate);
+ break;
+ case "VTODO":
+ item = cal.createTodo();
+ item.icalComponent = subComp;
+ self.checkTimezone(item, item.entryDate);
+ self.checkTimezone(item, item.dueDate);
+ // completed is defined to be in UTC
+ break;
+ case "VTIMEZONE":
+ // this should already be attached to the relevant
+ // events in the calendar, so there's no need to
+ // do anything with it here.
+ break;
+ default:
+ self.extraComponents.push(subComp);
+ break;
+ }
+
+ if (item) {
+ let rid = item.recurrenceId;
+ if (rid) {
+ self.excItems.push(item);
+ } else {
+ self.items.push(item);
+ if (item.recurrenceInfo) {
+ self.uid2parent[item.id] = item;
+ }
+ }
+ }
+ self.threadCount--;
+ self.checkCompletion();
+ }
+ };
+
+ this.threadCount++;
+ if (this.listener) {
+ // If we have a listener, we are doing this asynchronously. Go ahead
+ // and use the thread manager to dispatch the above runner
+ Services.tm.currentThread.dispatch(runner, Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
+ } else {
+ // No listener means synchonous. Just run the runner instead
+ runner.run();
+ }
+ },
+
+ /**
+ * Checks if the processing of all events has completed. If a join function
+ * has been set, this function is called.
+ *
+ * @return True, if all tasks have been completed
+ */
+ checkCompletion: function() {
+ if (this.joinFunc && this.threadCount == 0) {
+ this.joinFunc();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Sets a join function that is called when all tasks have been completed
+ *
+ * @param joinFunc The join function to call
+ */
+ join: function(joinFunc) {
+ this.joinFunc = joinFunc;
+ this.checkCompletion();
+ }
+};
diff --git a/calendar/base/src/calIcsSerializer.js b/calendar/base/src/calIcsSerializer.js
new file mode 100644
index 000000000..e645c4e7e
--- /dev/null
+++ b/calendar/base/src/calIcsSerializer.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+
+function calIcsSerializer() {
+ this.wrappedJSObject = this;
+ this.mItems = [];
+ this.mProperties = [];
+ this.mComponents = [];
+}
+var calIcsSerializerClassID = Components.ID("{207a6682-8ff1-4203-9160-729ec28c8766}");
+var calIcsSerializerInterfaces = [Components.interfaces.calIIcsSerializer];
+calIcsSerializer.prototype = {
+ classID: calIcsSerializerClassID,
+ QueryInterface: XPCOMUtils.generateQI(calIcsSerializerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calIcsSerializerClassID,
+ contractID: "@mozilla.org/calendar/ics-serializer;1",
+ classDescription: "Calendar ICS Serializer",
+ interfaces: calIcsSerializerInterfaces,
+ }),
+
+ addItems: function(aItems, aCount) {
+ if (aCount > 0) {
+ this.mItems = this.mItems.concat(aItems);
+ }
+ },
+
+ addProperty: function(aProperty) {
+ this.mProperties.push(aProperty);
+ },
+
+ addComponent: function(aComponent) {
+ this.mComponents.push(aComponent);
+ },
+
+ serializeToString: function() {
+ let calComp = this.getIcalComponent();
+ return calComp.serializeToICS();
+ },
+
+ serializeToInputStream: function(aStream) {
+ let calComp = this.getIcalComponent();
+ return calComp.serializeToICSStream();
+ },
+
+ serializeToStream: function(aStream) {
+ let str = this.serializeToString();
+
+ // Convert the javascript string to an array of bytes, using the
+ // UTF8 encoder
+ let convStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ convStream.init(aStream, "UTF-8", 0, 0x0000);
+
+ convStream.writeString(str);
+ convStream.close();
+ },
+
+ getIcalComponent: function() {
+ let calComp = getIcsService().createIcalComponent("VCALENDAR");
+ calSetProdidVersion(calComp);
+
+ // xxx todo: think about that the below code doesn't clone the properties/components,
+ // thus ownership is moved to returned VCALENDAR...
+
+ for (let prop of this.mProperties) {
+ calComp.addProperty(prop);
+ }
+ for (let comp of this.mComponents) {
+ calComp.addSubcomponent(comp);
+ }
+
+ for (let item of cal.itemIterator(this.mItems)) {
+ calComp.addSubcomponent(item.icalComponent);
+ }
+
+ return calComp;
+ }
+};
diff --git a/calendar/base/src/calInternalInterfaces.idl b/calendar/base/src/calInternalInterfaces.idl
new file mode 100644
index 000000000..2170f3a58
--- /dev/null
+++ b/calendar/base/src/calInternalInterfaces.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** Don't use these if you're not the calendar glue code! **/
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+[scriptable, uuid(1903648f-a0ee-4ae1-84b0-d8e8d0b10506)]
+interface calIInternalShallowCopy : nsISupports
+{
+ /**
+ * create a proxy for this item; the returned item
+ * proxy will have parentItem set to this instance.
+ *
+ * @param aRecurrenceId RECURRENCE-ID of the proxy to be created
+ */
+ calIItemBase createProxy(in calIDateTime aRecurrenceId);
+
+ // used by recurrenceInfo when cloning proxy objects to
+ // avoid an infinite loop. aNewParent is optional, and is
+ // used to set the parent of the new item; it should be null
+ // if no new parent is passed in.
+ calIItemBase cloneShallow(in calIItemBase aNewParent);
+};
diff --git a/calendar/base/src/calItemBase.js b/calendar/base/src/calItemBase.js
new file mode 100644
index 000000000..bd113ca29
--- /dev/null
+++ b/calendar/base/src/calItemBase.js
@@ -0,0 +1,1135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * calItemBase prototype definition
+ *
+ * @implements calIItemBase
+ * @constructor
+ */
+function calItemBase() {
+ cal.ASSERT(false, "Inheriting objects call initItemBase()!");
+}
+
+calItemBase.prototype = {
+ mProperties: null,
+ mPropertyParams: null,
+
+ mIsProxy: false,
+ mHashId: null,
+ mImmutable: false,
+ mDirty: false,
+ mCalendar: null,
+ mParentItem: null,
+ mRecurrenceInfo: null,
+ mOrganizer: null,
+
+ mAlarms: null,
+ mAlarmLastAck: null,
+
+ mAttendees: null,
+ mAttachments: null,
+ mRelations: null,
+ mCategories: null,
+
+ mACLEntry: null,
+
+ /**
+ * Initialize the base item's attributes. Can be called from inheriting
+ * objects in their constructor.
+ */
+ initItemBase: function() {
+ this.wrappedJSObject = this;
+ this.mProperties = new calPropertyBag();
+ this.mPropertyParams = {};
+ this.mProperties.setProperty("CREATED", cal.jsDateToDateTime(new Date()));
+ },
+
+ /**
+ * @see nsISupports
+ */
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIItemBase]),
+
+ /**
+ * @see calIItemBase
+ */
+ get aclEntry() {
+ let aclEntry = this.mACLEntry;
+ let aclManager = this.calendar && this.calendar.superCalendar.aclManager;
+
+ if (!aclEntry && aclManager) {
+ this.mACLEntry = aclManager.getItemEntry(this);
+ aclEntry = this.mACLEntry;
+ }
+
+ if (!aclEntry && this.parentItem != this) {
+ // No ACL entry on this item, check the parent
+ aclEntry = this.parentItem.aclEntry;
+ }
+
+ return aclEntry;
+ },
+
+ // readonly attribute AUTF8String hashId;
+ get hashId() {
+ if (this.mHashId === null) {
+ let rid = this.recurrenceId;
+ let calendar = this.calendar;
+ // some unused delim character:
+ this.mHashId = [encodeURIComponent(this.id),
+ rid ? rid.getInTimezone(UTC()).icalString : "",
+ calendar ? encodeURIComponent(calendar.id) : ""].join("#");
+ }
+ return this.mHashId;
+ },
+
+ // attribute AUTF8String id;
+ get id() {
+ return this.getProperty("UID");
+ },
+ set id(uid) {
+ this.mHashId = null; // recompute hashId
+ this.setProperty("UID", uid);
+ if (this.mRecurrenceInfo) {
+ this.mRecurrenceInfo.onIdChange(uid);
+ }
+ return uid;
+ },
+
+ // attribute calIDateTime recurrenceId;
+ get recurrenceId() {
+ return this.getProperty("RECURRENCE-ID");
+ },
+ set recurrenceId(rid) {
+ this.mHashId = null; // recompute hashId
+ return this.setProperty("RECURRENCE-ID", rid);
+ },
+
+ // attribute calIRecurrenceInfo recurrenceInfo;
+ get recurrenceInfo() {
+ return this.mRecurrenceInfo;
+ },
+ set recurrenceInfo(value) {
+ this.modify();
+ return (this.mRecurrenceInfo = calTryWrappedJSObject(value));
+ },
+
+ // attribute calIItemBase parentItem;
+ get parentItem() {
+ return this.mParentItem || this;
+ },
+ set parentItem(value) {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ return (this.mParentItem = calTryWrappedJSObject(value));
+ },
+
+ /**
+ * Initializes the base item to be an item proxy. Used by inheriting
+ * objects createProxy() method.
+ *
+ * XXXdbo Explain proxy a bit better, either here or in
+ * calIInternalShallowCopy.
+ *
+ * @see calIInternalShallowCopy
+ * @param aParentItem The parent item to initialize the proxy on.
+ * @param aRecurrenceId The recurrence id to initialize the proxy for.
+ */
+ initializeProxy: function(aParentItem, aRecurrenceId) {
+ this.mIsProxy = true;
+
+ aParentItem = calTryWrappedJSObject(aParentItem);
+ this.mParentItem = aParentItem;
+ this.mCalendar = aParentItem.mCalendar;
+ this.recurrenceId = aRecurrenceId;
+
+ // Make sure organizer is unset, as the getter checks for this.
+ this.mOrganizer = undefined;
+
+ this.mImmutable = aParentItem.mImmutable;
+ },
+
+ // readonly attribute boolean isMutable;
+ get isMutable() { return !this.mImmutable; },
+
+ /**
+ * This function should be called by all members that modify the item. It
+ * checks if the item is immutable and throws accordingly, and sets the
+ * mDirty property.
+ */
+ modify: function() {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ this.mDirty = true;
+ },
+
+ /**
+ * Makes sure the item is not dirty. If the item is dirty, properties like
+ * LAST-MODIFIED and DTSTAMP are set to now.
+ */
+ ensureNotDirty: function() {
+ if (this.mDirty) {
+ let now = cal.jsDateToDateTime(new Date());
+ this.setProperty("LAST-MODIFIED", now);
+ this.setProperty("DTSTAMP", now);
+ this.mDirty = false;
+ }
+ },
+
+ /**
+ * Makes all properties of the base item immutable. Can be called by
+ * inheriting objects' makeImmutable method.
+ */
+ makeItemBaseImmutable: function() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ // make all our components immutable
+ if (this.mRecurrenceInfo) {
+ this.mRecurrenceInfo.makeImmutable();
+ }
+
+ if (this.mOrganizer) {
+ this.mOrganizer.makeImmutable();
+ }
+ if (this.mAttendees) {
+ for (let att of this.mAttendees) {
+ att.makeImmutable();
+ }
+ }
+
+ for (let [, propValue] of this.mProperties) {
+ if (propValue instanceof Components.interfaces.calIDateTime &&
+ propValue.isMutable) {
+ propValue.makeImmutable();
+ }
+ }
+
+ if (this.mAlarms) {
+ for (let alarm of this.mAlarms) {
+ alarm.makeImmutable();
+ }
+ }
+
+ if (this.mAlarmLastAck) {
+ this.mAlarmLastAck.makeImmutable();
+ }
+
+ this.ensureNotDirty();
+ this.mImmutable = true;
+ },
+
+ // boolean hasSameIds(in calIItemBase aItem);
+ hasSameIds: function(that) {
+ return that && this.id == that.id &&
+ (this.recurrenceId == that.recurrenceId || // both null
+ (this.recurrenceId && that.recurrenceId &&
+ this.recurrenceId.compare(that.recurrenceId) == 0));
+ },
+
+ // calIItemBase clone();
+ clone: function() {
+ return this.cloneShallow(this.mParentItem);
+ },
+
+ /**
+ * Clones the base item's properties into the passed object, potentially
+ * setting a new parent item.
+ *
+ * @param m The item to clone this item into
+ * @param aNewParent (optional) The new parent item to set on m.
+ */
+ cloneItemBaseInto: function(cloned, aNewParent) {
+ cloned.mImmutable = false;
+ cloned.mACLEntry = this.mACLEntry;
+ cloned.mIsProxy = this.mIsProxy;
+ cloned.mParentItem = calTryWrappedJSObject(aNewParent) || this.mParentItem;
+ cloned.mHashId = this.mHashId;
+ cloned.mCalendar = this.mCalendar;
+ if (this.mRecurrenceInfo) {
+ cloned.mRecurrenceInfo = calTryWrappedJSObject(this.mRecurrenceInfo.clone());
+ cloned.mRecurrenceInfo.item = cloned;
+ }
+
+ let org = this.organizer;
+ if (org) {
+ org = org.clone();
+ }
+ cloned.mOrganizer = org;
+
+ cloned.mAttendees = [];
+ for (let att of this.getAttendees({})) {
+ cloned.mAttendees.push(att.clone());
+ }
+
+ cloned.mProperties = new calPropertyBag();
+ for (let [name, value] of this.mProperties) {
+ if (value instanceof Components.interfaces.calIDateTime) {
+ value = value.clone();
+ }
+
+ cloned.mProperties.setProperty(name, value);
+
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ let newBucket = {};
+ for (let param in propBucket) {
+ newBucket[param] = propBucket[param];
+ }
+ cloned.mPropertyParams[name] = newBucket;
+ }
+ }
+
+ cloned.mAttachments = [];
+ for (let att of this.getAttachments({})) {
+ cloned.mAttachments.push(att.clone());
+ }
+
+ cloned.mRelations = [];
+ for (let rel of this.getRelations({})) {
+ cloned.mRelations.push(rel.clone());
+ }
+
+ cloned.mCategories = this.getCategories({});
+
+ cloned.mAlarms = [];
+ for (let alarm of this.getAlarms({})) {
+ // Clone alarms into new item, assume the alarms from the old item
+ // are valid and don't need validation.
+ cloned.mAlarms.push(alarm.clone());
+ }
+
+ let alarmLastAck = this.alarmLastAck;
+ if (alarmLastAck) {
+ alarmLastAck = alarmLastAck.clone();
+ }
+ cloned.mAlarmLastAck = alarmLastAck;
+
+ cloned.mDirty = this.mDirty;
+
+ return cloned;
+ },
+
+ // attribute calIDateTime alarmLastAck;
+ get alarmLastAck() {
+ return this.mAlarmLastAck;
+ },
+ set alarmLastAck(aValue) {
+ this.modify();
+ if (aValue && !aValue.timezone.isUTC) {
+ aValue = aValue.getInTimezone(UTC());
+ }
+ return (this.mAlarmLastAck = aValue);
+ },
+
+ // readonly attribute calIDateTime lastModifiedTime;
+ get lastModifiedTime() {
+ this.ensureNotDirty();
+ return this.getProperty("LAST-MODIFIED");
+ },
+
+ // readonly attribute calIDateTime stampTime;
+ get stampTime() {
+ this.ensureNotDirty();
+ return this.getProperty("DTSTAMP");
+ },
+
+ // readonly attribute nsISimpleEnumerator propertyEnumerator;
+ get propertyEnumerator() {
+ if (this.mIsProxy) {
+ cal.ASSERT(this.parentItem != this);
+ return { // nsISimpleEnumerator:
+ mProxyEnum: this.mProperties.enumerator,
+ mParentEnum: this.mParentItem.propertyEnumerator,
+ mHandledProps: { },
+ mCurrentProp: null,
+
+ hasMoreElements: function() {
+ if (this.mCurrentProp) {
+ return true;
+ }
+ if (this.mProxyEnum) {
+ while (this.mProxyEnum.hasMoreElements()) {
+ let prop = this.mProxyEnum.getNext();
+ this.mHandledProps[prop.name] = true;
+ if (prop.value !== null) {
+ this.mCurrentProp = prop;
+ return true;
+ } // else skip the deleted properties
+ }
+ this.mProxyEnum = null;
+ }
+ while (this.mParentEnum.hasMoreElements()) {
+ let prop = this.mParentEnum.getNext();
+ if (!this.mHandledProps[prop.name]) {
+ this.mCurrentProp = prop;
+ return true;
+ }
+ }
+ return false;
+ },
+
+ getNext: function() {
+ if (!this.hasMoreElements()) { // hasMoreElements is called by intention to skip yet deleted properties
+ cal.ASSERT(false, Components.results.NS_ERROR_UNEXPECTED);
+ throw Components.results.NS_ERROR_UNEXPECTED;
+ }
+ let ret = this.mCurrentProp;
+ this.mCurrentProp = null;
+ return ret;
+ }
+ };
+ } else {
+ return this.mProperties.enumerator;
+ }
+ },
+
+ // nsIVariant getProperty(in AString name);
+ getProperty: function(aName) {
+ aName = aName.toUpperCase();
+ let aValue = this.mProperties.getProperty_(aName);
+ if (aValue === undefined) {
+ aValue = (this.mIsProxy ? this.mParentItem.getProperty(aName) : null);
+ }
+ return aValue;
+ },
+
+ // boolean hasProperty(in AString name);
+ hasProperty: function(aName) {
+ return (this.getProperty(aName.toUpperCase()) != null);
+ },
+
+ // void setProperty(in AString name, in nsIVariant value);
+ setProperty: function(aName, aValue) {
+ this.modify();
+ aName = aName.toUpperCase();
+ if (aValue || !isNaN(parseInt(aValue, 10))) {
+ this.mProperties.setProperty(aName, aValue);
+ } else {
+ this.deleteProperty(aName);
+ }
+ if (aName == "LAST-MODIFIED") {
+ // setting LAST-MODIFIED cleans/undirties the item, we use this for preserving DTSTAMP
+ this.mDirty = false;
+ }
+ },
+
+ // void deleteProperty(in AString name);
+ deleteProperty: function(aName) {
+ this.modify();
+ aName = aName.toUpperCase();
+ if (this.mIsProxy) {
+ // deleting a proxy's property will mark the bag's item as null, so we could
+ // distinguish it when enumerating/getting properties from the undefined ones.
+ this.mProperties.setProperty(aName, null);
+ } else {
+ this.mProperties.deleteProperty(aName);
+ }
+ delete this.mPropertyParams[aName];
+ },
+
+ // AString getPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName);
+ getPropertyParameter: function(aPropName, aParamName) {
+ let propName = aPropName.toUpperCase();
+ let paramName = aParamName.toUpperCase();
+ if (propName in this.mPropertyParams && paramName in this.mPropertyParams[propName]) {
+ // If the property is not in mPropertyParams, then this just means
+ // there are no properties set.
+ return this.mPropertyParams[propName][paramName];
+ }
+ return null;
+ },
+
+ // boolean hasPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName);
+ hasPropertyParameter: function(aPropName, aParamName) {
+ let propName = aPropName.toUpperCase();
+ let paramName = aParamName.toUpperCase();
+ return (propName in this.mPropertyParams) &&
+ (paramName in this.mPropertyParams[propName]);
+ },
+
+ // void setPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName,
+ // in AUTF8String aParameterValue);
+ setPropertyParameter: function(aPropName, aParamName, aParamValue) {
+ let propName = aPropName.toUpperCase();
+ let paramName = aParamName.toUpperCase();
+ this.modify();
+ if (!(propName in this.mPropertyParams)) {
+ if (this.hasProperty(propName)) {
+ this.mPropertyParams[propName] = {};
+ } else {
+ throw "Property " + aPropName + " not set";
+ }
+ }
+ if (aParamValue || !isNaN(parseInt(aParamValue, 10))) {
+ this.mPropertyParams[propName][paramName] = aParamValue;
+ } else {
+ delete this.mPropertyParams[propName][paramName];
+ }
+ return aParamValue;
+ },
+
+ // nsISimpleEnumerator getParameterEnumerator(in AString aPropertyName);
+ getParameterEnumerator: function(aPropName) {
+ let propName = aPropName.toUpperCase();
+ if (!(propName in this.mPropertyParams)) {
+ throw "Property " + aPropName + " not set";
+ }
+ let parameters = this.mPropertyParams[propName];
+ return { // nsISimpleEnumerator
+ mParamNames: Object.keys(parameters),
+ hasMoreElements: function() {
+ return (this.mParamNames.length > 0);
+ },
+
+ getNext: function() {
+ let paramName = this.mParamNames.pop();
+ return { // nsIProperty
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProperty]),
+ name: paramName,
+ value: parameters[paramName]
+ };
+ }
+ };
+ },
+
+ // void getAttendees(out PRUint32 count,
+ // [array,size_is(count),retval] out calIAttendee attendees);
+ getAttendees: function(countObj) {
+ if (!this.mAttendees && this.mIsProxy) {
+ this.mAttendees = this.mParentItem.getAttendees(countObj);
+ }
+ if (this.mAttendees) {
+ countObj.value = this.mAttendees.length;
+ return this.mAttendees.concat([]); // clone
+ } else {
+ countObj.value = 0;
+ return [];
+ }
+ },
+
+ // calIAttendee getAttendeeById(in AUTF8String id);
+ getAttendeeById: function(id) {
+ let attendees = this.getAttendees({});
+ let lowerCaseId = id.toLowerCase();
+ for (let attendee of attendees) {
+ // This match must be case insensitive to deal with differing
+ // cases of things like MAILTO:
+ if (attendee.id.toLowerCase() == lowerCaseId) {
+ return attendee;
+ }
+ }
+ return null;
+ },
+
+ // void removeAttendee(in calIAttendee attendee);
+ removeAttendee: function(attendee) {
+ this.modify();
+ let found = false, newAttendees = [];
+ let attendees = this.getAttendees({});
+ let attIdLowerCase = attendee.id.toLowerCase();
+
+ for (let i = 0; i < attendees.length; i++) {
+ if (attendees[i].id.toLowerCase() == attIdLowerCase) {
+ found = true;
+ } else {
+ newAttendees.push(attendees[i]);
+ }
+ }
+ if (found) {
+ this.mAttendees = newAttendees;
+ }
+ },
+
+ // void removeAllAttendees();
+ removeAllAttendees: function() {
+ this.modify();
+ this.mAttendees = [];
+ },
+
+ // void addAttendee(in calIAttendee attendee);
+ addAttendee: function(attendee) {
+ // the duplicate check is migration code for bug 1204255
+ let exists = this.getAttendeeById(attendee.id);
+ if (exists) {
+ cal.LOG("Ignoring attendee duplicate for item " + this.id +
+ " (" + this.title + "): " + exists.id);
+ if (exists.participationStatus == "NEEDS-ACTION" ||
+ attendee.participationStatus == "DECLINED") {
+ this.removeAttendee(exists);
+ } else {
+ attendee = null;
+ }
+ }
+ if (attendee) {
+ if (attendee.commonName) {
+ // migration code for bug 1209399 to remove leading/training double quotes in
+ let commonName = attendee.commonName.replace(/^["]*([^"]*)["]*$/, "$1");
+ if (commonName.length == 0) {
+ commonName = null;
+ }
+ if (commonName != attendee.commonName) {
+ if (attendee.isMutable) {
+ attendee.commonName = commonName;
+ } else {
+ cal.LOG("Failed to cleanup malformed commonName for immutable attendee " +
+ attendee.toString() + "\n" + cal.STACK(20));
+ }
+ }
+ }
+ this.modify();
+ this.mAttendees = this.getAttendees({});
+ this.mAttendees.push(attendee);
+ }
+ },
+
+ // void getAttachments(out PRUint32 count,
+ // [array,size_is(count),retval] out calIAttachment attachments);
+ getAttachments: function(aCount) {
+ if (!this.mAttachments && this.mIsProxy) {
+ this.mAttachments = this.mParentItem.getAttachments(aCount);
+ }
+ if (this.mAttachments) {
+ aCount.value = this.mAttachments.length;
+ return this.mAttachments.concat([]); // clone
+ } else {
+ aCount.value = 0;
+ return [];
+ }
+ },
+
+ // void removeAttachment(in calIAttachment attachment);
+ removeAttachment: function(aAttachment) {
+ this.modify();
+ for (let attIndex in this.mAttachments) {
+ if (cal.compareObjects(this.mAttachments[attIndex], aAttachment, Components.interfaces.calIAttachment)) {
+ this.modify();
+ this.mAttachments.splice(attIndex, 1);
+ break;
+ }
+ }
+ },
+
+ // void addAttachment(in calIAttachment attachment);
+ addAttachment: function(attachment) {
+ this.modify();
+ this.mAttachments = this.getAttachments({});
+ if (!this.mAttachments.some(x => x.hashId == attachment.hashId)) {
+ this.mAttachments.push(attachment);
+ }
+ },
+
+ // void removeAllAttachments();
+ removeAllAttachments: function() {
+ this.modify();
+ this.mAttachments = [];
+ },
+
+ // void getRelations(out PRUint32 count,
+ // [array,size_is(count),retval] out calIRelation relations);
+ getRelations: function(aCount) {
+ if (!this.mRelations && this.mIsProxy) {
+ this.mRelations = this.mParentItem.getRelations(aCount);
+ }
+ if (this.mRelations) {
+ aCount.value = this.mRelations.length;
+ return this.mRelations.concat([]);
+ } else {
+ aCount.value = 0;
+ return [];
+ }
+ },
+
+ // void removeRelation(in calIRelation relation);
+ removeRelation: function(aRelation) {
+ this.modify();
+ for (let attIndex in this.mRelations) {
+ // Could we have the same item as parent and as child ?
+ if (this.mRelations[attIndex].relId == aRelation.relId &&
+ this.mRelations[attIndex].relType == aRelation.relType) {
+ this.modify();
+ this.mRelations.splice(attIndex, 1);
+ break;
+ }
+ }
+ },
+
+ // void addRelation(in calIRelation relation);
+ addRelation: function(aRelation) {
+ this.modify();
+ this.mRelations = this.getRelations({});
+ this.mRelations.push(aRelation);
+ // XXX ensure that the relation isn't already there?
+ },
+
+ // void removeAllRelations();
+ removeAllRelations: function() {
+ this.modify();
+ this.mRelations = [];
+ },
+
+ // attribute calICalendar calendar;
+ get calendar() {
+ if (!this.mCalendar && (this.parentItem != this)) {
+ return this.parentItem.calendar;
+ } else {
+ return this.mCalendar;
+ }
+ },
+ set calendar(calendar) {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ this.mHashId = null; // recompute hashId
+ this.mCalendar = calendar;
+ },
+
+ // attribute calIAttendee organizer;
+ get organizer() {
+ if (this.mIsProxy && (this.mOrganizer === undefined)) {
+ return this.mParentItem.organizer;
+ } else {
+ return this.mOrganizer;
+ }
+ },
+ set organizer(organizer) {
+ this.modify();
+ this.mOrganizer = organizer;
+ },
+
+ // void getCategories(out PRUint32 aCount,
+ // [array, size_is(aCount), retval] out wstring aCategories);
+ getCategories: function(aCount) {
+ if (!this.mCategories && this.mIsProxy) {
+ this.mCategories = this.mParentItem.getCategories(aCount);
+ }
+ if (this.mCategories) {
+ aCount.value = this.mCategories.length;
+ return this.mCategories.concat([]); // clone
+ } else {
+ aCount.value = 0;
+ return [];
+ }
+ },
+
+ // void setCategories(in PRUint32 aCount,
+ // [array, size_is(aCount)] in wstring aCategories);
+ setCategories: function(aCount, aCategories) {
+ this.modify();
+ this.mCategories = aCategories.concat([]);
+ },
+
+ // attribute AUTF8String icalString;
+ get icalString() {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ set icalString(str) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * The map of promoted properties is a list of those properties that are
+ * represented directly by getters/setters.
+ * All of these property names must be in upper case isPropertyPromoted to
+ * function correctly. The has/get/set/deleteProperty interfaces
+ * are case-insensitive, but these are not.
+ */
+ itemBasePromotedProps: {
+ "CREATED": true,
+ "UID": true,
+ "LAST-MODIFIED": true,
+ "SUMMARY": true,
+ "PRIORITY": true,
+ "STATUS": true,
+ "DTSTAMP": true,
+ "RRULE": true,
+ "EXDATE": true,
+ "RDATE": true,
+ "ATTENDEE": true,
+ "ATTACH": true,
+ "CATEGORIES": true,
+ "ORGANIZER": true,
+ "RECURRENCE-ID": true,
+ "X-MOZ-LASTACK": true,
+ "RELATED-TO": true
+ },
+
+ /**
+ * A map of properties that need translation between the ical component
+ * property and their ICS counterpart.
+ */
+ icsBasePropMap: [
+ { cal: "CREATED", ics: "createdTime" },
+ { cal: "LAST-MODIFIED", ics: "lastModified" },
+ { cal: "DTSTAMP", ics: "stampTime" },
+ { cal: "UID", ics: "uid" },
+ { cal: "SUMMARY", ics: "summary" },
+ { cal: "PRIORITY", ics: "priority" },
+ { cal: "STATUS", ics: "status" },
+ { cal: "RECURRENCE-ID", ics: "recurrenceId" }
+ ],
+
+ /**
+ * Walks through the propmap and sets all properties on this item from the
+ * given icalcomp.
+ *
+ * @param icalcomp The calIIcalComponent to read from.
+ * @param propmap The property map to walk through.
+ */
+ mapPropsFromICS: function(icalcomp, propmap) {
+ for (let i = 0; i < propmap.length; i++) {
+ let prop = propmap[i];
+ let val = icalcomp[prop.ics];
+ if (val != null && val != Components.interfaces.calIIcalComponent.INVALID_VALUE) {
+ this.setProperty(prop.cal, val);
+ }
+ }
+ },
+
+ /**
+ * Walks through the propmap and sets all properties on the given icalcomp
+ * from the properties set on this item.
+ * given icalcomp.
+ *
+ * @param icalcomp The calIIcalComponent to write to.
+ * @param propmap The property map to walk through.
+ */
+ mapPropsToICS: function(icalcomp, propmap) {
+ for (let i = 0; i < propmap.length; i++) {
+ let prop = propmap[i];
+ let val = this.getProperty(prop.cal);
+ if (val != null && val != Components.interfaces.calIIcalComponent.INVALID_VALUE) {
+ icalcomp[prop.ics] = val;
+ }
+ }
+ },
+
+
+ /**
+ * Reads an ical component and sets up the base item's properties to match
+ * it.
+ *
+ * @param icalcomp The ical component to read.
+ */
+ setItemBaseFromICS: function(icalcomp) {
+ this.modify();
+
+ // re-initializing from scratch -- no light proxy anymore:
+ this.mIsProxy = false;
+ this.mProperties = new calPropertyBag();
+ this.mPropertyParams = {};
+
+ this.mapPropsFromICS(icalcomp, this.icsBasePropMap);
+
+ this.mAttendees = []; // don't inherit anything from parent
+ for (let attprop of cal.ical.propertyIterator(icalcomp, "ATTENDEE")) {
+ let att = new calAttendee();
+ att.icalProperty = attprop;
+ this.addAttendee(att);
+ }
+
+ this.mAttachments = []; // don't inherit anything from parent
+ for (let attprop of cal.ical.propertyIterator(icalcomp, "ATTACH")) {
+ let att = new calAttachment();
+ att.icalProperty = attprop;
+ this.addAttachment(att);
+ }
+
+ this.mRelations = []; // don't inherit anything from parent
+ for (let relprop of cal.ical.propertyIterator(icalcomp, "RELATED-TO")) {
+ let rel = new calRelation();
+ rel.icalProperty = relprop;
+ this.addRelation(rel);
+ }
+
+ let org = null;
+ let orgprop = icalcomp.getFirstProperty("ORGANIZER");
+ if (orgprop) {
+ org = new calAttendee();
+ org.icalProperty = orgprop;
+ org.isOrganizer = true;
+ }
+ this.mOrganizer = org;
+
+ this.mCategories = [];
+ for (let catprop of cal.ical.propertyIterator(icalcomp, "CATEGORIES")) {
+ this.mCategories.push(catprop.value);
+ }
+
+ // find recurrence properties
+ let rec = null;
+ if (!this.recurrenceId) {
+ for (let recprop of cal.ical.propertyIterator(icalcomp)) {
+ let ritem = null;
+ switch (recprop.propertyName) {
+ case "RRULE":
+ case "EXRULE":
+ ritem = cal.createRecurrenceRule();
+ break;
+ case "RDATE":
+ case "EXDATE":
+ ritem = cal.createRecurrenceDate();
+ break;
+ default:
+ continue;
+ }
+ ritem.icalProperty = recprop;
+
+ if (!rec) {
+ rec = cal.createRecurrenceInfo(this);
+ }
+ rec.appendRecurrenceItem(ritem);
+ }
+ }
+ this.mRecurrenceInfo = rec;
+
+ this.mAlarms = []; // don't inherit anything from parent
+ for (let alarmComp of cal.ical.subcomponentIterator(icalcomp, "VALARM")) {
+ let alarm = cal.createAlarm();
+ try {
+ alarm.icalComponent = alarmComp;
+ this.addAlarm(alarm, true);
+ } catch (e) {
+ cal.ERROR("Invalid alarm for item: " +
+ this.id + " (" +
+ alarmComp.serializeToICS() + ")" +
+ " exception: " + e);
+ }
+ }
+
+ let lastAck = icalcomp.getFirstProperty("X-MOZ-LASTACK");
+ this.mAlarmLastAck = null;
+ if (lastAck) {
+ this.mAlarmLastAck = cal.createDateTime(lastAck.value);
+ }
+
+ this.mDirty = false;
+ },
+
+ /**
+ * Import all properties not in the promoted map into this item's extended
+ * properties bag.
+ *
+ * @param icalcomp The ical component to read.
+ * @param promoted The map of promoted properties.
+ */
+ importUnpromotedProperties: function(icalcomp, promoted) {
+ for (let prop of cal.ical.propertyIterator(icalcomp)) {
+ let propName = prop.propertyName;
+ if (!promoted[propName]) {
+ this.setProperty(propName, prop.value);
+ for (let [paramName, paramValue] of cal.ical.paramIterator(prop)) {
+ if (!(propName in this.mPropertyParams)) {
+ this.mPropertyParams[propName] = {};
+ }
+ this.mPropertyParams[propName][paramName] = paramValue;
+ }
+ }
+ }
+ },
+
+ // boolean isPropertyPromoted(in AString name);
+ isPropertyPromoted: function(name) {
+ return this.itemBasePromotedProps[name.toUpperCase()];
+ },
+
+ // attribute calIIcalComponent icalComponent;
+ get icalComponent() {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ set icalComponent(val) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // attribute PRUint32 generation;
+ get generation() {
+ let gen = this.getProperty("X-MOZ-GENERATION");
+ return (gen ? parseInt(gen, 10) : 0);
+ },
+ set generation(aValue) {
+ return this.setProperty("X-MOZ-GENERATION", String(aValue));
+ },
+
+ /**
+ * Fills the passed ical component with the base item's properties.
+ *
+ * @param icalcomp The ical component to write to.
+ */
+ fillIcalComponentFromBase: function(icalcomp) {
+ this.ensureNotDirty();
+ let icssvc = cal.getIcsService();
+
+ this.mapPropsToICS(icalcomp, this.icsBasePropMap);
+
+ let org = this.organizer;
+ if (org) {
+ icalcomp.addProperty(org.icalProperty);
+ }
+
+ for (let attendee of this.getAttendees({})) {
+ icalcomp.addProperty(attendee.icalProperty);
+ }
+
+ for (let attachment of this.getAttachments({})) {
+ icalcomp.addProperty(attachment.icalProperty);
+ }
+
+ for (let relation of this.getRelations({})) {
+ icalcomp.addProperty(relation.icalProperty);
+ }
+
+ if (this.mRecurrenceInfo) {
+ for (let ritem of this.mRecurrenceInfo.getRecurrenceItems({})) {
+ icalcomp.addProperty(ritem.icalProperty);
+ }
+ }
+
+ for (let cat of this.getCategories({})) {
+ let catprop = icssvc.createIcalProperty("CATEGORIES");
+ catprop.value = cat;
+ icalcomp.addProperty(catprop);
+ }
+
+ if (this.mAlarms) {
+ for (let alarm of this.mAlarms) {
+ icalcomp.addSubcomponent(alarm.icalComponent);
+ }
+ }
+
+ let alarmLastAck = this.alarmLastAck;
+ if (alarmLastAck) {
+ let lastAck = cal.getIcsService().createIcalProperty("X-MOZ-LASTACK");
+ // - should we further ensure that those are UTC or rely on calAlarmService doing so?
+ lastAck.value = alarmLastAck.icalString;
+ icalcomp.addProperty(lastAck);
+ }
+ },
+
+ // void getAlarms(out PRUint32 count, [array, size_is(count), retval] out calIAlarm aAlarms);
+ getAlarms: function(aCount) {
+ if (typeof aCount != "object") {
+ throw Components.results.NS_ERROR_XPC_NEED_OUT_OBJECT;
+ }
+
+ if (!this.mAlarms && this.mIsProxy) {
+ this.mAlarms = this.mParentItem.getAlarms(aCount);
+ }
+ if (this.mAlarms) {
+ aCount.value = this.mAlarms.length;
+ return this.mAlarms.concat([]); // clone
+ } else {
+ aCount.value = 0;
+ return [];
+ }
+ },
+
+ /**
+ * Adds an alarm. The second parameter is for internal use only, i.e not
+ * provided on the interface.
+ *
+ * @see calIItemBase
+ * @param aDoNotValidate Don't serialize the component to check for
+ * errors.
+ */
+ addAlarm: function(aAlarm, aDoNotValidate) {
+ if (!aDoNotValidate) {
+ try {
+ // Trigger the icalComponent getter to make sure the alarm is valid.
+ aAlarm.icalComponent; // eslint-disable-line no-unused-expressions
+ } catch (e) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ this.modify();
+ this.mAlarms = this.getAlarms({});
+ this.mAlarms.push(aAlarm);
+ },
+
+ // void deleteAlarm(in calIAlarm aAlarm);
+ deleteAlarm: function(aAlarm) {
+ this.modify();
+ this.mAlarms = this.getAlarms({});
+ for (let i = 0; i < this.mAlarms.length; i++) {
+ if (cal.compareObjects(this.mAlarms[i], aAlarm, Components.interfaces.calIAlarm)) {
+ this.mAlarms.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ // void clearAlarms();
+ clearAlarms: function() {
+ this.modify();
+ this.mAlarms = [];
+ },
+
+ // void getOccurrencesBetween (in calIDateTime aStartDate, in calIDateTime aEndDate,
+ // out PRUint32 aCount,
+ // [array,size_is(aCount),retval] out calIItemBase aOccurrences);
+ getOccurrencesBetween: function(aStartDate, aEndDate, aCount) {
+ if (this.recurrenceInfo) {
+ return this.recurrenceInfo.getOccurrences(aStartDate, aEndDate, 0, aCount);
+ }
+
+ if (checkIfInRange(this, aStartDate, aEndDate)) {
+ aCount.value = 1;
+ return [this];
+ }
+
+ aCount.value = 0;
+ return [];
+ }
+};
+
+makeMemberAttr(calItemBase, "CREATED", null, "creationDate", true);
+makeMemberAttr(calItemBase, "SUMMARY", null, "title", true);
+makeMemberAttr(calItemBase, "PRIORITY", 0, "priority", true);
+makeMemberAttr(calItemBase, "CLASS", "PUBLIC", "privacy", true);
+makeMemberAttr(calItemBase, "STATUS", null, "status", true);
+makeMemberAttr(calItemBase, "ALARMTIME", null, "alarmTime", true);
+
+makeMemberAttr(calItemBase, "mProperties", null, "properties");
+
+/**
+ * Helper function to add a member attribute on the given prototype
+ *
+ * @param ctor The constructor function of the prototype
+ * @param varname The local variable name to get/set, or the property in
+ * case asProperty is true.
+ * @param dflt The default value in case none is set
+ * @param attr The attribute name to be used
+ * @param asProperty If true, getProperty will be used to get/set the
+ * member.
+ */
+function makeMemberAttr(ctor, varname, dflt, attr, asProperty) {
+ // XXX handle defaults!
+ let getter = function() {
+ if (asProperty) {
+ return this.getProperty(varname);
+ } else {
+ return (varname in this ? this[varname] : undefined);
+ }
+ };
+ let setter = function(value) {
+ this.modify();
+ if (asProperty) {
+ return this.setProperty(varname, value);
+ } else {
+ return (this[varname] = value);
+ }
+ };
+ ctor.prototype.__defineGetter__(attr, getter);
+ ctor.prototype.__defineSetter__(attr, setter);
+}
diff --git a/calendar/base/src/calItemModule.js b/calendar/base/src/calItemModule.js
new file mode 100644
index 000000000..d78905eb7
--- /dev/null
+++ b/calendar/base/src/calItemModule.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+var scriptLoadOrder = [
+ "calItemBase.js",
+ "calUtils.js",
+ "calCachedCalendar.js",
+
+ "calAlarm.js",
+ "calAlarmService.js",
+ "calAlarmMonitor.js",
+ "calAttendee.js",
+ "calAttachment.js",
+ "calCalendarManager.js",
+ "calCalendarSearchService.js",
+ "calDateTimeFormatter.js",
+ "calDeletedItems.js",
+ "calEvent.js",
+ "calFreeBusyService.js",
+ "calIcsParser.js",
+ "calIcsSerializer.js",
+ "calItipItem.js",
+ "calProtocolHandler.js",
+ "calRecurrenceDate.js",
+ "calRecurrenceInfo.js",
+ "calRelation.js",
+ "calStartupService.js",
+ "calTransactionManager.js",
+ "calTodo.js",
+ "calWeekInfoService.js"
+];
+
+function getComponents() {
+ Components.classes["@mozilla.org/calendar/backend-loader;1"].getService();
+
+ return [
+ calAlarm,
+ calAlarmService,
+ calAlarmMonitor,
+ calAttendee,
+ calAttachment,
+ calCalendarManager,
+ calCalendarSearchService,
+ calDateTimeFormatter,
+ calDeletedItems,
+ calEvent,
+ calFreeBusyService,
+ calIcsParser,
+ calIcsSerializer,
+ calItipItem,
+ calProtocolHandlerWebcal,
+ calProtocolHandlerWebcals,
+ calRecurrenceDate,
+ calRecurrenceInfo,
+ calRelation,
+ calStartupService,
+ calTransaction,
+ calTransactionManager,
+ calTodo,
+ calWeekInfoService,
+ ];
+}
+
+this.NSGetFactory = cal.loadingNSGetFactory(scriptLoadOrder, getComponents, this);
diff --git a/calendar/base/src/calItemModule.manifest b/calendar/base/src/calItemModule.manifest
new file mode 100644
index 000000000..4826e4ed0
--- /dev/null
+++ b/calendar/base/src/calItemModule.manifest
@@ -0,0 +1,75 @@
+component {b8db7c7f-c168-4e11-becb-f26c1c4f5f8f} calItemModule.js
+contract @mozilla.org/calendar/alarm;1 {b8db7c7f-c168-4e11-becb-f26c1c4f5f8f}
+
+component {7a9200dd-6a64-4fff-a798-c5802186e2cc} calItemModule.js
+contract @mozilla.org/calendar/alarm-service;1 {7a9200dd-6a64-4fff-a798-c5802186e2cc}
+
+component {4b7ae030-ed79-11d9-8cd6-0800200c9a66} calItemModule.js
+contract @mozilla.org/calendar/alarm-monitor;1 {4b7ae030-ed79-11d9-8cd6-0800200c9a66}
+category alarm-service-startup calendar-alarm-monitor service,@mozilla.org/calendar/alarm-monitor;1
+category alarm-service-shutdown calendar-alarm-monitor service,@mozilla.org/calendar/alarm-monitor;1
+
+component {5c8dcaa3-170c-4a73-8142-d531156f664d} calItemModule.js
+contract @mozilla.org/calendar/attendee;1 {5c8dcaa3-170c-4a73-8142-d531156f664d}
+
+component {5f76b352-ab75-4c2b-82c9-9206dbbf8571} calItemModule.js
+contract @mozilla.org/calendar/attachment;1 {5f76b352-ab75-4c2b-82c9-9206dbbf8571}
+
+component {f42585e7-e736-4600-985d-9624c1c51992} calItemModule.js
+contract @mozilla.org/calendar/manager;1 {f42585e7-e736-4600-985d-9624c1c51992}
+
+component {f5f743cd-8997-428e-bc1b-644e73f61203} calItemModule.js
+contract @mozilla.org/calendar/calendarsearch-service;1 {f5f743cd-8997-428e-bc1b-644e73f61203}
+
+component {4123da9a-f047-42da-a7d0-cc4175b9f36a} calItemModule.js
+contract @mozilla.org/calendar/datetime-formatter;1 {4123da9a-f047-42da-a7d0-cc4175b9f36a}
+
+component {8e6799af-e7e9-4e6c-9a82-a2413e86d8c3} calItemModule.js
+contract @mozilla.org/calendar/deleted-items-manager;1 {8e6799af-e7e9-4e6c-9a82-a2413e86d8c3}
+category profile-after-change deleted-items-manager @mozilla.org/calendar/deleted-items-manager;1
+
+component {974339d5-ab86-4491-aaaf-2b2ca177c12b} calItemModule.js
+contract @mozilla.org/calendar/event;1 {974339d5-ab86-4491-aaaf-2b2ca177c12b}
+
+component {29c56cd5-d36e-453a-acde-0083bd4fe6d3} calItemModule.js
+contract @mozilla.org/calendar/freebusy-service;1 {29c56cd5-d36e-453a-acde-0083bd4fe6d3}
+
+component {6fe88047-75b6-4874-80e8-5f5800f14984} calItemModule.js
+contract @mozilla.org/calendar/ics-parser;1 {6fe88047-75b6-4874-80e8-5f5800f14984}
+
+component {207a6682-8ff1-4203-9160-729ec28c8766} calItemModule.js
+contract @mozilla.org/calendar/ics-serializer;1 {207a6682-8ff1-4203-9160-729ec28c8766}
+
+component {f41392ab-dcad-4bad-818f-b3d1631c4d93} calItemModule.js
+contract @mozilla.org/calendar/itip-item;1 {f41392ab-dcad-4bad-818f-b3d1631c4d93}
+
+component {1153c73a-39be-46aa-9ba9-656d188865ca} calItemModule.js
+contract @mozilla.org/network/protocol;1?name=webcal {1153c73a-39be-46aa-9ba9-656d188865ca}
+
+component {bdf71224-365d-4493-856a-a7e74026f766} calItemModule.js
+contract @mozilla.org/network/protocol;1?name=webcals {bdf71224-365d-4493-856a-a7e74026f766}
+
+component {806b6423-3aaa-4b26-afa3-de60563e9cec} calItemModule.js
+contract @mozilla.org/calendar/recurrence-date;1 {806b6423-3aaa-4b26-afa3-de60563e9cec}
+
+component {04027036-5884-4a30-b4af-f2cad79f6edf} calItemModule.js
+contract @mozilla.org/calendar/recurrence-info;1 {04027036-5884-4a30-b4af-f2cad79f6edf}
+
+component {76810fae-abad-4019-917a-08e95d5bbd68} calItemModule.js
+contract @mozilla.org/calendar/relation;1 {76810fae-abad-4019-917a-08e95d5bbd68}
+
+component {2547331f-34c0-4a4b-b93c-b503538ba6d6} calItemModule.js
+contract @mozilla.org/calendar/startup-service;1 {2547331f-34c0-4a4b-b93c-b503538ba6d6}
+category profile-after-change calendar-startup-service @mozilla.org/calendar/startup-service;1
+
+component {fcb54c82-2fb9-42cb-bf44-1e197a55e520} calItemModule.js
+contract @mozilla.org/calendar/transaction;1 {fcb54c82-2fb9-42cb-bf44-1e197a55e520}
+
+component {40a1ccf4-5f54-4815-b842-abf06f84dbfd} calItemModule.js
+contract @mozilla.org/calendar/transactionmanager;1 {40a1ccf4-5f54-4815-b842-abf06f84dbfd}
+
+component {7af51168-6abe-4a31-984d-6f8a3989212d} calItemModule.js
+contract @mozilla.org/calendar/todo;1 {7af51168-6abe-4a31-984d-6f8a3989212d}
+
+component {6877bbdd-f336-46f5-98ce-fe86d0285cc1} calItemModule.js
+contract @mozilla.org/calendar/weekinfo-service;1 {6877bbdd-f336-46f5-98ce-fe86d0285cc1}
diff --git a/calendar/base/src/calItipItem.js b/calendar/base/src/calItipItem.js
new file mode 100644
index 000000000..1a25f2a4c
--- /dev/null
+++ b/calendar/base/src/calItipItem.js
@@ -0,0 +1,215 @@
+/* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Constructor of calItipItem object
+ */
+function calItipItem() {
+ this.wrappedJSObject = this;
+ this.mCurrentItemIndex = 0;
+}
+var calItipItemClassID = Components.ID("{f41392ab-dcad-4bad-818f-b3d1631c4d93}");
+var calItipItemInterfaces = [Components.interfaces.calIItipItem];
+calItipItem.prototype = {
+ mIsInitialized: false,
+
+ classID: calItipItemClassID,
+ QueryInterface: XPCOMUtils.generateQI(calItipItemInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calItipItemClassID,
+ contractID: "@mozilla.org/calendar/itip-item;1",
+ classDescription: "Calendar iTIP item",
+ interfaces: calItipItemInterfaces
+ }),
+
+ mSender: null,
+ get sender() {
+ return this.mSender;
+ },
+ set sender(aValue) {
+ return (this.mSender = aValue);
+ },
+
+ mIsSend: false,
+ get isSend() {
+ return this.mIsSend;
+ },
+ set isSend(aValue) {
+ return (this.mIsSend = aValue);
+ },
+
+ mReceivedMethod: "REQUEST",
+ get receivedMethod() {
+ return this.mReceivedMethod;
+ },
+ set receivedMethod(aMethod) {
+ return (this.mReceivedMethod = aMethod.toUpperCase());
+ },
+
+ mResponseMethod: "REPLY",
+ get responseMethod() {
+ if (!this.mIsInitialized) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ return this.mResponseMethod;
+ },
+ set responseMethod(aMethod) {
+ return (this.mResponseMethod = aMethod.toUpperCase());
+ },
+
+ mAutoResponse: null,
+ get autoResponse() {
+ return this.mAutoResponse;
+ },
+ set autoResponse(aValue) {
+ return (this.mAutoResponse = aValue);
+ },
+
+ mTargetCalendar: null,
+ get targetCalendar() {
+ return this.mTargetCalendar;
+ },
+ set targetCalendar(aValue) {
+ return (this.mTargetCalendar = aValue);
+ },
+
+ mIdentity: null,
+ get identity() {
+ return this.mIdentity;
+ },
+ set identity(aValue) {
+ return (this.mIdentity = aValue);
+ },
+
+ mLocalStatus: null,
+ get localStatus() {
+ return this.mLocalStatus;
+ },
+ set localStatus(aValue) {
+ return (this.mLocalStatus = aValue);
+ },
+
+ mItemList: {},
+
+ init: function(aIcalString) {
+ let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ parser.parseString(aIcalString, null);
+
+ // - User specific alarms as well as X-MOZ- properties are irrelevant w.r.t. iTIP messages,
+ // should not be sent out and should not be relevant for incoming messages
+ // - faked master items
+ // so clean them out:
+
+ function cleanItem(item) {
+ // the following changes will bump LAST-MODIFIED/DTSTAMP, we want to preserve the originals:
+ let stamp = item.stampTime;
+ let lastModified = item.lastModifiedTime;
+ item.clearAlarms();
+ item.alarmLastAck = null;
+ item.deleteProperty("RECEIVED-SEQUENCE");
+ item.deleteProperty("RECEIVED-DTSTAMP");
+ let propEnum = item.propertyEnumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ let pname = prop.name;
+ if (pname != "X-MOZ-FAKED-MASTER" && pname.substr(0, "X-MOZ-".length) == "X-MOZ-") {
+ item.deleteProperty(prop.name);
+ }
+ }
+ // never publish an organizer's RECEIVED params:
+ item.getAttendees({}).forEach((att) => {
+ att.deleteProperty("RECEIVED-SEQUENCE");
+ att.deleteProperty("RECEIVED-DTSTAMP");
+ });
+ item.setProperty("DTSTAMP", stamp);
+ item.setProperty("LAST-MODIFIED", lastModified); // need to be last to undirty the item
+ }
+
+ this.mItemList = [];
+ for (let item of cal.itemIterator(parser.getItems({}))) {
+ cleanItem(item);
+ // only push non-faked master items or
+ // the overridden instances of faked master items
+ // to the list:
+ if (item == item.parentItem) {
+ if (!item.hasProperty("X-MOZ-FAKED-MASTER")) {
+ this.mItemList.push(item);
+ }
+ } else if (item.parentItem.hasProperty("X-MOZ-FAKED-MASTER")) {
+ this.mItemList.push(item);
+ }
+ }
+
+ // We set both methods now for safety's sake. It's the ItipProcessor's
+ // responsibility to properly ascertain what the correct response
+ // method is (using user feedback, prefs, etc.) for the given
+ // receivedMethod. The RFC tells us to treat items without a METHOD
+ // as if they were METHOD:REQUEST.
+ for (let prop of parser.getProperties({})) {
+ if (prop.propertyName == "METHOD") {
+ this.mReceivedMethod = prop.value;
+ this.mResponseMethod = prop.value;
+ break;
+ }
+ }
+
+ this.mIsInitialized = true;
+ },
+
+ clone: function() {
+ let newItem = new calItipItem();
+ newItem.mItemList = this.mItemList.map(item => item.clone());
+ newItem.mReceivedMethod = this.mReceivedMethod;
+ newItem.mResponseMethod = this.mResponseMethod;
+ newItem.mAutoResponse = this.mAutoResponse;
+ newItem.mTargetCalendar = this.mTargetCalendar;
+ newItem.mIdentity = this.mIdentity;
+ newItem.mLocalStatus = this.mLocalStatus;
+ newItem.mSender = this.mSender;
+ newItem.mIsSend = this.mIsSend;
+ newItem.mIsInitialized = this.mIsInitialized;
+ return newItem;
+ },
+
+ /**
+ * This returns both the array and the number of items. An easy way to
+ * call it is: let itemArray = itipItem.getItemList({ });
+ */
+ getItemList: function(itemCountRef) {
+ if (!this.mIsInitialized) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ itemCountRef.value = this.mItemList.length;
+ return this.mItemList;
+ },
+
+ /**
+ * Note that this code forces the user to respond to all items in the same
+ * way, which is a current limitation of the spec.
+ */
+ setAttendeeStatus: function(aAttendeeId, aStatus) {
+ // Append "mailto:" to the attendee if it is missing it.
+ if (!aAttendeeId.match(/^mailto:/i)) {
+ aAttendeeId = "mailto:" + aAttendeeId;
+ }
+
+ for (let item of this.mItemList) {
+ let attendee = item.getAttendeeById(aAttendeeId);
+ if (attendee) {
+ // Replies should not have the RSVP property.
+ // XXX BUG 351589: workaround for updating an attendee
+ item.removeAttendee(attendee);
+ attendee = attendee.clone();
+ attendee.rsvp = null;
+ item.addAttendee(attendee);
+ }
+ }
+ }
+};
diff --git a/calendar/base/src/calProtocolHandler.js b/calendar/base/src/calProtocolHandler.js
new file mode 100644
index 000000000..95a16af4a
--- /dev/null
+++ b/calendar/base/src/calProtocolHandler.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/** Constructor for webcal: protocol handler */
+function calProtocolHandlerWebcal() {
+ calProtocolHandler.call(this, "webcal");
+}
+
+/** Constructor for webcals: protocl handler */
+function calProtocolHandlerWebcals() {
+ calProtocolHandler.call(this, "webcals");
+}
+
+/**
+ * Generic webcal constructor
+ *
+ * @param scheme The scheme to init for (webcal, webcals)
+ */
+function calProtocolHandler(scheme) {
+ this.scheme = scheme;
+ this.mHttpProtocol = Services.io.getProtocolHandler(this.scheme == "webcal" ? "http" : "https");
+ this.wrappedJSObject = this;
+}
+
+calProtocolHandler.prototype = {
+ get defaultPort() { return this.mHttpProtocol.defaultPort; },
+ get protocolFlags() { return this.mHttpProtocol.protocolFlags; },
+
+ newURI: function(aSpec, anOriginalCharset, aBaseURI) {
+ let uri = Components.classes["@mozilla.org/network/standard-url;1"]
+ .createInstance(Components.interfaces.nsIStandardURL);
+ uri.init(Components.interfaces.nsIStandardURL.URLTYPE_STANDARD,
+ this.mHttpProtocol.defaultPort, aSpec, anOriginalCharset, aBaseURI);
+ return uri;
+ },
+
+ newChannel: function(aUri) {
+ return this.newChannel2(aUri, null);
+ },
+
+ newChannel2: function(aUri, aLoadInfo) {
+ // make sure to clone the uri, because we are about to change
+ // it, and we don't want to change the original uri.
+ let uri = aUri.clone();
+ uri.scheme = this.mHttpProtocol.scheme;
+
+ let channel;
+ if (aLoadInfo) {
+ channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+ } else {
+ channel = Services.io.newChannelFromURI2(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_NORMAL,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+ }
+ channel.originalURI = aUri;
+ return channel;
+ },
+
+ // We are not overriding any special ports
+ allowPort: function(aPort, aScheme) { return false; }
+};
+
+var calProtocolHandlerWebcalClassID = Components.ID("{1153c73a-39be-46aa-9ba9-656d188865ca}");
+var calProtocolHandlerWebcalInterfaces = [Components.interfaces.nsIProtocolHandler];
+calProtocolHandlerWebcal.prototype = {
+ __proto__: calProtocolHandler.prototype,
+ classID: calProtocolHandlerWebcalClassID,
+ QueryInterface: XPCOMUtils.generateQI(calProtocolHandlerWebcalInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calProtocolHandlerWebcalClassID,
+ contractID: "@mozilla.org/network/protocol;1?name=webcal",
+ classDescription: "Calendar webcal protocal handler",
+ interfaces: calProtocolHandlerWebcalInterfaces
+ }),
+};
+
+var calProtocolHandlerWebcalsClassID = Components.ID("{bdf71224-365d-4493-856a-a7e74026f766}");
+var calProtocolHandlerWebcalsInterfaces = [Components.interfaces.nsIProtocolHandler];
+calProtocolHandlerWebcals.prototype = {
+ __proto__: calProtocolHandler.prototype,
+ classID: calProtocolHandlerWebcalsClassID,
+ QueryInterface: XPCOMUtils.generateQI(calProtocolHandlerWebcalsInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calProtocolHandlerWebcalsClassID,
+ contractID: "@mozilla.org/network/protocol;1?name=webcals",
+ classDescription: "Calendar webcals protocal handler",
+ interfaces: calProtocolHandlerWebcalsInterfaces
+ }),
+};
diff --git a/calendar/base/src/calRecurrenceDate.js b/calendar/base/src/calRecurrenceDate.js
new file mode 100644
index 000000000..62532c513
--- /dev/null
+++ b/calendar/base/src/calRecurrenceDate.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+function calRecurrenceDate() {
+ this.wrappedJSObject = this;
+}
+
+var calRecurrenceDateClassID = Components.ID("{806b6423-3aaa-4b26-afa3-de60563e9cec}");
+var calRecurrenceDateInterfaces = [Components.interfaces.calIRecurrenceDate];
+calRecurrenceDate.prototype = {
+ isMutable: true,
+
+ mIsNegative: false,
+ mDate: null,
+
+ classID: calRecurrenceDateClassID,
+ QueryInterface: XPCOMUtils.generateQI(calRecurrenceDateInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calRecurrenceDateClassID,
+ contractID: "@mozilla.org/calendar/recurrence-date;1",
+ classDescription: "The date of an occurrence of a recurring item",
+ interfaces: calRecurrenceDateInterfaces
+ }),
+ makeImmutable: function() {
+ this.isMutable = false;
+ },
+
+ ensureMutable: function() {
+ if (!this.isMutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_MUTABLE;
+ }
+ },
+
+ clone: function() {
+ let other = new calRecurrenceDate();
+ other.mDate = (this.mDate ? this.mDate.clone() : null);
+ other.mIsNegative = this.mIsNegative;
+ return other;
+ },
+
+ get isNegative() { return this.mIsNegative; },
+ set isNegative(val) {
+ this.ensureMutable();
+ return (this.mIsNegative = val);
+ },
+
+ get isFinite() { return true; },
+
+ get date() { return this.mDate; },
+ set date(val) {
+ this.ensureMutable();
+ return (this.mDate = val);
+ },
+
+ getNextOccurrence: function(aStartTime, aOccurrenceTime) {
+ if (this.mDate && this.mDate.compare(aStartTime) > 0) {
+ return this.mDate;
+ } else {
+ return null;
+ }
+ },
+
+ getOccurrences: function(aStartTime, aRangeStart, aRangeEnd, aMaxCount, aCount) {
+ if (this.mDate &&
+ this.mDate.compare(aRangeStart) >= 0 &&
+ (!aRangeEnd || this.mDate.compare(aRangeEnd) < 0)) {
+ aCount.value = 1;
+ return [this.mDate];
+ } else {
+ aCount.value = 0;
+ return [];
+ }
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return (comp ? comp.icalString : "");
+ },
+ set icalString(val) {
+ let prop = cal.getIcsService().createIcalPropertyFromString(val);
+ let propName = prop.propertyName;
+ if (propName != "RDATE" && propName != "EXDATE") {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ this.icalProperty = prop;
+ return val;
+ },
+
+ get icalProperty() {
+ let prop = cal.getIcsService().createIcalProperty(this.mIsNegative ? "EXDATE" : "RDATE");
+ prop.valueAsDatetime = this.mDate;
+ return prop;
+ },
+ set icalProperty(prop) {
+ if (prop.propertyName == "RDATE") {
+ this.mIsNegative = false;
+ if (prop.getParameter("VALUE") == "PERIOD") {
+ let period = Components.classes["@mozilla.org/calendar/period;1"]
+ .createInstance(Components.interfaces.calIPeriod);
+ period.icalString = prop.valueAsIcalString;
+ this.mDate = period.start;
+ } else {
+ this.mDate = prop.valueAsDatetime;
+ }
+ } else if (prop.propertyName == "EXDATE") {
+ this.mIsNegative = true;
+ this.mDate = prop.valueAsDatetime;
+ }
+ return prop;
+ }
+};
diff --git a/calendar/base/src/calRecurrenceInfo.js b/calendar/base/src/calRecurrenceInfo.js
new file mode 100644
index 000000000..e09818501
--- /dev/null
+++ b/calendar/base/src/calRecurrenceInfo.js
@@ -0,0 +1,807 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function getRidKey(date) {
+ if (!date) {
+ return null;
+ }
+ let timezone = date.timezone;
+ if (!timezone.isUTC && !timezone.isFloating) {
+ date = date.getInTimezone(UTC());
+ }
+ return date.icalString;
+}
+
+function calRecurrenceInfo() {
+ this.mRecurrenceItems = [];
+ this.mExceptionMap = {};
+
+ this.wrappedJSObject = this;
+}
+
+var calRecurrenceInfoClassID = Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}");
+var calRecurrenceInfoInterfaces = [Components.interfaces.calIRecurrenceInfo];
+calRecurrenceInfo.prototype = {
+ mImmutable: false,
+ mBaseItem: null,
+ mRecurrenceItems: null,
+ mPositiveRules: null,
+ mNegativeRules: null,
+ mExceptionMap: null,
+
+ classID: calRecurrenceInfoClassID,
+ QueryInterface: XPCOMUtils.generateQI(calRecurrenceInfoInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calRecurrenceInfoClassID,
+ contractID: "@mozilla.org/calendar/recurrence-info;1",
+ classDescription: "Calendar Recurrence Info",
+ interfaces: calRecurrenceInfoInterfaces,
+ }),
+
+ /**
+ * Helpers
+ */
+ ensureBaseItem: function() {
+ if (!this.mBaseItem) {
+ throw Components.results.NS_ERROR_NOT_INITIALIZED;
+ }
+ },
+ ensureMutable: function() {
+ if (this.mImmutable) {
+ throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
+ }
+ },
+ ensureSortedRecurrenceRules: function() {
+ if (!this.mPositiveRules || !this.mNegativeRules) {
+ this.mPositiveRules = [];
+ this.mNegativeRules = [];
+ for (let ritem of this.mRecurrenceItems) {
+ if (ritem.isNegative) {
+ this.mNegativeRules.push(ritem);
+ } else {
+ this.mPositiveRules.push(ritem);
+ }
+ }
+ }
+ },
+
+ /**
+ * Mutability bits
+ */
+ get isMutable() {
+ return !this.mImmutable;
+ },
+ makeImmutable: function() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ for (let ritem of this.mRecurrenceItems) {
+ if (ritem.isMutable) {
+ ritem.makeImmutable();
+ }
+ }
+
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ if (item.isMutable) {
+ item.makeImmutable();
+ }
+ }
+
+ this.mImmutable = true;
+ },
+
+ clone: function() {
+ let cloned = new calRecurrenceInfo();
+ cloned.mBaseItem = this.mBaseItem;
+
+ let clonedItems = [];
+ for (let ritem of this.mRecurrenceItems) {
+ clonedItems.push(ritem.clone());
+ }
+ cloned.mRecurrenceItems = clonedItems;
+
+ let clonedExceptions = {};
+ for (let exitem in this.mExceptionMap) {
+ clonedExceptions[exitem] = this.mExceptionMap[exitem].cloneShallow(this.mBaseItem);
+ }
+ cloned.mExceptionMap = clonedExceptions;
+
+ return cloned;
+ },
+
+ /*
+ * calIRecurrenceInfo
+ */
+ get item() {
+ return this.mBaseItem;
+ },
+ set item(value) {
+ this.ensureMutable();
+
+ value = calTryWrappedJSObject(value);
+ this.mBaseItem = value;
+ // patch exception's parentItem:
+ for (let ex in this.mExceptionMap) {
+ let exitem = this.mExceptionMap[ex];
+ exitem.parentItem = value;
+ }
+ },
+
+ get isFinite() {
+ this.ensureBaseItem();
+
+ for (let ritem of this.mRecurrenceItems) {
+ if (!ritem.isFinite) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ getRecurrenceItems: function(aCount) {
+ this.ensureBaseItem();
+
+ aCount.value = this.mRecurrenceItems.length;
+ return this.mRecurrenceItems;
+ },
+
+ setRecurrenceItems: function(aCount, aItems) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ // XXX should we clone these?
+ this.mRecurrenceItems = aItems;
+ this.mPositiveRules = null;
+ this.mNegativeRules = null;
+ },
+
+ countRecurrenceItems: function() {
+ this.ensureBaseItem();
+
+ return this.mRecurrenceItems.length;
+ },
+
+ getRecurrenceItemAt: function(aIndex) {
+ this.ensureBaseItem();
+
+ if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ return this.mRecurrenceItems[aIndex];
+ },
+
+ appendRecurrenceItem: function(aItem) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ this.mRecurrenceItems.push(aItem);
+ if (aItem.isNegative) {
+ this.mNegativeRules.push(aItem);
+ } else {
+ this.mPositiveRules.push(aItem);
+ }
+ },
+
+ deleteRecurrenceItemAt: function(aIndex) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ if (this.mRecurrenceItems[aIndex].isNegative) {
+ this.mNegativeRules = null;
+ } else {
+ this.mPositiveRules = null;
+ }
+
+ this.mRecurrenceItems.splice(aIndex, 1);
+ },
+
+ deleteRecurrenceItem: function(aItem) {
+ // Because xpcom objects can be wrapped in various ways, testing for
+ // mere == sometimes returns false even when it should be true. Use
+ // the interface pointer returned by sip to avoid that problem.
+ let sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ sip1.data = aItem;
+ sip1.dataIID = Components.interfaces.calIRecurrenceItem;
+
+ let pos;
+ if ((pos = this.mRecurrenceItems.indexOf(sip1.data)) > -1) {
+ this.deleteRecurrenceItemAt(pos);
+ } else {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ },
+
+ insertRecurrenceItemAt: function(aItem, aIndex) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ if (aIndex < 0 || aIndex > this.mRecurrenceItems.length) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ if (aItem.isNegative) {
+ this.mNegativeRules.push(aItem);
+ } else {
+ this.mPositiveRules.push(aItem);
+ }
+
+ this.mRecurrenceItems.splice(aIndex, 0, aItem);
+ },
+
+ clearRecurrenceItems: function() {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ this.mRecurrenceItems = [];
+ this.mPositiveRules = [];
+ this.mNegativeRules = [];
+ },
+
+ /*
+ * calculations
+ */
+ getNextOccurrence: function(aTime) {
+ this.ensureBaseItem();
+ this.ensureSortedRecurrenceRules();
+
+ let startDate = this.mBaseItem.recurrenceStartDate;
+ let nextOccurrences = [];
+ let invalidOccurrences;
+ let negMap = {};
+ let minOccRid;
+
+ // Go through all negative rules to create a map of occurrences that
+ // should be skipped when going through occurrences.
+ for (let ritem of this.mNegativeRules) {
+ // TODO Infinite rules (i.e EXRULE) are not taken into account,
+ // because its very performance hungry and could potentially
+ // lead to a deadlock (i.e RRULE is canceled out by an EXRULE).
+ // This is ok for now, since EXRULE is deprecated anyway.
+ if (ritem.isFinite) {
+ // Get all occurrences starting at our recurrence start date.
+ // This is fine, since there will never be an EXDATE that
+ // occurrs before the event started and its illegal to EXDATE an
+ // RDATE.
+ let rdates = ritem.getOccurrences(startDate,
+ startDate,
+ null,
+ 0,
+ {});
+ // Map all negative dates.
+ for (let rdate of rdates) {
+ negMap[getRidKey(rdate)] = true;
+ }
+ } else {
+ WARN("Item '" + this.mBaseItem.title + "'" +
+ (this.mBaseItem.calendar ? " (" + this.mBaseItem.calendar.name + ")" : "") +
+ " has an infinite negative rule (EXRULE)");
+ }
+ }
+
+ let bailCounter = 0;
+ do {
+ invalidOccurrences = 0;
+ // Go through all positive rules and get the next recurrence id
+ // according to that rule. If for all rules the rid is "invalid",
+ // (i.e an EXDATE removed it, or an exception moved it somewhere
+ // else), then get the respective next rid.
+ //
+ // If in a loop at least one rid is valid (i.e not an exception, not
+ // an exdate, is after aTime), then remember the lowest one.
+ for (let i = 0; i < this.mPositiveRules.length; i++) {
+ let rDateInstance = cal.wrapInstance(this.mPositiveRules[i], Components.interfaces.calIRecurrenceDate);
+ let rRuleInstance = cal.wrapInstance(this.mPositiveRules[i], Components.interfaces.calIRecurrenceRule);
+ if (rDateInstance) {
+ // RDATEs are special. there is only one date in this rule,
+ // so no need to search anything.
+ let rdate = rDateInstance.date;
+ if (!nextOccurrences[i] && rdate.compare(aTime) > 0) {
+ // The RDATE falls into range, save it.
+ nextOccurrences[i] = rdate;
+ } else {
+ // The RDATE doesn't fall into range. This rule will
+ // always be invalid, since it can't give out a date.
+ nextOccurrences[i] = null;
+ invalidOccurrences++;
+ }
+ } else if (rRuleInstance) {
+ // RRULEs must not start searching before |startDate|, since
+ // the pattern is only valid afterwards. If an occurrence
+ // was found in a previous round, we can go ahead and start
+ // searching from that occurrence.
+ let searchStart = nextOccurrences[i] || startDate;
+
+ // Search for the next occurrence after aTime. If the last
+ // round was invalid, then in this round we need to search
+ // after nextOccurrences[i] to make sure getNextOccurrence()
+ // doesn't find the same occurrence again.
+ let searchDate = nextOccurrences[i] && nextOccurrences[i].compare(aTime) > 0
+ ? nextOccurrences[i]
+ : aTime;
+
+ nextOccurrences[i] = rRuleInstance
+ .getNextOccurrence(searchStart, searchDate);
+ }
+
+ // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
+ let nextKey = getRidKey(nextOccurrences[i]);
+ let isInExceptionMap = nextKey && (this.mExceptionMap[nextKey.substring(0, 8)] ||
+ this.mExceptionMap[nextKey]);
+ let isInNegMap = nextKey && (negMap[nextKey.substring(0, 8)] ||
+ negMap[nextKey]);
+ if (nextKey && (isInNegMap || isInExceptionMap)) {
+ // If the found recurrence id points to either an exception
+ // (will handle later) or an EXDATE, then nextOccurrences[i]
+ // is invalid and we might need to try again next round.
+ invalidOccurrences++;
+ } else if (nextOccurrences[i]) {
+ // We have a valid recurrence id (not an exception, not an
+ // EXDATE, falls into range). We only need to save the
+ // earliest occurrence after aTime (checking for aTime is
+ // not needed, since getNextOccurrence() above returns only
+ // occurrences after aTime).
+ if (!minOccRid || minOccRid.compare(nextOccurrences[i]) > 0) {
+ minOccRid = nextOccurrences[i];
+ }
+ }
+ }
+
+ // To make sure users don't just report bugs like "the application
+ // hangs", bail out after 100 runs. If this happens, it is most
+ // likely a bug.
+ if (bailCounter++ > 100) {
+ ERROR("Could not find next occurrence after 100 runs!");
+ return null;
+ }
+
+ // We counted how many positive rules found out that their next
+ // candidate is invalid. If all rules produce invalid next
+ // occurrences, a second round is needed.
+ } while (invalidOccurrences == this.mPositiveRules.length);
+
+ // Since we need to compare occurrences by date, save the rid found
+ // above also as a date. This works out because above we skipped
+ // exceptions.
+ let minOccDate = minOccRid;
+
+ // Scan exceptions for any dates earlier than the above found
+ // minOccDate, but still after aTime.
+ for (let ex in this.mExceptionMap) {
+ let exc = this.mExceptionMap[ex];
+ let start = exc.recurrenceStartDate;
+ if (start.compare(aTime) > 0 &&
+ (!minOccDate || start.compare(minOccDate) <= 0)) {
+ // This exception is earlier, save its rid (for getting the
+ // occurrence later on) and its date (for comparing to other
+ // exceptions).
+ minOccRid = exc.recurrenceId;
+ minOccDate = start;
+ }
+ }
+
+ // If we found a recurrence id any time above, then return the
+ // occurrence for it.
+ return (minOccRid ? this.getOccurrenceFor(minOccRid) : null);
+ },
+
+ getPreviousOccurrence: function(aTime) {
+ // TODO libical currently does not provide us with easy means of
+ // getting the previous occurrence. This could be fixed to improve
+ // performance greatly. Filed as libical feature request 1944020.
+
+ // HACK We never know how early an RDATE might be before the actual
+ // recurrence start. Since rangeStart cannot be null for recurrence
+ // items like calIRecurrenceRule, we need to work around by supplying a
+ // very early date. Again, this might have a high performance penalty.
+ let early = createDateTime();
+ early.icalString = "00000101T000000Z";
+
+ let rids = this.calculateDates(early,
+ aTime,
+ 0);
+ // The returned dates are sorted, so the last one is a good
+ // candidate, if it exists.
+ return (rids.length > 0 ? this.getOccurrenceFor(rids[rids.length - 1].id) : null);
+ },
+
+ // internal helper function;
+ calculateDates: function(aRangeStart, aRangeEnd, aMaxCount) {
+ this.ensureBaseItem();
+ this.ensureSortedRecurrenceRules();
+
+ function ridDateSortComptor(a, b) {
+ return a.rstart.compare(b.rstart);
+ }
+
+ // workaround for UTC- timezones
+ let rangeStart = ensureDateTime(aRangeStart);
+ let rangeEnd = ensureDateTime(aRangeEnd);
+
+ // If aRangeStart falls in the middle of an occurrence, libical will
+ // not return that occurrence when we go and ask for an
+ // icalrecur_iterator_new. This actually seems fairly rational, so
+ // instead of hacking libical, I'm going to move aRangeStart back far
+ // enough to make sure we get the occurrences we might miss.
+ let searchStart = rangeStart.clone();
+ let baseDuration = this.mBaseItem.duration;
+ if (baseDuration) {
+ let duration = baseDuration.clone();
+ duration.isNegative = true;
+ searchStart.addDuration(duration);
+ }
+
+ let startDate = this.mBaseItem.recurrenceStartDate;
+ if (startDate == null) {
+ // Todo created by other apps may have a saved recurrence but
+ // start and due dates disabled. Since no recurrenceStartDate,
+ // treat as undated task.
+ return [];
+ }
+
+ let dates = [];
+
+ // toss in exceptions first. Save a map of all exceptions ids, so we
+ // don't add the wrong occurrences later on.
+ let occurrenceMap = {};
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ let occDate = checkIfInRange(item, aRangeStart, aRangeEnd, true);
+ occurrenceMap[ex] = true;
+ if (occDate) {
+ binaryInsert(dates, { id: item.recurrenceId, rstart: occDate }, ridDateSortComptor);
+ }
+ }
+
+ // DTSTART/DUE is always part of the (positive) expanded set:
+ // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
+ let baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
+ let baseOccDateKey = getRidKey(baseOccDate);
+ if (baseOccDate && !occurrenceMap[baseOccDateKey]) {
+ occurrenceMap[baseOccDateKey] = true;
+ binaryInsert(dates, { id: baseOccDate, rstart: baseOccDate }, ridDateSortComptor);
+ }
+
+ // if both range start and end are specified, we ask for all of the occurrences,
+ // to make sure we catch all possible exceptions. If aRangeEnd isn't specified,
+ // then we have to ask for aMaxCount, and hope for the best.
+ let maxCount;
+ if (rangeStart && rangeEnd) {
+ maxCount = 0;
+ } else {
+ maxCount = aMaxCount;
+ }
+
+ // Apply positive rules
+ for (let ritem of this.mPositiveRules) {
+ let cur_dates = ritem.getOccurrences(startDate,
+ searchStart,
+ rangeEnd,
+ maxCount, {});
+ if (cur_dates.length == 0) {
+ continue;
+ }
+
+ // if positive, we just add these date to the existing set,
+ // but only if they're not already there
+
+ let index = 0;
+ let len = cur_dates.length;
+
+ // skip items before rangeStart due to searchStart libical hack:
+ if (rangeStart && baseDuration) {
+ for (; index < len; ++index) {
+ let date = cur_dates[index].clone();
+ date.addDuration(baseDuration);
+ if (rangeStart.compare(date) < 0) {
+ break;
+ }
+ }
+ }
+ for (; index < len; ++index) {
+ let date = cur_dates[index];
+ let dateKey = getRidKey(date);
+ if (occurrenceMap[dateKey]) {
+ // Don't add occurrences twice (i.e exception was
+ // already added before)
+ continue;
+ }
+ // TODO if cur_dates[] is also sorted, then this binary
+ // search could be optimized further
+ binaryInsert(dates, { id: date, rstart: date }, ridDateSortComptor);
+ occurrenceMap[dateKey] = true;
+ }
+ }
+
+ // Apply negative rules
+ for (let ritem of this.mNegativeRules) {
+ let cur_dates = ritem.getOccurrences(startDate,
+ searchStart,
+ rangeEnd,
+ maxCount, {});
+ if (cur_dates.length == 0) {
+ continue;
+ }
+
+ // XXX: i'm pretty sure negative dates can't really have exceptions
+ // (like, you can't make a date "real" by defining an RECURRENCE-ID which
+ // is an EXDATE, and then giving it a real DTSTART) -- so we don't
+ // check exceptions here
+ for (let dateToRemove of cur_dates) {
+ let dateToRemoveKey = getRidKey(dateToRemove);
+ if (dateToRemove.isDate) {
+ // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
+ let toRemove = [];
+ for (let occurenceKey in occurrenceMap) {
+ if (occurrenceMap[occurenceKey] && occurenceKey.substring(0, 8) == dateToRemoveKey) {
+ dates = dates.filter(date => date.id.compare(dateToRemove) != 0);
+ toRemove.push(occurenceKey);
+ }
+ }
+ for (let i = 0; i < toRemove.length; i++) {
+ delete occurrenceMap[toRemove[i]];
+ }
+ } else if (occurrenceMap[dateToRemoveKey]) {
+ // TODO PERF Theoretically we could use occurrence map
+ // to construct the array of occurrences. Right now I'm
+ // just using the occurrence map to skip the filter
+ // action if the occurrence isn't there anyway.
+ dates = dates.filter(date => date.id.compare(dateToRemove) != 0);
+ delete occurrenceMap[dateToRemoveKey];
+ }
+ }
+ }
+
+ // The list was already sorted above, chop anything over aMaxCount, if
+ // specified.
+ if (aMaxCount && dates.length > aMaxCount) {
+ dates = dates.slice(0, aMaxCount);
+ }
+
+ return dates;
+ },
+
+ getOccurrenceDates: function(aRangeStart, aRangeEnd, aMaxCount, aCount) {
+ let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
+ dates = dates.map(date => date.rstart);
+ aCount.value = dates.length;
+ return dates;
+ },
+
+ getOccurrences: function(aRangeStart, aRangeEnd, aMaxCount, aCount) {
+ let results = [];
+ let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
+ if (dates.length) {
+ let count;
+ if (aMaxCount) {
+ count = Math.min(aMaxCount, dates.length);
+ } else {
+ count = dates.length;
+ }
+
+ for (let i = 0; i < count; i++) {
+ results.push(this.getOccurrenceFor(dates[i].id));
+ }
+ }
+
+ aCount.value = results.length;
+ return results;
+ },
+
+ getOccurrenceFor: function(aRecurrenceId) {
+ let proxy = this.getExceptionFor(aRecurrenceId);
+ if (!proxy) {
+ return this.item.createProxy(aRecurrenceId);
+ }
+ return proxy;
+ },
+
+ removeOccurrenceAt: function(aRecurrenceId) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ let rdate = Components.classes["@mozilla.org/calendar/recurrence-date;1"]
+ .createInstance(Components.interfaces.calIRecurrenceDate);
+ rdate.isNegative = true;
+ rdate.date = aRecurrenceId.clone();
+
+ this.removeExceptionFor(rdate.date);
+
+ this.appendRecurrenceItem(rdate);
+ },
+
+ restoreOccurrenceAt: function(aRecurrenceId) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ for (let i = 0; i < this.mRecurrenceItems.length; i++) {
+ let rdate = cal.wrapInstance(this.mRecurrenceItems[i], Components.interfaces.calIRecurrenceDate);
+ if (rdate) {
+ if (rdate.isNegative && rdate.date.compare(aRecurrenceId) == 0) {
+ return this.deleteRecurrenceItemAt(i);
+ }
+ }
+ }
+
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ },
+
+ //
+ // exceptions
+ //
+
+ //
+ // Some notes:
+ //
+ // The way I read ICAL, RECURRENCE-ID is used to specify a
+ // particular instance of a recurring event, according to the
+ // RRULEs/RDATEs/etc. specified in the base event. If one of
+ // these is to be changed ("an exception"), then it can be
+ // referenced via the UID of the original event, and a
+ // RECURRENCE-ID of the start time of the instance to change.
+ // This, to me, means that an event where one of the instances has
+ // changed to a different time has a RECURRENCE-ID of the original
+ // start time, and a DTSTART/DTEND representing the new time.
+ //
+ // ITIP, however, seems to want something different -- you're
+ // supposed to use UID/RECURRENCE-ID to select from the current
+ // set of occurrences of an event. If you change the DTSTART for
+ // an instance, you're supposed to use the old (original) DTSTART
+ // as the RECURRENCE-ID, and put the new time as the DTSTART.
+ // However, after that change, to refer to that instance in the
+ // future, you have to use the modified DTSTART as the
+ // RECURRENCE-ID. This madness is described in ITIP end of
+ // section 3.7.1.
+ //
+ // This implementation does the first approach (RECURRENCE-ID will
+ // never change even if DTSTART for that instance changes), which
+ // I think is the right thing to do for CalDAV; I don't know what
+ // we'll do for incoming ITIP events though.
+ //
+ modifyException: function(anItem, aTakeOverOwnership) {
+ this.ensureBaseItem();
+
+ anItem = calTryWrappedJSObject(anItem);
+
+ if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
+ anItem.parentItem.id != this.mBaseItem.id) {
+ ERROR("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!");
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ if (anItem.recurrenceId == null) {
+ ERROR("recurrenceInfo::addException: item with null recurrenceId!");
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+
+ let itemtoadd;
+ if (aTakeOverOwnership && anItem.isMutable) {
+ itemtoadd = anItem;
+ itemtoadd.parentItem = this.mBaseItem;
+ } else {
+ itemtoadd = anItem.cloneShallow(this.mBaseItem);
+ }
+
+ // we're going to assume that the recurrenceId is valid here,
+ // because presumably the item came from one of our functions
+
+ let exKey = getRidKey(itemtoadd.recurrenceId);
+ this.mExceptionMap[exKey] = itemtoadd;
+ },
+
+ getExceptionFor: function(aRecurrenceId) {
+ this.ensureBaseItem();
+ // Interface calIRecurrenceInfo specifies result be null if not found.
+ // To avoid strict "reference to undefined property" warning, appending
+ // "|| null" gives explicit result in case where property undefined
+ // (or false, 0, null, or "", but here it should never be those values).
+ return this.mExceptionMap[getRidKey(aRecurrenceId)] || null;
+ },
+
+ removeExceptionFor: function(aRecurrenceId) {
+ this.ensureBaseItem();
+ delete this.mExceptionMap[getRidKey(aRecurrenceId)];
+ },
+
+ getExceptionIds: function(aCount) {
+ this.ensureBaseItem();
+
+ let ids = [];
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ ids.push(item.recurrenceId);
+ }
+
+ aCount.value = ids.length;
+ return ids;
+ },
+
+ // changing the startdate of an item needs to take exceptions into account.
+ // in case we're about to modify a parentItem (aka 'folded' item), we need
+ // to modify the recurrenceId's of all possibly existing exceptions as well.
+ onStartDateChange: function(aNewStartTime, aOldStartTime) {
+ // passing null for the new starttime would indicate an error condition,
+ // since having a recurrence without a starttime is invalid.
+ cal.ASSERT(aNewStartTime, "invalid arg!", true);
+
+ // no need to check for changes if there's no previous starttime.
+ if (!aOldStartTime) {
+ return;
+ }
+
+ // convert both dates to UTC since subtractDate is not timezone aware.
+ let timeDiff = aNewStartTime.getInTimezone(UTC()).subtractDate(aOldStartTime.getInTimezone(UTC()));
+
+ let rdates = {};
+
+ // take RDATE's and EXDATE's into account.
+ const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
+ let ritems = this.getRecurrenceItems({});
+ for (let ritem of ritems) {
+ let rDateInstance = cal.wrapInstance(ritem, kCalIRecurrenceDate);
+ let rRuleInstance = cal.wrapInstance(ritem, Components.interfaces.calIRecurrenceRule);
+ if (rDateInstance) {
+ ritem = rDateInstance;
+ let date = ritem.date;
+ date.addDuration(timeDiff);
+ if (!ritem.isNegative) {
+ rdates[getRidKey(date)] = date;
+ }
+ ritem.date = date;
+ } else if (rRuleInstance) {
+ ritem = rRuleInstance;
+ if (!ritem.isByCount) {
+ let untilDate = ritem.untilDate;
+ if (untilDate) {
+ untilDate.addDuration(timeDiff);
+ ritem.untilDate = untilDate;
+ }
+ }
+ }
+ }
+
+ let startTimezone = aNewStartTime.timezone;
+ let modifiedExceptions = [];
+ for (let exid of this.getExceptionIds({})) {
+ let ex = this.getExceptionFor(exid);
+ if (ex) {
+ ex = ex.clone();
+ // track RECURRENCE-IDs in DTSTART's or RDATE's timezone,
+ // otherwise those won't match any longer w.r.t DST:
+ let rid = ex.recurrenceId;
+ let rdate = rdates[getRidKey(rid)];
+ rid = rid.getInTimezone(rdate ? rdate.timezone : startTimezone);
+ rid.addDuration(timeDiff);
+ ex.recurrenceId = rid;
+ cal.shiftItem(ex, timeDiff);
+ modifiedExceptions.push(ex);
+ this.removeExceptionFor(exid);
+ }
+ }
+ for (let modifiedEx of modifiedExceptions) {
+ this.modifyException(modifiedEx, true);
+ }
+ },
+
+ onIdChange: function(aNewId) {
+ // patch all overridden items' id:
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ item.id = aNewId;
+ }
+ }
+};
diff --git a/calendar/base/src/calRelation.js b/calendar/base/src/calRelation.js
new file mode 100644
index 000000000..65b156361
--- /dev/null
+++ b/calendar/base/src/calRelation.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * calRelation prototype definition
+ *
+ * @implements calIRelation
+ * @constructor
+ */
+function calRelation() {
+ this.wrappedJSObject = this;
+ this.mProperties = new calPropertyBag();
+}
+var calRelationClassID = Components.ID("{76810fae-abad-4019-917a-08e95d5bbd68}");
+var calRelationInterfaces = [Components.interfaces.calIRelation];
+calRelation.prototype = {
+ mType: null,
+ mId: null,
+
+ classID: calRelationClassID,
+ QueryInterface: XPCOMUtils.generateQI(calRelationInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calRelationClassID,
+ contractID: "@mozilla.org/calendar/relation;1",
+ classDescription: "Calendar Item Relation",
+ interfaces: calRelationInterfaces
+ }),
+
+ /**
+ * @see calIRelation
+ */
+
+ get relType() {
+ return this.mType;
+ },
+ set relType(aType) {
+ return (this.mType = aType);
+ },
+
+ get relId() {
+ return this.mId;
+ },
+ set relId(aRelId) {
+ return (this.mId = aRelId);
+ },
+
+ get icalProperty() {
+ let icssvc = getIcsService();
+ let icalatt = icssvc.createIcalProperty("RELATED-TO");
+ if (this.mId) {
+ icalatt.value = this.mId;
+ }
+
+ if (this.mType) {
+ icalatt.setParameter("RELTYPE", this.mType);
+ }
+
+ for (let [key, value] of this.mProperties) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid relation property value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+ return icalatt;
+ },
+
+ set icalProperty(attProp) {
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new calPropertyBag();
+
+ if (attProp.value) {
+ this.mId = attProp.value;
+ }
+ for (let [name, value] of cal.ical.paramIterator(attProp)) {
+ if (name == "RELTYPE") {
+ this.mType = value;
+ continue;
+ }
+
+ this.setParameter(name, value);
+ }
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return (comp ? comp.icalString : "");
+ },
+ set icalString(val) {
+ let prop = cal.getIcsService().createIcalPropertyFromString(val);
+ if (prop.propertyName != "RELATED-TO") {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+ this.icalProperty = prop;
+ return val;
+ },
+
+ getParameter: function(aName) {
+ return this.mProperties.getProperty(aName);
+ },
+
+ setParameter: function(aName, aValue) {
+ return this.mProperties.setProperty(aName, aValue);
+ },
+
+ deleteParameter: function(aName) {
+ return this.mProperties.deleteProperty(aName);
+ },
+
+ clone: function() {
+ let newRelation = new calRelation();
+ newRelation.mId = this.mId;
+ newRelation.mType = this.mType;
+ for (let [name, value] of this.mProperties) {
+ newRelation.mProperties.setProperty(name, value);
+ }
+ return newRelation;
+ }
+};
diff --git a/calendar/base/src/calSleepMonitor.js b/calendar/base/src/calSleepMonitor.js
new file mode 100644
index 000000000..c98c5017c
--- /dev/null
+++ b/calendar/base/src/calSleepMonitor.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var calSleepMonitorClassID = Components.ID("9b987a8d-c2ef-4cb9-9602-1261b4b2f6fa");
+var calSleepMonitorInterfaces = [Components.interfaces.nsIObserver];
+
+function calSleepMonitor() {
+}
+
+calSleepMonitor.prototype = {
+ classID: calSleepMonitorClassID,
+ QueryInterface: XPCOMUtils.generateQI(calSleepMonitorInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calSleepMonitorClassID,
+ contractID: "@mozilla.org/calendar/sleep-monitor;1",
+ classDescription: "Calendar Sleep Monitor",
+ interfaces: calSleepMonitorInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ interval: 60000,
+ timer: null,
+ expected: null,
+ tolerance: 1000,
+
+ callback: function() {
+ let now = Date.now();
+ if (now - this.expected > this.tolerance) {
+ cal.LOG("[calSleepMonitor] Sleep cycle detected, notifying observers.");
+ Services.obs.notifyObservers(null, "wake_notification", null);
+ }
+ this.expected = now + this.interval;
+ },
+ start: function() {
+ this.stop();
+ this.expected = Date.now() + this.interval;
+ this.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ this.timer.initWithCallback(this.callback.bind(this), this.interval, Components.interfaces.nsITimer.TYPE_REPEATING_PRECISE);
+ },
+ stop: function() {
+ if (this.timer) {
+ this.timer.cancel();
+ this.timer = null;
+ }
+ },
+
+ // nsIObserver:
+ observe: function(aSubject, aTopic, aData) {
+ // calSleepMonitor is not used on Windows.
+ if (Services.appinfo.OS == "WINNT") {
+ return;
+ }
+
+ if (aTopic == "profile-after-change") {
+ cal.LOG("[calSleepMonitor] Starting sleep monitor.");
+ this.start();
+
+ Services.obs.addObserver(this, "quit-application", false);
+ } else if (aTopic == "quit-application") {
+ cal.LOG("[calSleepMonitor] Stopping sleep monitor.");
+ this.stop();
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([calSleepMonitor]);
diff --git a/calendar/base/src/calSleepMonitor.manifest b/calendar/base/src/calSleepMonitor.manifest
new file mode 100644
index 000000000..7089b5445
--- /dev/null
+++ b/calendar/base/src/calSleepMonitor.manifest
@@ -0,0 +1,3 @@
+component {9b987a8d-c2ef-4cb9-9602-1261b4b2f6fa} calSleepMonitor.js
+contract @mozilla.org/calendar/sleep-monitor;1 {9b987a8d-c2ef-4cb9-9602-1261b4b2f6fa}
+category profile-after-change calSleepMonitor @mozilla.org/calendar/sleep-monitor;1
diff --git a/calendar/base/src/calStartupService.js b/calendar/base/src/calStartupService.js
new file mode 100644
index 000000000..9af0cf70d
--- /dev/null
+++ b/calendar/base/src/calStartupService.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Helper function to asynchronously call a certain method on the objects passed
+ * in 'services' in order (i.e wait until the first completes before calling the
+ * second
+ *
+ * @param method The method name to call. Usually startup/shutdown.
+ * @param services The array of service objects to call on.
+ */
+function callOrderedServices(method, services) {
+ let service = services.shift();
+ if (service) {
+ service[method]({
+ onResult: function() {
+ callOrderedServices(method, services);
+ }
+ });
+ }
+}
+
+function calStartupService() {
+ this.wrappedJSObject = this;
+ this.setupObservers();
+}
+
+var calStartupServiceInterfaces = [Components.interfaces.nsIObserver];
+var calStartupServiceClassID = Components.ID("{2547331f-34c0-4a4b-b93c-b503538ba6d6}");
+calStartupService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI(calStartupServiceInterfaces),
+ classID: calStartupServiceClassID,
+ classInfo: XPCOMUtils.generateCI({
+ contractID: "@mozilla.org/calendar/startup-service;1",
+ classDescription: "Calendar Startup Service",
+ classID: calStartupServiceClassID,
+ interfaces: calStartupServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ // Startup Service Methods
+
+ /**
+ * Sets up the needed observers for noticing startup/shutdown
+ */
+ setupObservers: function() {
+ Services.obs.addObserver(this, "profile-after-change", false);
+ Services.obs.addObserver(this, "profile-before-change", false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ started: false,
+
+ /**
+ * Gets the startup order of services. This is an array of service objects
+ * that should be called in order at startup.
+ *
+ * @return The startup order as an array.
+ */
+ getStartupOrder: function() {
+ let self = this;
+ let tzService = Components.classes["@mozilla.org/calendar/timezone-service;1"]
+ .getService(Components.interfaces.calITimezoneService);
+ let calMgr = Components.classes["@mozilla.org/calendar/manager;1"]
+ .getService(Components.interfaces.calICalendarManager);
+
+ // Notification object
+ let notify = {
+ startup: function(aCompleteListener) {
+ self.started = true;
+ Services.obs.notifyObservers(null, "calendar-startup-done", null);
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ },
+ shutdown: function(aCompleteListener) {
+ // Argh, it would have all been so pretty! Since we just reverse
+ // the array, the shutdown notification would happen before the
+ // other shutdown calls. For lack of pretty code, I'm
+ // leaving this out! Users can still listen to xpcom-shutdown.
+ self.started = false;
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ }
+ };
+
+ // We need to spin up the timezone service before the calendar manager
+ // to ensure we have the timezones initialized. Make sure "notify" is
+ // last in this array!
+ return [tzService, calMgr, notify];
+ },
+
+ /**
+ * Observer notification callback
+ */
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "profile-after-change":
+ callOrderedServices("startup", this.getStartupOrder());
+ break;
+ case "profile-before-change":
+ callOrderedServices("shutdown", this.getStartupOrder().reverse());
+ break;
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, "profile-after-change");
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ break;
+ }
+ }
+};
diff --git a/calendar/base/src/calTimezone.js b/calendar/base/src/calTimezone.js
new file mode 100644
index 000000000..ac88d511c
--- /dev/null
+++ b/calendar/base/src/calTimezone.js
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/ical.js");
+
+function calICALJSTimezone(innerObject) {
+ this.innerObject = innerObject || new ICAL.Timezone();
+ this.wrappedJSObject = this;
+}
+
+var calTimezoneInterfaces = [Components.interfaces.calITimezone];
+var calTimezoneClassID = Components.ID("{6702eb17-a968-4b43-b562-0d0c5f8e9eb5}");
+calICALJSTimezone.prototype = {
+ QueryInterface: XPCOMUtils.generateQI(calTimezoneInterfaces),
+ classID: calTimezoneClassID,
+ classInfo: XPCOMUtils.generateCI({
+ contractID: "@mozilla.org/calendar/timezone;1",
+ classDescription: "Calendar Timezone",
+ classID: calTimezoneClassID,
+ interfaces: calTimezoneInterfaces
+ }),
+
+ innerObject: null,
+
+ get provider() { return cal.getTimezoneService(); },
+ get icalComponent() {
+ let innerComp = this.innerObject.component;
+ let comp = null;
+ if (innerComp) {
+ comp = cal.getIcsService().createIcalComponent("VTIMEZONE");
+ comp.icalComponent = innerComp;
+ }
+ return comp;
+ },
+ get tzid() { return this.innerObject.tzid; },
+ get isFloating() { return this.innerObject == ICAL.Timezone.localTimezone; },
+ get isUTC() { return this.innerObject == ICAL.Timezone.utcTimezone; },
+ get latitude() { return this.innerObject.latitude; },
+ get longitude() { return this.innerObject.longitude; },
+ get displayName() {
+ let bundle = ICAL.Timezone.cal_tz_bundle;
+ let stringName = "pref.timezone." + this.tzid.replace(/\//g, ".");
+ let displayName = this.tzid;
+ try {
+ displayName = bundle.GetStringFromName(stringName);
+ } catch (e) {
+ // Just use the TZID if the string is mising.
+ }
+ this.__defineGetter__("displayName", () => {
+ return displayName;
+ });
+ return displayName;
+ },
+
+ tostring: function() { return this.innerObject.toString(); }
+};
+
+function calLibicalTimezone(tzid, component, latitude, longitude) {
+ this.wrappedJSObject = this;
+ this.tzid = tzid;
+ this.mComponent = component;
+ this.mUTC = false;
+ this.isFloating = false;
+ this.latitude = latitude;
+ this.longitude = longitude;
+}
+calLibicalTimezone.prototype = {
+ QueryInterface: XPCOMUtils.generateQI(calTimezoneInterfaces),
+ classID: calTimezoneClassID,
+ classInfo: XPCOMUtils.generateCI({
+ contractID: "@mozilla.org/calendar/timezone;1",
+ classDescription: "Calendar Timezone",
+ classID: calTimezoneClassID,
+ interfaces: calTimezoneInterfaces
+ }),
+
+ toString: function() {
+ return (this.icalComponent ? this.icalComponent.toString() : this.tzid);
+ },
+
+ get isUTC() { return this.mUTC; },
+
+ get icalComponent() {
+ let comp = this.mComponent;
+ if (comp && (typeof comp == "string")) {
+ this.mComponent = cal.getIcsService().parseICS("BEGIN:VCALENDAR\r\n" + comp + "\r\nEND:VCALENDAR\r\n", null)
+ .getFirstSubcomponent("VTIMEZONE");
+ }
+ return this.mComponent;
+ },
+
+ get displayName() {
+ if (this.mDisplayName === undefined) {
+ try {
+ this.mDisplayName = g_stringBundle.GetStringFromName("pref.timezone." + this.tzid.replace(/\//g, "."));
+ } catch (exc) {
+ // don't assert here, but gracefully fall back to TZID:
+ cal.LOG("Timezone property lookup failed! Falling back to " + this.tzid + "\n" + exc);
+ this.mDisplayName = this.tzid;
+ }
+ }
+ return this.mDisplayName;
+ },
+
+ get provider() { return cal.getTimezoneService(); }
+};
diff --git a/calendar/base/src/calTimezoneService.js b/calendar/base/src/calTimezoneService.js
new file mode 100644
index 000000000..42cd60c97
--- /dev/null
+++ b/calendar/base/src/calTimezoneService.js
@@ -0,0 +1,817 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://calendar/modules/ical.js");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+/* exported g_stringBundle */
+
+var g_stringBundle = null;
+
+function calStringEnumerator(stringArray) {
+ this.mIndex = 0;
+ this.mStringArray = stringArray;
+}
+calStringEnumerator.prototype = {
+ // nsIUTF8StringEnumerator:
+ hasMore: function() {
+ return (this.mIndex < this.mStringArray.length);
+ },
+ getNext: function() {
+ if (!this.hasMore()) {
+ throw Components.results.NS_ERROR_UNEXPECTED;
+ }
+ return this.mStringArray[this.mIndex++];
+ }
+};
+
+function calTimezoneService() {
+ this.wrappedJSObject = this;
+
+ this.mZones = new Map();
+
+ ICAL.TimezoneService = this.wrappedJSObject;
+}
+var calTimezoneServiceClassID = Components.ID("{e736f2bd-7640-4715-ab35-887dc866c587}");
+var calTimezoneServiceInterfaces = [
+ Components.interfaces.calITimezoneService,
+ Components.interfaces.calITimezoneProvider,
+ Components.interfaces.calIStartupService
+];
+calTimezoneService.prototype = {
+ mDefaultTimezone: null,
+ mHasSetupObservers: false,
+ mVersion: null,
+ mZones: null,
+
+ classID: calTimezoneServiceClassID,
+ QueryInterface: XPCOMUtils.generateQI(calTimezoneServiceInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calTimezoneServiceClassID,
+ contractID: "@mozilla.org/calendar/timezone-service;1",
+ classDescription: "Calendar Timezone Service",
+ interfaces: calTimezoneServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ // ical.js TimezoneService methods
+ has: function(id) { return this.getTimezone(id) != null; },
+ get: function(id) {
+ return id ? unwrapSingle(ICAL.Timezone, this.getTimezone(id)) : null;
+ },
+ remove: function() {},
+ register: function() {},
+
+ // calIStartupService:
+ startup: function(aCompleteListener) {
+ function fetchJSON(aURL) {
+ cal.LOG("[calTimezoneService] Loading " + aURL);
+
+ return new Promise((resolve, reject) => {
+ let uri = Services.io.newURI(aURL, null, null);
+ let channel = Services.io.newChannelFromURI2(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(status);
+ return;
+ }
+
+ try {
+ let jsonData = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ let tzData = JSON.parse(jsonData);
+ resolve(tzData);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ }
+
+ let resNamespace = "calendar";
+ // Check for presence of the calendar timezones add-on.
+ let resProtocol = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Components.interfaces.nsIResProtocolHandler);
+ if (resProtocol.hasSubstitution("calendar-timezones")) {
+ resNamespace = "calendar-timezones";
+ }
+
+ fetchJSON("resource://" + resNamespace + "/timezones/zones.json").then((tzData) => {
+ for (let tzid of Object.keys(tzData.aliases)) {
+ let data = tzData.aliases[tzid];
+ if (typeof data == "object" && data !== null) {
+ this.mZones.set(tzid, data);
+ }
+ }
+ for (let tzid of Object.keys(tzData.zones)) {
+ let data = tzData.zones[tzid];
+ if (typeof data == "object" && data !== null) {
+ this.mZones.set(tzid, data);
+ }
+ }
+
+ this.mVersion = tzData.version;
+ cal.LOG("[calTimezoneService] Timezones version " + this.version + " loaded");
+
+ let bundleURL = "chrome://" + resNamespace + "/locale/timezones.properties";
+ g_stringBundle = ICAL.Timezone.cal_tz_bundle = Services.strings.createBundle(bundleURL);
+
+ // Make sure UTC and floating are cached by calling their getters
+ this.UTC; // eslint-disable-line no-unused-expressions
+ this.floating; // eslint-disable-line no-unused-expressions
+ }).then(() => {
+ if (aCompleteListener) {
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ }
+ }, (error) => {
+ // We have to give up. Show an error and fail hard!
+ let msg = cal.calGetString("calendar", "missingCalendarTimezonesError");
+ cal.ERROR(msg);
+ cal.showError(msg);
+ });
+ },
+
+ shutdown: function(aCompleteListener) {
+ Services.prefs.removeObserver("calendar.timezone.local", this);
+ aCompleteListener.onResult(null, Components.results.NS_OK);
+ },
+
+ get UTC() {
+ if (!this.mZones.has("UTC")) {
+ let utc;
+ if (Preferences.get("calendar.icaljs", false)) {
+ utc = new calICALJSTimezone(ICAL.Timezone.utcTimezone);
+ } else {
+ utc = new calLibicalTimezone("UTC", null, "", "");
+ utc.mUTC = true;
+ }
+
+ this.mZones.set("UTC", { zone: utc });
+ }
+
+ return this.mZones.get("UTC").zone;
+ },
+
+ get floating() {
+ if (!this.mZones.has("floating")) {
+ let floating;
+ if (Preferences.get("calendar.icaljs", false)) {
+ floating = new calICALJSTimezone(ICAL.Timezone.localTimezone);
+ } else {
+ floating = new calLibicalTimezone("floating", null, "", "");
+ floating.isFloating = true;
+ }
+ this.mZones.set("floating", { zone: floating });
+ }
+
+ return this.mZones.get("floating").zone;
+ },
+
+ // calITimezoneProvider:
+ getTimezone: function(tzid) {
+ if (!tzid) {
+ cal.ERROR("Unknown timezone requested\n" + cal.STACK(10));
+ return null;
+ }
+ if (tzid.startsWith("/mozilla.org/")) {
+ // We know that our former tzids look like "/mozilla.org/<dtstamp>/continent/..."
+ // The ending of the mozilla prefix is the index of that slash before the
+ // continent. Therefore, we start looking for the prefix-ending slash
+ // after position 13.
+ tzid = tzid.substring(tzid.indexOf("/", 13) + 1);
+ }
+
+ let timezone = this.mZones.get(tzid);
+ if (!timezone) {
+ cal.ERROR("Couldn't find " + tzid);
+ return null;
+ }
+ if (!timezone.zone) {
+ if (timezone.aliasTo) {
+ // This zone is an alias.
+ timezone.zone = this.getTimezone(timezone.aliasTo);
+ } else if (Preferences.get("calendar.icaljs", false)) {
+ let parsedComp = ICAL.parse("BEGIN:VCALENDAR\r\n" + timezone.ics + "\r\nEND:VCALENDAR");
+ let icalComp = new ICAL.Component(parsedComp);
+ let tzComp = icalComp.getFirstSubcomponent("vtimezone");
+ timezone.zone = new calICALJSTimezone(ICAL.Timezone.fromData({
+ tzid: tzid,
+ component: tzComp,
+ latitude: timezone.latitude,
+ longitude: timezone.longitude
+ }));
+ } else {
+ timezone.zone = new calLibicalTimezone(tzid, timezone.ics, timezone.latitude, timezone.longitude);
+ }
+ }
+ return timezone.zone;
+ },
+
+ get timezoneIds() {
+ let zones = [];
+ for (let [k, v] of this.mZones.entries()) {
+ if (!v.aliasTo && k != "UTC" && k != "floating") {
+ zones.push(k);
+ }
+ }
+ return new calStringEnumerator(zones);
+ },
+
+ get aliasIds() {
+ let zones = [];
+ for (let [key, value] of this.mZones.entries()) {
+ if (value.aliasTo && key != "UTC" && key != "floating") {
+ zones.push(key);
+ }
+ }
+ return new calStringEnumerator(zones);
+ },
+
+ get version() {
+ return this.mVersion;
+ },
+
+ get defaultTimezone() {
+ if (!this.mDefaultTimezone) {
+ let prefTzid = Preferences.get("calendar.timezone.local", null);
+ let tzid = prefTzid;
+ if (!tzid) {
+ try {
+ tzid = guessSystemTimezone();
+ } catch (e) {
+ cal.WARN("An exception occurred guessing the system timezone, trying UTC. Exception: " + e);
+ tzid = "UTC";
+ }
+ }
+ this.mDefaultTimezone = this.getTimezone(tzid);
+ cal.ASSERT(this.mDefaultTimezone, "Timezone not found: " + tzid);
+ // Update prefs if necessary:
+ if (this.mDefaultTimezone && this.mDefaultTimezone.tzid != prefTzid) {
+ Preferences.set("calendar.timezone.local", this.mDefaultTimezone.tzid);
+ }
+
+ // We need to observe the timezone preference to update the default
+ // timezone if needed.
+ this.setupObservers();
+ }
+ return this.mDefaultTimezone;
+ },
+
+ setupObservers: function() {
+ if (!this.mHasSetupObservers) {
+ // Now set up the observer
+ Services.prefs.addObserver("calendar.timezone.local", this, false);
+ this.mHasSetupObservers = true;
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed" && aData == "calendar.timezone.local") {
+ // Unsetting the default timezone will make the next call to the
+ // default timezone getter set up the correct timezone again.
+ this.mDefaultTimezone = null;
+ }
+ }
+};
+
+/**
+ * We're going to do everything in our power, short of rumaging through the
+ * user's actual file-system, to figure out the time-zone they're in. The
+ * deciding factors are the offsets given by (northern-hemisphere) summer and
+ * winter JSdates. However, when available, we also use the name of the
+ * timezone in the JSdate, or a string-bundle term from the locale.
+ *
+ * @return a mozilla ICS timezone string.
+*/
+function guessSystemTimezone() {
+ // Probe JSDates for basic OS timezone offsets and names.
+ const dateJun = (new Date(2005, 5, 20)).toString();
+ const dateDec = (new Date(2005, 11, 20)).toString();
+ const tzNameRegex = /[^(]* ([^ ]*) \(([^)]+)\)/;
+ const nameDataJun = dateJun.match(tzNameRegex);
+ const nameDataDec = dateDec.match(tzNameRegex);
+ const tzNameJun = nameDataJun && nameDataJun[2];
+ const tzNameDec = nameDataDec && nameDataDec[2];
+ const offsetRegex = /[+-]\d{4}/;
+ const offsetJun = dateJun.match(offsetRegex)[0];
+ const offsetDec = dateDec.match(offsetRegex)[0];
+
+ const tzSvc = cal.getTimezoneService();
+
+ let continent = "Africa|America|Antarctica|Asia|Australia|Europe";
+ let ocean = "Arctic|Atlantic|Indian|Pacific";
+ let tzRegex = new RegExp(".*((?:" + continent + "|" + ocean + ")" +
+ "(?:[/][-A-Z_a-z]+)+)");
+
+ function getIcalString(component, property) {
+ let prop = (component && component.getFirstProperty(property));
+ return (prop ? prop.valueAsIcalString : null);
+ }
+
+ // Check if Olson ZoneInfo timezone matches OS/JSDate timezone properties:
+ // * standard offset and daylight/summer offset if present (longitude),
+ // * if has summer time, direction of change (northern/southern hemisphere)
+ // * if has summer time, dates of next transitions
+ // * timezone name (such as "Western European Standard Time").
+ // Score is 3 if matches dates and names, 2 if matches dates without names,
+ // 1 if matches dates within a week (so changes on different weekday),
+ // otherwise 0 if no match.
+ function checkTZ(tzId) {
+ let timezone = tzSvc.getTimezone(tzId);
+
+ // Have to handle UTC separately because it has no .icalComponent.
+ if (timezone.isUTC) {
+ if (offsetDec == 0 && offsetJun == 0) {
+ if (tzNameJun == "UTC" && tzNameDec == "UTC") {
+ return 3;
+ } else {
+ return 2;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ let subComp = timezone.icalComponent;
+ // find currently applicable time period, not just first,
+ // because offsets of timezone may be changed over the years.
+ let standard = findCurrentTimePeriod(timezone, subComp, "STANDARD");
+ let standardTZOffset = getIcalString(standard, "TZOFFSETTO");
+ let standardName = getIcalString(standard, "TZNAME");
+ let daylight = findCurrentTimePeriod(timezone, subComp, "DAYLIGHT");
+ let daylightTZOffset = getIcalString(daylight, "TZOFFSETTO");
+ let daylightName = getIcalString(daylight, "TZNAME");
+
+ // Try northern hemisphere cases.
+ if (offsetDec == standardTZOffset && offsetDec == offsetJun &&
+ !daylight) {
+ if (standardName && standardName == tzNameJun) {
+ return 3;
+ } else {
+ return 2;
+ }
+ }
+
+ if (offsetDec == standardTZOffset && offsetJun == daylightTZOffset &&
+ daylight) {
+ let dateMatchWt = systemTZMatchesTimeShiftDates(timezone, subComp);
+ if (dateMatchWt > 0) {
+ if (standardName && standardName == tzNameJun &&
+ daylightName && daylightName == tzNameDec) {
+ return 3;
+ } else {
+ return dateMatchWt;
+ }
+ }
+ }
+
+ // Now flip them and check again, to cover southern hemisphere cases.
+ if (offsetJun == standardTZOffset && offsetDec == offsetJun &&
+ !daylight) {
+ if (standardName && standardName == tzNameDec) {
+ return 3;
+ } else {
+ return 2;
+ }
+ }
+
+ if (offsetJun == standardTZOffset && offsetDec == daylightTZOffset &&
+ daylight) {
+ let dateMatchWt = systemTZMatchesTimeShiftDates(timezone, subComp);
+ if (dateMatchWt > 0) {
+ if (standardName && standardName == tzNameJun &&
+ daylightName && daylightName == tzNameDec) {
+ return 3;
+ } else {
+ return dateMatchWt;
+ }
+ }
+ }
+ return 0;
+ }
+
+ // returns 2=match-within-hours, 1=match-within-week, 0=no-match
+ function systemTZMatchesTimeShiftDates(timezone, subComp) {
+ // Verify local autumn and spring shifts also occur in system timezone
+ // (jsDate) on correct date in correct direction.
+ // (Differs for northern/southern hemisphere.
+ // Local autumn shift is to local winter STANDARD time.
+ // Local spring shift is to local summer DAYLIGHT time.)
+ const autumnShiftJSDate =
+ findCurrentTimePeriod(timezone, subComp, "STANDARD", true);
+ const afterAutumnShiftJSDate = new Date(autumnShiftJSDate);
+ const beforeAutumnShiftJSDate = new Date(autumnShiftJSDate);
+ const springShiftJSDate =
+ findCurrentTimePeriod(timezone, subComp, "DAYLIGHT", true);
+ const beforeSpringShiftJSDate = new Date(springShiftJSDate);
+ const afterSpringShiftJSDate = new Date(springShiftJSDate);
+ // Try with 6 HOURS fuzz in either direction, since OS and ZoneInfo
+ // may disagree on the exact time of shift (midnight, 2am, 4am, etc).
+ beforeAutumnShiftJSDate.setHours(autumnShiftJSDate.getHours() - 6);
+ afterAutumnShiftJSDate.setHours(autumnShiftJSDate.getHours() + 6);
+ afterSpringShiftJSDate.setHours(afterSpringShiftJSDate.getHours() + 6);
+ beforeSpringShiftJSDate.setHours(beforeSpringShiftJSDate.getHours() - 6);
+ if ((beforeAutumnShiftJSDate.getTimezoneOffset() <
+ afterAutumnShiftJSDate.getTimezoneOffset()) &&
+ (beforeSpringShiftJSDate.getTimezoneOffset() >
+ afterSpringShiftJSDate.getTimezoneOffset())) {
+ return 2;
+ }
+ // Try with 7 DAYS fuzz in either direction, so if no other timezone
+ // found, will have a nearby timezone that disagrees only on the
+ // weekday of shift (sunday vs. friday vs. calendar day), or off by
+ // exactly one week, (e.g., needed to guess Africa/Cairo on w2k in
+ // 2006).
+ beforeAutumnShiftJSDate.setDate(autumnShiftJSDate.getDate() - 7);
+ afterAutumnShiftJSDate.setDate(autumnShiftJSDate.getDate() + 7);
+ afterSpringShiftJSDate.setDate(afterSpringShiftJSDate.getDate() + 7);
+ beforeSpringShiftJSDate.setDate(beforeSpringShiftJSDate.getDate() - 7);
+ if ((beforeAutumnShiftJSDate.getTimezoneOffset() <
+ afterAutumnShiftJSDate.getTimezoneOffset()) &&
+ (beforeSpringShiftJSDate.getTimezoneOffset() >
+ afterSpringShiftJSDate.getTimezoneOffset())) {
+ return 1;
+ }
+ // no match
+ return 0;
+ }
+
+ const todayUTC = cal.jsDateToDateTime(new Date());
+ const oneYrUTC = todayUTC.clone(); oneYrUTC.year += 1;
+ const periodStartCalDate = cal.createDateTime();
+ const periodUntilCalDate = cal.createDateTime(); // until timezone is UTC
+ const periodCalRule = cal.createRecurrenceRule();
+ const untilRegex = /UNTIL=(\d{8}T\d{6}Z)/;
+
+ function findCurrentTimePeriod(timezone, subComp, standardOrDaylight,
+ isForNextTransitionDate) {
+ // Iterate through 'STANDARD' declarations or 'DAYLIGHT' declarations
+ // (periods in history with different settings.
+ // e.g., US changes daylight start in 2007 (from April to March).)
+ // Each period is marked by a DTSTART.
+ // Find the currently applicable period: has most recent DTSTART
+ // not later than today and no UNTIL, or UNTIL is greater than today.
+ for (let period of cal.ical.subcomponentIterator(subComp, standardOrDaylight)) {
+ periodStartCalDate.icalString = getIcalString(period, "DTSTART");
+ periodStartCalDate.timezone = timezone;
+ if (oneYrUTC.nativeTime < periodStartCalDate.nativeTime) {
+ continue; // period starts too far in future
+ }
+ // Must examine UNTIL date (not next daylight start) because
+ // some zones (e.g., Arizona, Hawaii) may stop using daylight
+ // time, so there might not be a next daylight start.
+ let rrule = period.getFirstProperty("RRULE");
+ if (rrule) {
+ let match = untilRegex.exec(rrule.valueAsIcalString);
+ if (match) {
+ periodUntilCalDate.icalString = match[1];
+ if (todayUTC.nativeTime > periodUntilDate.nativeTime) {
+ continue; // period ends too early
+ }
+ } // else forever rule
+ } // else no daylight rule
+
+ // found period that covers today.
+ if (!isForNextTransitionDate) {
+ return period;
+ } else if (todayUTC.nativeTime < periodStartCalDate.nativeTime) {
+ // already know periodStartCalDate < oneYr from now,
+ // and transitions are at most once per year, so it is next.
+ return cal.dateTimeToJsDate(periodStartCalDate);
+ } else if (rrule) {
+ // find next occurrence after today
+ periodCalRule.icalProperty = rrule;
+ let nextTransitionDate =
+ periodCalRule.getNextOccurrence(periodStartCalDate,
+ todayUTC);
+ // make sure rule doesn't end before next transition date.
+ if (nextTransitionDate) {
+ return cal.dateTimeToJsDate(nextTransitionDate);
+ }
+ }
+ }
+ // no such period found
+ return null;
+ }
+
+ function environmentVariableValue(varName) {
+ let envSvc = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let value = envSvc.get(varName);
+ if (!value || !value.match(tzRegex)) {
+ return "";
+ }
+ return varName + "=" + value;
+ }
+
+ function symbolicLinkTarget(filepath) {
+ try {
+ let file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ file.initWithPath(filepath);
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (!file.exists() || !file.isSymlink() || !file.target.match(tzRegex)) {
+ return "";
+ }
+
+ return filepath + " -> " + file.target;
+ } catch (ex) {
+ Components.utils.reportError(filepath + ": " + ex);
+ return "";
+ }
+ }
+
+ function fileFirstZoneLineString(filepath) {
+ // return first line of file that matches tzRegex (ZoneInfo id),
+ // or "" if no file or no matching line.
+ try {
+ let file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ file.initWithPath(filepath);
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (!file.exists()) {
+ return "";
+ }
+ let fileInstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ const PR_RDONLY = 0x1;
+ fileInstream.init(file, PR_RDONLY, 0, 0);
+ fileInstream.QueryInterface(Components.interfaces.nsILineInputStream);
+ try {
+ let line = {}, hasMore = true, MAXLINES = 10;
+ for (let i = 0; hasMore && i < MAXLINES; i++) {
+ hasMore = fileInstream.readLine(line);
+ if (line.value && line.value.match(tzRegex)) {
+ return filepath + ": " + line.value;
+ }
+ }
+ return ""; // not found
+ } finally {
+ fileInstream.close();
+ }
+ } catch (ex) {
+ Components.utils.reportError(filepath + ": " + ex);
+ return "";
+ }
+ }
+
+ function weekday(icsDate, timezone) {
+ let calDate = cal.createDateTime(icsDate);
+ calDate.timezone = timezone;
+ return cal.dateTimeToJsDate(calDate).toLocaleFormat("%a");
+ }
+
+ // Try to find a tz that matches OS/JSDate timezone. If no name match,
+ // will use first of probable timezone(s) with highest score.
+ let probableTZId = "floating"; // default fallback tz if no tz matches.
+ let probableTZScore = 0;
+ let probableTZSource = null;
+
+ const calProperties = Services.strings.createBundle("chrome://calendar/locale/calendar.properties");
+
+ // First, try to detect operating system timezone.
+ let zoneInfoIdFromOSUserTimeZone = null;
+ let osUserTimeZone = null;
+ try {
+ let handler = Components.classes["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Components.interfaces.nsIHttpProtocolHandler);
+
+ if (handler.oscpu.match(/^Windows/)) {
+ let wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
+ .createInstance(Components.interfaces.nsIWindowsRegKey);
+ wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE,
+ "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation",
+ wrk.ACCESS_READ);
+ if (wrk.hasValue("TimeZoneKeyName")) {
+ // Windows Vista and later have this key.
+ // Clear trailing garbage on this key, see bug 1129712.
+ osUserTimeZone = wrk.readStringValue("TimeZoneKeyName").split("\0")[0];
+ } else {
+ // If on Windows XP, current timezone only lists its localized name,
+ // so to find its registry key name, match localized name to
+ // localized names of each windows timezone listed in registry.
+ // Then use the registry key name to see if this timezone has a
+ // known ZoneInfo name.
+ let currentTZStandardName = wrk.readStringValue("StandardName");
+ wrk.close();
+
+ wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones",
+ wrk.ACCESS_READ);
+
+ // Linear search matching localized name of standard timezone
+ // to find the non-localized registry key.
+ // (Registry keys are sorted by subkeyName, not by localized name
+ // nor offset, so cannot use binary search.)
+ for (let i = 0; i < wrk.childCount; i++) {
+ let subkeyName = wrk.getChildName(i);
+ let subkey = wrk.openChild(subkeyName, wrk.ACCESS_READ);
+ let std = subkey.readStringValue("Std");
+ subkey.close();
+ if (std == currentTZStandardName) {
+ osUserTimeZone = subkeyName;
+ break;
+ }
+ }
+ }
+ wrk.close();
+
+ if (osUserTimeZone != null) {
+ // Lookup timezone registry key in table of known tz keys
+ // to convert to ZoneInfo timezone id.
+ const regKeyToZoneInfoBundle = Services.strings.createBundle(
+ "chrome://calendar/content/WindowsNTToZoneInfoTZId.properties");
+ zoneInfoIdFromOSUserTimeZone =
+ regKeyToZoneInfoBundle.GetStringFromName(osUserTimeZone);
+ }
+ } else {
+ // Else look for ZoneInfo timezone id in
+ // - TZ environment variable value
+ // - /etc/localtime symbolic link target path
+ // - /etc/TIMEZONE or /etc/timezone file content
+ // - /etc/sysconfig/clock file line content.
+ // The timezone is set per user via the TZ environment variable.
+ // TZ may contain a path that may start with a colon and ends with
+ // a ZoneInfo timezone identifier, such as ":America/New_York" or
+ // ":/share/lib/zoneinfo/America/New_York". The others are
+ // in the filesystem so they give one timezone for the system;
+ // the values are similar (but cannot have a leading colon).
+ // (Note: the OS ZoneInfo database may be a different version from
+ // the one we use, so still need to check that DST dates match.)
+ osUserTimeZone = environmentVariableValue("TZ") ||
+ symbolicLinkTarget("/etc/localtime") ||
+ fileFirstZoneLineString("/etc/TIMEZONE") ||
+ fileFirstZoneLineString("/etc/timezone") ||
+ fileFirstZoneLineString("/etc/sysconfig/clock");
+ let results = osUserTimeZone.match(tzRegex);
+ if (results) {
+ zoneInfoIdFromOSUserTimeZone = results[1];
+ }
+ }
+
+ // check how well OS tz matches tz defined in our version of zoneinfo db
+ if (zoneInfoIdFromOSUserTimeZone != null) {
+ let tzId = zoneInfoIdFromOSUserTimeZone;
+ let score = checkTZ(tzId);
+ switch (score) {
+ case 0:
+ // Did not match.
+ // Maybe OS or Application is old, and the timezone changed.
+ // Or maybe user turned off DST in Date/Time control panel.
+ // Will look for a better matching tz, or fallback to floating.
+ // (Match OS so alarms go off at time indicated by OS clock.)
+ cal.WARN(calProperties.formatStringFromName(
+ "WarningOSTZNoMatch", [osUserTimeZone, zoneInfoIdFromOSUserTimeZone], 2));
+ break;
+ case 1: case 2:
+ // inexact match: OS TZ and our ZoneInfo TZ matched imperfectly.
+ // Will keep looking, will use tzId unless another is better.
+ // (maybe OS TZ has changed to match a nearby TZ, so maybe
+ // another ZoneInfo TZ matches it better).
+ probableTZId = tzId;
+ probableTZScore = score;
+ probableTZSource = calProperties.formatStringFromName(
+ "TZFromOS", [osUserTimeZone], 1);
+
+ break;
+ case 3:
+ // exact match
+ return tzId;
+ }
+ }
+ } catch (ex) {
+ // zoneInfo id given was not recognized by our ZoneInfo database
+ let errParams = [zoneInfoIdFromOSUserTimeZone || osUserTimeZone];
+ let errMsg = calProperties.formatStringFromName("SkippingOSTimezone", errParams, 1);
+ Components.utils.reportError(errMsg + " " + ex);
+ }
+
+ // Second, give priority to "likelyTimezone"s if provided by locale.
+ try {
+ // The likelyTimezone property is a comma-separated list of
+ // ZoneInfo timezone ids.
+ const bundleTZString =
+ calProperties.GetStringFromName("likelyTimezone");
+ const bundleTZIds = bundleTZString.split(/\s*,\s*/);
+ for (let bareTZId of bundleTZIds) {
+ let tzId = bareTZId;
+ try {
+ let score = checkTZ(tzId);
+
+ switch (score) {
+ case 0:
+ break;
+ case 1: case 2:
+ if (score > probableTZScore) {
+ probableTZId = tzId;
+ probableTZScore = score;
+ probableTZSource = calProperties.GetStringFromName("TZFromLocale");
+ }
+ break;
+ case 3:
+ return tzId;
+ }
+ } catch (ex) {
+ let errMsg = calProperties.formatStringFromName(
+ "SkippingLocaleTimezone", [bareTZId], 1);
+ Components.utils.reportError(errMsg + " " + ex);
+ }
+ }
+ } catch (ex) { // Oh well, this didn't work, next option...
+ Components.utils.reportError(ex);
+ }
+
+ // Third, try all known timezones.
+ const tzIDs = tzSvc.timezoneIds;
+ while (tzIDs.hasMore()) {
+ let tzId = tzIDs.getNext();
+ try {
+ let score = checkTZ(tzId);
+ switch (score) {
+ case 0: break;
+ case 1: case 2:
+ if (score > probableTZScore) {
+ probableTZId = tzId;
+ probableTZScore = score;
+ probableTZSource = calProperties.GetStringFromName("TZFromKnownTimezones");
+ }
+ break;
+ case 3:
+ return tzId;
+ }
+ } catch (ex) { // bug if ics service doesn't recognize own tzid!
+ let msg = "ics-service doesn't recognize own tzid: " + tzId + "\n" + ex;
+ Components.utils.reportError(msg);
+ }
+ }
+
+ // If reach here, there were no score=3 matches, so Warn in console.
+ try {
+ switch (probableTZScore) {
+ case 0: {
+ cal.WARN(calProperties.GetStringFromName("warningUsingFloatingTZNoMatch"));
+ break;
+ }
+ case 1:
+ case 2: {
+ let tzId = probableTZId;
+ let timezone = tzSvc.getTimezone(tzId);
+ let subComp = timezone.icalComponent;
+ let standard = findCurrentTimePeriod(timezone, subComp, "STANDARD");
+ let standardTZOffset = getIcalString(standard, "TZOFFSETTO");
+ let daylight = findCurrentTimePeriod(timezone, subComp, "DAYLIGHT");
+ let daylightTZOffset = getIcalString(daylight, "TZOFFSETTO");
+ let warningDetail;
+ if (probableTZScore == 1) {
+ // score 1 means has daylight time,
+ // but transitions start on different weekday from os timezone.
+ let standardStart = getIcalString(standard, "DTSTART");
+ let standardStartWeekday = weekday(standardStart, timezone);
+ let standardRule = getIcalString(standard, "RRULE");
+ let standardText =
+ " Standard: " + standardStart + " " + standardStartWeekday + "\n" +
+ " " + standardRule + "\n";
+ let daylightStart = getIcalString(daylight, "DTSTART");
+ let daylightStartWeekday = weekday(daylightStart, timezone);
+ let daylightRule = getIcalString(daylight, "RRULE");
+ let daylightText =
+ " Daylight: " + daylightStart + " " + daylightStartWeekday + "\n" +
+ " " + daylightRule + "\n";
+ warningDetail =
+ (standardStart < daylightStart
+ ? standardText + daylightText
+ : daylightText + standardText) +
+ calProperties.GetStringFromName("TZAlmostMatchesOSDifferAtMostAWeek");
+ } else {
+ warningDetail = calProperties.GetStringFromName("TZSeemsToMatchOS");
+ }
+ let offsetString = standardTZOffset +
+ (daylightTZOffset ? "/" + daylightTZOffset : "");
+ let warningMsg = calProperties.formatStringFromName("WarningUsingGuessedTZ",
+ [tzId, offsetString, warningDetail, probableTZSource], 4);
+ cal.WARN(warningMsg);
+ break;
+ }
+ }
+ } catch (ex) { // don't abort if error occurs warning user
+ Components.utils.reportError(ex);
+ }
+
+ // return the guessed timezone
+ return probableTZId;
+}
+
+this.NSGetFactory = cal.loadingNSGetFactory(["calTimezone.js"], [calTimezoneService], this);
diff --git a/calendar/base/src/calTimezoneService.manifest b/calendar/base/src/calTimezoneService.manifest
new file mode 100644
index 000000000..e8089ed2e
--- /dev/null
+++ b/calendar/base/src/calTimezoneService.manifest
@@ -0,0 +1,2 @@
+component {e736f2bd-7640-4715-ab35-887dc866c587} calTimezoneService.js
+contract @mozilla.org/calendar/timezone-service;1 {e736f2bd-7640-4715-ab35-887dc866c587}
diff --git a/calendar/base/src/calTodo.js b/calendar/base/src/calTodo.js
new file mode 100644
index 000000000..bb0fae782
--- /dev/null
+++ b/calendar/base/src/calTodo.js
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+//
+// constructor
+//
+function calTodo() {
+ this.initItemBase();
+
+ this.todoPromotedProps = {
+ DTSTART: true,
+ DTEND: true,
+ DUE: true,
+ COMPLETED: true,
+ __proto__: this.itemBasePromotedProps
+ };
+}
+
+var calTodoClassID = Components.ID("{7af51168-6abe-4a31-984d-6f8a3989212d}");
+var calTodoInterfaces = [
+ Components.interfaces.calIItemBase,
+ Components.interfaces.calITodo,
+ Components.interfaces.calIInternalShallowCopy
+];
+calTodo.prototype = {
+ __proto__: calItemBase.prototype,
+
+ classID: calTodoClassID,
+ QueryInterface: XPCOMUtils.generateQI(calTodoInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calTodoClassID,
+ contractID: "@mozilla.org/calendar/todo;1",
+ classDescription: "Calendar Todo",
+ interfaces: calTodoInterfaces,
+ }),
+
+ cloneShallow: function(aNewParent) {
+ let cloned = new calTodo();
+ this.cloneItemBaseInto(cloned, aNewParent);
+ return cloned;
+ },
+
+ createProxy: function(aRecurrenceId) {
+ cal.ASSERT(!this.mIsProxy, "Tried to create a proxy for an existing proxy!", true);
+
+ let proxy = new calTodo();
+
+ // override proxy's DTSTART/DUE/RECURRENCE-ID
+ // before master is set (and item might get immutable):
+ let duration = this.duration;
+ if (duration) {
+ let dueDate = aRecurrenceId.clone();
+ dueDate.addDuration(duration);
+ proxy.dueDate = dueDate;
+ }
+ proxy.entryDate = aRecurrenceId;
+
+ proxy.initializeProxy(this, aRecurrenceId);
+ proxy.mDirty = false;
+
+ return proxy;
+ },
+
+ makeImmutable: function() {
+ this.makeItemBaseImmutable();
+ },
+
+ get isCompleted() {
+ return this.completedDate != null ||
+ this.percentComplete == 100 ||
+ this.status == "COMPLETED";
+ },
+
+ set isCompleted(completed) {
+ if (completed) {
+ if (!this.completedDate) {
+ this.completedDate = cal.jsDateToDateTime(new Date());
+ }
+ this.status = "COMPLETED";
+ this.percentComplete = 100;
+ } else {
+ this.deleteProperty("COMPLETED");
+ this.deleteProperty("STATUS");
+ this.deleteProperty("PERCENT-COMPLETE");
+ }
+ },
+
+ get duration() {
+ let dur = this.getProperty("DURATION");
+ // pick up duration if available, otherwise calculate difference
+ // between start and enddate
+ if (dur) {
+ return cal.createDuration(dur);
+ } else {
+ if (!this.entryDate || !this.dueDate) {
+ return null;
+ }
+ return this.dueDate.subtractDate(this.entryDate);
+ }
+ },
+
+ set duration(value) {
+ this.setProperty("DURATION", value);
+ },
+
+ get recurrenceStartDate() {
+ // DTSTART is optional for VTODOs, so it's unclear if RRULE is allowed then,
+ // so fallback to DUE if no DTSTART is present:
+ return this.entryDate || this.dueDate;
+ },
+
+ icsEventPropMap: [
+ { cal: "DTSTART", ics: "startTime" },
+ { cal: "DUE", ics: "dueTime" },
+ { cal: "COMPLETED", ics: "completedTime" }],
+
+ set icalString(value) {
+ this.icalComponent = getIcsService().parseICS(value, null);
+ },
+
+ get icalString() {
+ let calcomp = getIcsService().createIcalComponent("VCALENDAR");
+ calSetProdidVersion(calcomp);
+ calcomp.addSubcomponent(this.icalComponent);
+ return calcomp.serializeToICS();
+ },
+
+ get icalComponent() {
+ let icssvc = getIcsService();
+ let icalcomp = icssvc.createIcalComponent("VTODO");
+ this.fillIcalComponentFromBase(icalcomp);
+ this.mapPropsToICS(icalcomp, this.icsEventPropMap);
+
+ let bagenum = this.propertyEnumerator;
+ while (bagenum.hasMoreElements()) {
+ let iprop = bagenum.getNext()
+ .QueryInterface(Components.interfaces.nsIProperty);
+ try {
+ if (!this.todoPromotedProps[iprop.name]) {
+ let icalprop = icssvc.createIcalProperty(iprop.name);
+ icalprop.value = iprop.value;
+ let propBucket = this.mPropertyParams[iprop.name];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName, propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid todo parameter value " + paramName + "=" + propBucket[paramName]);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ icalcomp.addProperty(icalprop);
+ }
+ } catch (e) {
+ cal.ERROR("failed to set " + iprop.name + " to " + iprop.value + ": " + e + "\n");
+ }
+ }
+ return icalcomp;
+ },
+
+ todoPromotedProps: null,
+
+ set icalComponent(todo) {
+ this.modify();
+ if (todo.componentType != "VTODO") {
+ todo = todo.getFirstSubcomponent("VTODO");
+ if (!todo) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ this.mDueDate = undefined;
+ this.setItemBaseFromICS(todo);
+ this.mapPropsFromICS(todo, this.icsEventPropMap);
+
+ this.importUnpromotedProperties(todo, this.todoPromotedProps);
+ // Importing didn't really change anything
+ this.mDirty = false;
+ },
+
+ isPropertyPromoted: function(name) {
+ // avoid strict undefined property warning
+ return this.todoPromotedProps[name] || false;
+ },
+
+ set entryDate(value) {
+ this.modify();
+
+ // We're about to change the start date of an item which probably
+ // could break the associated calIRecurrenceInfo. We're calling
+ // the appropriate method here to adjust the internal structure in
+ // order to free clients from worrying about such details.
+ if (this.parentItem == this) {
+ let rec = this.recurrenceInfo;
+ if (rec) {
+ rec.onStartDateChange(value, this.entryDate);
+ }
+ }
+
+ return this.setProperty("DTSTART", value);
+ },
+
+ get entryDate() {
+ return this.getProperty("DTSTART");
+ },
+
+ mDueDate: undefined,
+ get dueDate() {
+ let dueDate = this.mDueDate;
+ if (dueDate === undefined) {
+ dueDate = this.getProperty("DUE");
+ if (!dueDate) {
+ let entryDate = this.entryDate;
+ let dur = this.getProperty("DURATION");
+ if (entryDate && dur) {
+ // If there is a duration set on the todo, calculate the right end time.
+ dueDate = entryDate.clone();
+ dueDate.addDuration(cal.createDuration(dur));
+ }
+ }
+ this.mDueDate = dueDate;
+ }
+ return dueDate;
+ },
+
+ set dueDate(value) {
+ this.deleteProperty("DURATION"); // setting dueDate once removes DURATION
+ this.setProperty("DUE", value);
+ return (this.mDueDate = value);
+ }
+};
+
+// var decl to prevent spurious error messages when loaded as component
+
+var makeMemberAttr;
+if (makeMemberAttr) {
+ makeMemberAttr(calTodo, "COMPLETED", null, "completedDate", true);
+ makeMemberAttr(calTodo, "PERCENT-COMPLETE", 0, "percentComplete", true);
+}
diff --git a/calendar/base/src/calTransactionManager.js b/calendar/base/src/calTransactionManager.js
new file mode 100644
index 000000000..e76eb2269
--- /dev/null
+++ b/calendar/base/src/calTransactionManager.js
@@ -0,0 +1,215 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calItipUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function calTransactionManager() {
+ this.wrappedJSObject = this;
+ if (!this.transactionManager) {
+ this.transactionManager =
+ Components.classes["@mozilla.org/transactionmanager;1"]
+ .createInstance(Components.interfaces.nsITransactionManager);
+ }
+}
+
+var calTransactionManagerClassID = Components.ID("{40a1ccf4-5f54-4815-b842-abf06f84dbfd}");
+var calTransactionManagerInterfaces = [Components.interfaces.calITransactionManager];
+calTransactionManager.prototype = {
+
+ classID: calTransactionManagerClassID,
+ QueryInterface: XPCOMUtils.generateQI(calTransactionManagerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calTransactionManagerClassID,
+ classDescription: "Calendar Transaction Manager",
+ contractID: "mozilla.org/calendar/transactionmanager;1",
+ interfaces: calTransactionManagerInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ transactionManager: null,
+ createAndCommitTxn: function(aAction, aItem, aCalendar, aOldItem, aListener) {
+ let txn = new calTransaction(aAction,
+ aItem,
+ aCalendar,
+ aOldItem,
+ aListener);
+ this.transactionManager.doTransaction(txn);
+ },
+
+ beginBatch: function() {
+ this.transactionManager.beginBatch(null);
+ },
+
+ endBatch: function() {
+ this.transactionManager.endBatch(false);
+ },
+
+ checkWritable: function(transaction) {
+ function checkItem(item) {
+ return item && item.calendar &&
+ isCalendarWritable(item.calendar) &&
+ userCanAddItemsToCalendar(item.calendar);
+ }
+
+ let trans = transaction && transaction.wrappedJSObject;
+ return trans && checkItem(trans.mItem) && checkItem(trans.mOldItem);
+ },
+
+ undo: function() {
+ this.transactionManager.undoTransaction();
+ },
+
+ canUndo: function() {
+ return this.transactionManager.numberOfUndoItems > 0 &&
+ this.checkWritable(this.transactionManager.peekUndoStack());
+ },
+
+ redo: function() {
+ this.transactionManager.redoTransaction();
+ },
+
+ canRedo: function() {
+ return this.transactionManager.numberOfRedoItems > 0 &&
+ this.checkWritable(this.transactionManager.peekRedoStack());
+ }
+};
+
+function calTransaction(aAction, aItem, aCalendar, aOldItem, aListener) {
+ this.wrappedJSObject = this;
+ this.mAction = aAction;
+ this.mItem = aItem;
+ this.mCalendar = aCalendar;
+ this.mOldItem = aOldItem;
+ this.mListener = aListener;
+}
+
+var calTransactionClassID = Components.ID("{fcb54c82-2fb9-42cb-bf44-1e197a55e520}");
+var calTransactionInterfaces = [
+ Components.interfaces.nsITransaction,
+ Components.interfaces.calIOperationListener
+];
+calTransaction.prototype = {
+ classID: calTransactionClassID,
+ QueryInterface: XPCOMUtils.generateQI(calTransactionInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calTransactionClassID,
+ classDescription: "Calendar Transaction",
+ contractID: "mozilla.org/calendar/transaction;1",
+ interfaces: calTransactionInterfaces,
+ }),
+
+ mAction: null,
+ mCalendar: null,
+ mItem: null,
+ mOldItem: null,
+ mOldCalendar: null,
+ mListener: null,
+ mIsDoTransaction: false,
+
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ if (Components.isSuccessCode(aStatus)) {
+ cal.itip.checkAndSend(aOperationType,
+ aDetail,
+ this.mIsDoTransaction ? this.mOldItem : this.mItem);
+
+ if (aOperationType == Components.interfaces.calIOperationListener.ADD ||
+ aOperationType == Components.interfaces.calIOperationListener.MODIFY) {
+ if (this.mIsDoTransaction) {
+ this.mItem = aDetail;
+ } else {
+ this.mOldItem = aDetail;
+ }
+ }
+ }
+ if (this.mListener) {
+ this.mListener.onOperationComplete(aCalendar,
+ aStatus,
+ aOperationType,
+ aId,
+ aDetail);
+ }
+ },
+
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (this.mListener) {
+ this.mListener.onGetResult(aCalendar,
+ aStatus,
+ aItemType,
+ aDetail,
+ aCount,
+ aItems);
+ }
+ },
+
+ doTransaction: function() {
+ this.mIsDoTransaction = true;
+ switch (this.mAction) {
+ case "add":
+ this.mCalendar.addItem(this.mItem, this);
+ break;
+ case "modify":
+ if (this.mItem.calendar.id == this.mOldItem.calendar.id) {
+ this.mCalendar.modifyItem(cal.itip.prepareSequence(this.mItem, this.mOldItem),
+ this.mOldItem,
+ this);
+ } else {
+ let self = this;
+ let addListener = {
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ self.onOperationComplete(...arguments);
+ if (Components.isSuccessCode(aStatus)) {
+ self.mOldItem.calendar.deleteItem(self.mOldItem, self);
+ }
+ }
+ };
+
+ this.mOldCalendar = this.mOldItem.calendar;
+ this.mCalendar.addItem(this.mItem, addListener);
+ }
+ break;
+ case "delete":
+ this.mCalendar.deleteItem(this.mItem, this);
+ break;
+ default:
+ throw new Components.Exception("Invalid action specified",
+ Components.results.NS_ERROR_ILLEGAL_VALUE);
+ }
+ },
+
+ undoTransaction: function() {
+ this.mIsDoTransaction = false;
+ switch (this.mAction) {
+ case "add":
+ this.mCalendar.deleteItem(this.mItem, this);
+ break;
+ case "modify":
+ if (this.mOldItem.calendar.id == this.mItem.calendar.id) {
+ this.mCalendar.modifyItem(cal.itip.prepareSequence(this.mOldItem, this.mItem),
+ this.mItem, this);
+ } else {
+ this.mCalendar.deleteItem(this.mItem, this);
+ this.mOldCalendar.addItem(this.mOldItem, this);
+ }
+ break;
+ case "delete":
+ this.mCalendar.addItem(this.mItem, this);
+ break;
+ default:
+ throw new Components.Exception("Invalid action specified",
+ Components.results.NS_ERROR_ILLEGAL_VALUE);
+ }
+ },
+
+ redoTransaction: function() {
+ this.doTransaction();
+ },
+
+ isTransient: false,
+
+ merge: function(aTransaction) {
+ // No support for merging
+ return false;
+ }
+};
diff --git a/calendar/base/src/calUtils.js b/calendar/base/src/calUtils.js
new file mode 100644
index 000000000..bc150c371
--- /dev/null
+++ b/calendar/base/src/calUtils.js
@@ -0,0 +1,1914 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This file contains commonly used functions in a centralized place so that
+ * various components (and other js scopes) don't need to replicate them. Note
+ * that loading this file twice in the same scope will throw errors.
+ */
+
+
+/* exported createEvent, createTodo, createDateTime, createDuration, createAttendee,
+ * createAttachment, createAlarm, createRelation,
+ * createRecurrenceDate, createRecurrenceRule, createRecurrenceInfo,
+ * getCalendarManager, getIcsService, getCalendarSearchService,
+ * getFreeBusyService, getWeekInfoService, getDateFormatter, UTC,
+ * floating, saveRecentTimezone, getCalendarDirectory,
+ * isCalendarWritable, userCanAddItemsToCalendar,
+ * userCanDeleteItemsFromCalendar, attendeeMatchesAddresses,
+ * userCanRespondToInvitation, openCalendarWizard,
+ * openCalendarProperties, calPrint, makeURL, calRadioGroupSelectItem,
+ * isItemSupported, calInstanceOf, getPrefSafe, setPref,
+ * setLocalizedPref, getLocalizedPref, getPrefCategoriesArray,
+ * setPrefCategoriesFromArray, compareItems, calTryWrappedJSObject,
+ * compareArrays, doQueryInterface, setDefaultStartEndHour, LOG, WARN,
+ * ERROR, showError, getContrastingTextColor, calGetEndDateProp,
+ * checkIfInRange, getProgressAtom, sendMailTo, sameDay,
+ * calSetProdidVersion, applyAttributeToMenuChildren,
+ * isPropertyValueSame, getParentNodeOrThis,
+ * getParentNodeOrThisByAttribute, setItemProperty,
+ * calIterateEmailIdentities, compareItemContent, binaryInsert,
+ * getCompositeCalendar, findItemWindow
+ */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+function _calIcalCreator(cid, iid) {
+ return function(icalString) {
+ let thing = Components.classes[cid].createInstance(iid);
+ if (icalString) {
+ thing.icalString = icalString;
+ }
+ return thing;
+ };
+}
+
+var createEvent = _calIcalCreator("@mozilla.org/calendar/event;1",
+ Components.interfaces.calIEvent);
+var createTodo = _calIcalCreator("@mozilla.org/calendar/todo;1",
+ Components.interfaces.calITodo);
+var createDateTime = _calIcalCreator("@mozilla.org/calendar/datetime;1",
+ Components.interfaces.calIDateTime);
+var createDuration = _calIcalCreator("@mozilla.org/calendar/duration;1",
+ Components.interfaces.calIDuration);
+var createAttendee = _calIcalCreator("@mozilla.org/calendar/attendee;1",
+ Components.interfaces.calIAttendee);
+var createAttachment = _calIcalCreator("@mozilla.org/calendar/attachment;1",
+ Components.interfaces.calIAttachment);
+var createAlarm = _calIcalCreator("@mozilla.org/calendar/alarm;1",
+ Components.interfaces.calIAlarm);
+var createRelation = _calIcalCreator("@mozilla.org/calendar/relation;1",
+ Components.interfaces.calIRelation);
+var createRecurrenceDate = _calIcalCreator("@mozilla.org/calendar/recurrence-date;1",
+ Components.interfaces.calIRecurrenceDate);
+var createRecurrenceRule = _calIcalCreator("@mozilla.org/calendar/recurrence-rule;1",
+ Components.interfaces.calIRecurrenceRule);
+
+/* Returns a clean new calIRecurrenceInfo */
+function createRecurrenceInfo(aItem) {
+ let recInfo = Components.classes["@mozilla.org/calendar/recurrence-info;1"]
+ .createInstance(Components.interfaces.calIRecurrenceInfo);
+ recInfo.item = aItem;
+ return recInfo;
+}
+
+/* Shortcut to the calendar-manager service */
+function getCalendarManager() {
+ return Components.classes["@mozilla.org/calendar/manager;1"]
+ .getService(Components.interfaces.calICalendarManager);
+}
+
+/* Shortcut to the ICS service */
+function getIcsService() {
+ return Components.classes["@mozilla.org/calendar/ics-service;1"]
+ .getService(Components.interfaces.calIICSService);
+}
+
+/* Shortcut to the timezone service */
+function getTimezoneService() {
+ return Components.classes["@mozilla.org/calendar/timezone-service;1"]
+ .getService(Components.interfaces.calITimezoneService);
+}
+
+/* Shortcut to calendar search service */
+function getCalendarSearchService() {
+ return Components.classes["@mozilla.org/calendar/calendarsearch-service;1"]
+ .getService(Components.interfaces.calICalendarSearchProvider);
+}
+
+/* Shortcut to the freebusy service */
+function getFreeBusyService() {
+ return Components.classes["@mozilla.org/calendar/freebusy-service;1"]
+ .getService(Components.interfaces.calIFreeBusyService);
+}
+
+/* Shortcut to week info service */
+function getWeekInfoService() {
+ return Components.classes["@mozilla.org/calendar/weekinfo-service;1"]
+ .getService(Components.interfaces.calIWeekInfoService);
+}
+
+/* Shortcut to date formatter service */
+function getDateFormatter() {
+ return Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
+ .getService(Components.interfaces.calIDateTimeFormatter);
+}
+
+// @return the UTC timezone.
+function UTC() {
+ if (UTC.mObject === undefined) {
+ UTC.mObject = getTimezoneService().UTC;
+ }
+ return UTC.mObject;
+}
+
+// @return the floating timezone.
+function floating() {
+ if (floating.mObject === undefined) {
+ floating.mObject = getTimezoneService().floating;
+ }
+ return floating.mObject;
+}
+
+/**
+ * Function to get the best guess at a user's default timezone.
+ *
+ * @return user's default timezone.
+ */
+function calendarDefaultTimezone() {
+ return getTimezoneService().defaultTimezone;
+}
+
+/**
+ * Makes sure the given timezone id is part of the list of recent timezones.
+ *
+ * @param aTzid The timezone id to add
+ */
+function saveRecentTimezone(aTzid) {
+ let recentTimezones = getRecentTimezones();
+ const MAX_RECENT_TIMEZONES = 5; // We don't need a pref for *everything*.
+
+ if (aTzid != calendarDefaultTimezone().tzid &&
+ !recentTimezones.includes(aTzid)) {
+ // Add the timezone if its not already the default timezone
+ recentTimezones.unshift(aTzid);
+ recentTimezones.splice(MAX_RECENT_TIMEZONES);
+ Preferences.set("calendar.timezone.recent", JSON.stringify(recentTimezones));
+ }
+}
+
+/**
+ * Gets the list of recent timezones. Optionally retuns the list as
+ * calITimezones.
+ *
+ * @param aConvertZones (optional) If true, return calITimezones instead
+ * @return An array of timezone ids or calITimezones.
+ */
+function getRecentTimezones(aConvertZones) {
+ let recentTimezones = JSON.parse(Preferences.get("calendar.timezone.recent", "[]") || "[]");
+ if (!Array.isArray(recentTimezones)) {
+ recentTimezones = [];
+ }
+
+ let tzService = cal.getTimezoneService();
+ if (aConvertZones) {
+ let oldZonesLength = recentTimezones.length;
+ for (let i = 0; i < recentTimezones.length; i++) {
+ let timezone = tzService.getTimezone(recentTimezones[i]);
+ if (timezone) {
+ // Replace id with found timezone
+ recentTimezones[i] = timezone;
+ } else {
+ // Looks like the timezone doesn't longer exist, remove it
+ recentTimezones.splice(i, 1);
+ i--;
+ }
+ }
+
+ if (oldZonesLength != recentTimezones.length) {
+ // Looks like the one or other timezone dropped out. Go ahead and
+ // modify the pref.
+ Preferences.set("calendar.timezone.recent", JSON.stringify(recentTimezones));
+ }
+ }
+ return recentTimezones;
+}
+
+/**
+ * Format the given string to work inside a CSS rule selector
+ * (and as part of a non-unicode preference key).
+ *
+ * Replaces each space ' ' char with '_'.
+ * Replaces each char other than ascii digits and letters, with '-uxHHH-'
+ * where HHH is unicode in hexadecimal (variable length, terminated by the '-').
+ *
+ * Ensures: result only contains ascii digits, letters,'-', and '_'.
+ * Ensures: result is invertible, so (f(a) = f(b)) implies (a = b).
+ * also means f is not idempotent, so (a != f(a)) implies (f(a) != f(f(a))).
+ * Ensures: result must be lowercase.
+ * Rationale: preference keys require 8bit chars, and ascii chars are legible
+ * in most fonts (in case user edits PROFILE/prefs.js).
+ * CSS class names in Gecko 1.8 seem to require lowercase,
+ * no punctuation, and of course no spaces.
+ * nmchar [_a-zA-Z0-9-]|{nonascii}|{escape}
+ * name {nmchar}+
+ * http://www.w3.org/TR/CSS21/grammar.html#scanner
+ *
+ * @param aString The unicode string to format
+ * @return The formatted string using only chars [_a-zA-Z0-9-]
+ */
+function formatStringForCSSRule(aString) {
+ function toReplacement(char) {
+ // char code is natural number (positive integer)
+ let nat = char.charCodeAt(0);
+ switch (nat) {
+ case 0x20: // space
+ return "_";
+ default:
+ return "-ux" + nat.toString(16) + "-"; // lowercase
+ }
+ }
+ // Result must be lowercase or style rule will not work.
+ return aString.toLowerCase().replace(/[^a-zA-Z0-9]/g, toReplacement);
+}
+
+/**
+ * Shared dialog functions
+ * Gets the calendar directory, defaults to <profile-dir>/calendar
+ */
+function getCalendarDirectory() {
+ if (getCalendarDirectory.mDir === undefined) {
+ let dir = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile);
+ dir.append("calendar-data");
+ if (!dir.exists()) {
+ try {
+ dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
+ parseInt("0700", 8));
+ } catch (exc) {
+ ASSERT(false, exc);
+ throw exc;
+ }
+ }
+ getCalendarDirectory.mDir = dir;
+ }
+ return getCalendarDirectory.mDir.clone();
+}
+
+/**
+ * Check if the specified calendar is writable. This is the case when it is not
+ * marked readOnly, we are not offline, or we are offline and the calendar is
+ * local.
+ *
+ * @param aCalendar The calendar to check
+ * @return True if the calendar is writable
+ */
+function isCalendarWritable(aCalendar) {
+ return !aCalendar.getProperty("disabled") &&
+ !aCalendar.readOnly &&
+ (!Services.io.offline ||
+ aCalendar.getProperty("cache.enabled") ||
+ aCalendar.getProperty("cache.always") ||
+ aCalendar.getProperty("requiresNetwork") === false);
+}
+
+/**
+ * Check if the specified calendar is writable from an ACL point of view.
+ *
+ * @param aCalendar The calendar to check
+ * @return True if the calendar is writable
+ */
+function userCanAddItemsToCalendar(aCalendar) {
+ let aclEntry = aCalendar.aclEntry;
+ return !aclEntry || !aclEntry.hasAccessControl || aclEntry.userIsOwner || aclEntry.userCanAddItems;
+}
+
+/**
+ * Check if the user can delete items from the specified calendar, from an ACL point of view.
+ *
+ * @param aCalendar The calendar to check
+ * @return True if the calendar is writable
+ */
+function userCanDeleteItemsFromCalendar(aCalendar) {
+ let aclEntry = aCalendar.aclEntry;
+ return !aclEntry || !aclEntry.hasAccessControl || aclEntry.userIsOwner || aclEntry.userCanDeleteItems;
+}
+
+/**
+ * Check if the user can fully modify the specified item, from an ACL point of view.
+ * Note to be confused with the right to respond to an invitation, which is
+ * handled instead by userCanRespondToInvitation.
+ *
+ * @param aItem The calendar item to check
+ * @return True if the item is modifiable
+ */
+function userCanModifyItem(aItem) {
+ let aclEntry = aItem.aclEntry;
+ return !aclEntry || !aclEntry.calendarEntry.hasAccessControl || aclEntry.calendarEntry.userIsOwner || aclEntry.userCanModify;
+}
+
+/**
+ * Check if the attendee object matches one of the addresses in the list. This
+ * is useful to determine whether the current user acts as a delegate.
+ *
+ * @param aAttendee The reference attendee object
+ * @param addresses The list of addresses
+ * @return True if there is a match
+ */
+function attendeeMatchesAddresses(anAttendee, addresses) {
+ let attId = anAttendee.id;
+ if (!attId.match(/^mailto:/i)) {
+ // Looks like its not a normal attendee, possibly urn:uuid:...
+ // Try getting the email through the EMAIL property.
+ let emailProp = anAttendee.getProperty("EMAIL");
+ if (emailProp) {
+ attId = emailProp;
+ }
+ }
+
+ attId = attId.toLowerCase().replace(/^mailto:/, "");
+ for (let address of addresses) {
+ if (attId == address.toLowerCase().replace(/^mailto:/, "")) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Check if the user can fully modify the specified item, from an ACL point of view.
+ * Note to be confused with the right to respond to an invitation, which is
+ * handled instead by userCanRespondToInvitation.
+ *
+ * @param aItem The calendar item to check
+ * @return True if the item is modifiable
+ */
+function userCanRespondToInvitation(aItem) {
+ let aclEntry = aItem.aclEntry;
+ return userCanModifyItem(aItem) || aclEntry.userCanRespond;
+}
+
+/**
+ * Opens the Create Calendar wizard
+ *
+ * @param aCallback a function to be performed after calendar creation
+ */
+function openCalendarWizard(aCallback) {
+ openDialog("chrome://calendar/content/calendarCreation.xul", "caEditServer",
+ // Workaround for Bug 1151440 - the HTML color picker won't work
+ // in linux when opened from modal dialog
+ AppConstants.platform == "linux"
+ ? "chrome,titlebar,resizable"
+ : "modal,chrome,titlebar,resizable",
+ aCallback);
+}
+
+/**
+ * Opens the calendar properties window for aCalendar
+ *
+ * @param aCalendar the calendar whose properties should be displayed
+ */
+function openCalendarProperties(aCalendar) {
+ openDialog("chrome://calendar/content/calendar-properties-dialog.xul",
+ "CalendarPropertiesDialog",
+ // Workaround for Bug 1151440 - the HTML color picker won't work
+ // in linux when opened from modal dialog
+ AppConstants.platform == "linux"
+ ? "chrome,titlebar,resizable"
+ : "modal,chrome,titlebar,resizable",
+ { calendar: aCalendar });
+}
+
+/**
+ * Opens the print dialog
+ */
+function calPrint() {
+ openDialog("chrome://calendar/content/calendar-print-dialog.xul", "Print",
+ "centerscreen,chrome,resizable");
+}
+
+/**
+ * Other functions
+ */
+
+/**
+ * Takes a string and returns an nsIURI
+ *
+ * @param aUriString the string of the address to for the spec of the nsIURI
+ *
+ * @returns an nsIURI whose spec is aUriString
+ */
+function makeURL(aUriString) {
+ return Services.io.newURI(aUriString, null, null);
+}
+
+/**
+ * Returns a calIDateTime that corresponds to the current time in the user's
+ * default timezone.
+ */
+function now() {
+ let date = cal.jsDateToDateTime(new Date());
+ return date.getInTimezone(calendarDefaultTimezone());
+}
+
+/**
+ * Selects an item with id aItemId in the radio group with id aRadioGroupId
+ *
+ * @param aRadioGroupId the id of the radio group which contains the item
+ * @param aItemId the item to be selected
+ */
+function calRadioGroupSelectItem(aRadioGroupId, aItemId) {
+ let radioGroup = document.getElementById(aRadioGroupId);
+ let items = radioGroup.getElementsByTagName("radio");
+ let index;
+ for (let i in items) {
+ if (items[i].getAttribute("id") == aItemId) {
+ index = i;
+ break;
+ }
+ }
+ ASSERT(index && index != 0, "Can't find radioGroup item to select.", true);
+ radioGroup.selectedIndex = index;
+}
+
+
+/** checks if an item is supported by a Calendar
+* @param aCalendar the calendar
+* @param aItem the item either a task or an event
+* @return true or false
+*/
+function isItemSupported(aItem, aCalendar) {
+ if (isToDo(aItem)) {
+ return (aCalendar.getProperty("capabilities.tasks.supported") !== false);
+ } else if (isEvent(aItem)) {
+ return (aCalendar.getProperty("capabilities.events.supported") !== false);
+ }
+ return false;
+}
+
+/**
+ * @deprecated This function has been replaced by cal.wrapInstance()
+ */
+function calInstanceOf(aObject, aInterface) {
+ if (!calInstanceOf.warningIssued) {
+ cal.WARN("Use of calInstanceOf() is deprecated and will be removed " +
+ "with the next release. Use cal.wrapInstance() instead.\n" +
+ cal.STACK(10));
+ calInstanceOf.warningIssued = true;
+ }
+ return (cal.wrapInstance(aObject, aInterface) != null);
+}
+
+/**
+ * Determines whether or not the aObject is a calIEvent
+ *
+ * @param aObject the object to test
+ * @returns true if the object is a calIEvent, false otherwise
+ */
+function isEvent(aObject) {
+ return (cal.wrapInstance(aObject, Components.interfaces.calIEvent) != null);
+}
+
+/**
+ * Determines whether or not the aObject is a calITodo
+ *
+ * @param aObject the object to test
+ * @returns true if the object is a calITodo, false otherwise
+ */
+function isToDo(aObject) {
+ return (cal.wrapInstance(aObject, Components.interfaces.calITodo) != null);
+}
+
+/**
+ * Normal get*Pref calls will throw if the pref is undefined. This function
+ * will get a bool, int, or string pref. If the pref is undefined, it will
+ * return aDefault.
+ *
+ * @param aPrefName the (full) name of preference to get
+ * @param aDefault (optional) the value to return if the pref is undefined
+ */
+function getPrefSafe(aPrefName, aDefault) {
+ if (!getPrefSafe.warningIssued) {
+ cal.WARN("Use of getPrefSafe() is deprecated and will be removed " +
+ "with the next release. Use Preferences.get() instead.\n" +
+ cal.STACK(10));
+ getPrefSafe.warningIssued = true;
+ }
+
+ return Preferences.get(aPrefName, aDefault);
+}
+
+/**
+ * Wrapper for setting prefs of various types.
+ *
+ * @param aPrefName the (full) name of preference to set
+ * @param aPrefValue the value to set the pref to
+ * @param aPrefType (optional) the type of preference to set.
+ * Valid values are: BOOL, INT, and CHAR
+ */
+function setPref(aPrefName, aPrefValue, aPrefType) {
+ if (!setPref.warningIssued) {
+ cal.WARN("Use of setPref() is deprecated and will be removed " +
+ "with the next release. Use Preferences.set() instead.\n" +
+ cal.STACK(10));
+ setPref.warningIssued = true;
+ }
+
+ let prefValue = aPrefValue;
+
+ if (aPrefType == "BOOL") {
+ prefValue = Boolean(prefValue);
+ } else if (aPrefType == "INT") {
+ prefValue = Number(prefValue);
+ } else if (aPrefType == "CHAR") {
+ prefValue = String(prefValue);
+ }
+
+ return Preferences.set(aPrefName, prefValue);
+}
+
+/**
+ * Helper function to set a localized (complex) pref from a given string
+ *
+ * @param aPrefName the (full) name of preference to set
+ * @param aString the string to which the preference value should be set
+ */
+function setLocalizedPref(aPrefName, aString) {
+ if (!setLocalizedPref.warningIssued) {
+ cal.WARN("Use of setLocalizedPref() is deprecated and will be removed " +
+ "with the next release. Use Preferences.set() instead.\n" +
+ cal.STACK(10));
+ setLocalizedPref.warningIssued = true;
+ }
+
+ return Preferences.set(aPrefName, aString);
+}
+
+/**
+ * Like getPrefSafe, except for complex prefs (those used for localized data).
+ *
+ * @param aPrefName the (full) name of preference to get
+ * @param aDefault (optional) the value to return if the pref is undefined
+ */
+function getLocalizedPref(aPrefName, aDefault) {
+ if (!getLocalizedPref.warningIssued) {
+ cal.WARN("Use of getLocalizedPref() is deprecated and will be removed " +
+ "with the next release. Use Preferences.get() instead.\n" +
+ cal.STACK(10));
+ getLocalizedPref.warningIssued = true;
+ }
+
+ return Preferences.get(aPrefName, aDefault);
+}
+
+/**
+ * Get array of category names from preferences or locale default,
+ * unescaping any commas in each category name.
+ * @return array of category names
+ */
+function getPrefCategoriesArray() {
+ let categories = Preferences.get("calendar.categories.names", null);
+
+ // If no categories are configured load a default set from properties file
+ if (!categories) {
+ categories = setupDefaultCategories();
+ }
+ return categoriesStringToArray(categories);
+}
+
+/**
+ * Sets up the default categories from the localized string
+ *
+ * @return The default set of categories as a comma separated string.
+ */
+function setupDefaultCategories() {
+ // First, set up the category names
+ let categories = calGetString("categories", "categories2");
+ Preferences.set("calendar.categories.names", categories);
+
+ // Now, initialize the category default colors
+ let categoryArray = categoriesStringToArray(categories);
+ for (let category of categoryArray) {
+ let prefName = formatStringForCSSRule(category);
+ Preferences.set("calendar.category.color." + prefName,
+ hashColor(category));
+ }
+
+ // Return the list of categories for further processing
+ return categories;
+}
+
+/**
+ * Hash the given string into a color from the color palette of the standard
+ * color picker.
+ *
+ * @param str The string to hash into a color.
+ * @return The hashed color.
+ */
+function hashColor(str) {
+ // This is the palette of colors in the current colorpicker implementation.
+ // Unfortunately, there is no easy way to extract these colors from the
+ // binding directly.
+ const colorPalette = ["#FFFFFF", "#FFCCCC", "#FFCC99", "#FFFF99", "#FFFFCC",
+ "#99FF99", "#99FFFF", "#CCFFFF", "#CCCCFF", "#FFCCFF",
+ "#CCCCCC", "#FF6666", "#FF9966", "#FFFF66", "#FFFF33",
+ "#66FF99", "#33FFFF", "#66FFFF", "#9999FF", "#FF99FF",
+ "#C0C0C0", "#FF0000", "#FF9900", "#FFCC66", "#FFFF00",
+ "#33FF33", "#66CCCC", "#33CCFF", "#6666CC", "#CC66CC",
+ "#999999", "#CC0000", "#FF6600", "#FFCC33", "#FFCC00",
+ "#33CC00", "#00CCCC", "#3366FF", "#6633FF", "#CC33CC",
+ "#666666", "#990000", "#CC6600", "#CC9933", "#999900",
+ "#009900", "#339999", "#3333FF", "#6600CC", "#993399",
+ "#333333", "#660000", "#993300", "#996633", "#666600",
+ "#006600", "#336666", "#000099", "#333399", "#663366",
+ "#000000", "#330000", "#663300", "#663333", "#333300",
+ "#003300", "#003333", "#000066", "#330099", "#330033"];
+
+ let sum = Array.map(str || " ", e => e.charCodeAt(0)).reduce((a, b) => a + b);
+ return colorPalette[sum % colorPalette.length];
+}
+
+/**
+ * Convert categories string to list of category names.
+ *
+ * Stored categories may include escaped commas within a name.
+ * Split categories string at commas, but not at escaped commas (\,).
+ * Afterward, replace escaped commas (\,) with commas (,) in each name.
+ * @param aCategoriesPrefValue string from "calendar.categories.names" pref,
+ * which may contain escaped commas (\,) in names.
+ * @return list of category names
+ */
+function categoriesStringToArray(aCategories) {
+ if (!aCategories) {
+ return [];
+ }
+ // \u001A is the unicode "SUBSTITUTE" character
+ function revertCommas(name) { return name.replace(/\u001A/g, ","); }
+ let categories = aCategories.replace(/\\,/g, "\u001A").split(",").map(revertCommas);
+ if (categories.length == 1 && categories[0] == "") {
+ // Split will return an array with an empty element when splitting an
+ // empty string, correct this.
+ categories.pop();
+ }
+ return categories;
+}
+
+/**
+ * Set categories preference, escaping any commas in category names.
+ * @param aCategoriesArray array of category names,
+ * may contain unescaped commas which will be escaped in combined pref.
+ */
+function setPrefCategoriesFromArray(aCategoriesArray) {
+ Preferences.set("calendar.categories.names",
+ categoriesArrayToString(aCategoriesList));
+}
+
+/**
+ * Convert array of category names to string.
+ *
+ * Category names may contain commas (,). Escape commas (\,) in each,
+ * then join them in comma separated string for storage.
+ * @param aSortedCategoriesArray sorted array of category names,
+ * may contain unescaped commas, which will be escaped in combined string.
+ */
+function categoriesArrayToString(aSortedCategoriesArray) {
+ function escapeComma(category) { return category.replace(/,/g, "\\,"); }
+ return aSortedCategoriesArray.map(escapeComma).join(",");
+}
+
+/**
+ * Gets the value of a string in a .properties file from the calendar bundle
+ *
+ * @param aBundleName the name of the properties file. It is assumed that the
+ * file lives in chrome://calendar/locale/
+ * @param aStringName the name of the string within the properties file
+ * @param aParams optional array of parameters to format the string
+ * @param aComponent optional stringbundle component name
+ */
+function calGetString(aBundleName, aStringName, aParams, aComponent="calendar") {
+ let propName = "chrome://" + aComponent + "/locale/" + aBundleName + ".properties";
+
+ try {
+ let props = Services.strings.createBundle(propName);
+
+ if (aParams && aParams.length) {
+ return props.formatStringFromName(aStringName, aParams, aParams.length);
+ } else {
+ return props.GetStringFromName(aStringName);
+ }
+ } catch (ex) {
+ let msg = "Failed to read '" + aStringName + "' from " + propName + ".";
+ Components.utils.reportError(msg + " Error: " + ex);
+ return msg;
+ }
+}
+
+/**
+ * Make a UUID using the UUIDGenerator service available, we'll use that.
+ */
+function getUUID() {
+ let uuidGen = Components.classes["@mozilla.org/uuid-generator;1"]
+ .getService(Components.interfaces.nsIUUIDGenerator);
+ // generate uuids without braces to avoid problems with
+ // CalDAV servers that don't support filenames with {}
+ return uuidGen.generateUUID().toString().replace(/[{}]/g, "");
+}
+
+/**
+ * Due to a bug in js-wrapping, normal == comparison can fail when we
+ * have 2 objects. Use these functions to force them both to get wrapped
+ * the same way, allowing for normal comparison.
+ */
+
+/**
+ * calIItemBase comparer
+ */
+function compareItems(aItem, aOtherItem) {
+ let sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ sip1.data = aItem;
+ sip1.dataIID = Components.interfaces.calIItemBase;
+
+ let sip2 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ sip2.data = aOtherItem;
+ sip2.dataIID = Components.interfaces.calIItemBase;
+ return sip1.data == sip2.data;
+}
+
+/**
+ * Tries to get rid of wrappers. This is used to avoid cyclic references, and thus leaks.
+ */
+function calTryWrappedJSObject(obj) {
+ if (obj && obj.wrappedJSObject) {
+ obj = obj.wrappedJSObject;
+ }
+ return obj;
+}
+
+/**
+ * Generic object comparer
+ * Use to compare two objects which are not of type calIItemBase, in order
+ * to avoid the js-wrapping issues mentioned above.
+ *
+ * @param aObject first object to be compared
+ * @param aOtherObject second object to be compared
+ * @param aIID IID to use in comparison, undefined/null defaults to nsISupports
+ */
+function compareObjects(aObject, aOtherObject, aIID) {
+ // xxx todo: seems to work fine e.g. for WCAP, but I still mistrust this trickery...
+ // Anybody knows an official API that could be used for this purpose?
+ // For what reason do clients need to pass aIID since
+ // every XPCOM object has to implement nsISupports?
+ // XPCOM (like COM, like UNO, ...) defines that QueryInterface *only* needs to return
+ // the very same pointer for nsISupports during its lifetime.
+ if (!aIID) {
+ aIID = Components.interfaces.nsISupports;
+ }
+ let sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ sip1.data = aObject;
+ sip1.dataIID = aIID;
+
+ let sip2 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ sip2.data = aOtherObject;
+ sip2.dataIID = aIID;
+ return sip1.data == sip2.data;
+}
+
+/**
+ * Compare two arrays using the passed function.
+ */
+function compareArrays(aOne, aTwo, compareFunc) {
+ if (!aOne && !aTwo) {
+ return true;
+ }
+ if (!aOne || !aTwo) {
+ return false;
+ }
+ let len = aOne.length;
+ if (len != aTwo.length) {
+ return false;
+ }
+ for (let i = 0; i < len; ++i) {
+ if (!compareFunc(aOne[i], aTwo[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Takes care of all QueryInterface business, including calling the QI of any
+ * existing parent prototypes.
+ *
+ * @deprecated
+ * @param aSelf The object the QueryInterface is being made to
+ * @param aProto Caller's prototype object
+ * @param aIID The IID to check for
+ * @param aList (Optional if aClassInfo is specified) An array of
+ * interfaces from Components.interfaces
+ * @param aClassInfo (Optional) an Object containing the class info for this
+ * prototype.
+ */
+function doQueryInterface(aSelf, aProto, aIID, aList, aClassInfo) {
+ if (!doQueryInterface.warningIssued) {
+ cal.WARN("Use of doQueryInterface() is deprecated and will be removed " +
+ "with the next release. Use XPCOMUtils.generateQI() instead.\n" +
+ cal.STACK(10));
+ doQueryInterface.warningIssued = true;
+ }
+
+ if (aClassInfo) {
+ if (aIID.equals(Components.interfaces.nsIClassInfo)) {
+ return aClassInfo;
+ }
+ if (!aList) {
+ aList = aClassInfo.getInterfaces({});
+ }
+ }
+
+ if (aList) {
+ for (let iid of aList) {
+ if (aIID.equals(iid)) {
+ return aSelf;
+ }
+ }
+ }
+
+ if (aIID.equals(Components.interfaces.nsISupports)) {
+ return aSelf;
+ }
+
+ if (aProto) {
+ let base = aProto.__proto__;
+ if (base && base.QueryInterface) {
+ // Try to QI the base prototype
+ return base.QueryInterface.call(aSelf, aIID);
+ }
+ }
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/**
+ * Many computations want to work only with date-times, not with dates. This
+ * method will return a proper datetime (set to midnight) for a date object. If
+ * the object is already a datetime, it will simply be returned.
+ *
+ * @param aDate the date or datetime to check
+ */
+function ensureDateTime(aDate) {
+ if (!aDate || !aDate.isDate) {
+ return aDate;
+ }
+ let newDate = aDate.clone();
+ newDate.isDate = false;
+ return newDate;
+}
+
+/**
+ * Get the default event start date. This is the next full hour, or 23:00 if it
+ * is past 23:00.
+ *
+ * @param aReferenceDate If passed, the time of this date will be modified,
+ * keeping the date and timezone intact.
+ */
+function getDefaultStartDate(aReferenceDate) {
+ let startDate = now();
+ if (aReferenceDate) {
+ let savedHour = startDate.hour;
+ startDate = aReferenceDate;
+ if (!startDate.isMutable) {
+ startDate = startDate.clone();
+ }
+ startDate.isDate = false;
+ startDate.hour = savedHour;
+ }
+
+ startDate.second = 0;
+ startDate.minute = 0;
+ if (startDate.hour < 23) {
+ startDate.hour++;
+ }
+ return startDate;
+}
+
+/**
+ * Setup the default start and end hours of the given item. This can be a task
+ * or an event.
+ *
+ * @param aItem The item to set up the start and end date for.
+ * @param aReferenceDate If passed, the time of this date will be modified,
+ * keeping the date and timezone intact.
+ */
+function setDefaultStartEndHour(aItem, aReferenceDate) {
+ aItem[calGetStartDateProp(aItem)] = getDefaultStartDate(aReferenceDate);
+
+ if (isEvent(aItem)) {
+ aItem.endDate = aItem.startDate.clone();
+ aItem.endDate.minute += Preferences.get("calendar.event.defaultlength", 60);
+ }
+}
+
+/**
+ * Helper used in the following log functions to actually log the message.
+ * Should not be used outside of this file.
+ */
+function _log(message, flag) {
+ let frame = Components.stack.caller.caller;
+ let filename = frame.filename ? frame.filename.split(" -> ").pop() : null;
+ let scriptError = Components.classes["@mozilla.org/scripterror;1"]
+ .createInstance(Components.interfaces.nsIScriptError);
+ scriptError.init(message, filename, null, frame.lineNumber, frame.columnNumber,
+ flag, "component javascript");
+ Services.console.logMessage(scriptError);
+}
+
+/**
+ * Logs a string or an object to both stderr and the js-console only in the case
+ * where the calendar.debug.log pref is set to true.
+ *
+ * @param aArg either a string to log or an object whose entire set of
+ * properties should be logged.
+ */
+function LOG(aArg) {
+ if (!Preferences.get("calendar.debug.log", false)) {
+ return;
+ }
+
+ ASSERT(aArg, "Bad log argument.", false);
+ let string = aArg;
+ // We should just dump() both String objects, and string primitives.
+ if (!(aArg instanceof String) && !(typeof aArg == "string")) {
+ string = "Logging object...\n";
+ for (let prop in aArg) {
+ string += prop + ": " + aArg[prop] + "\n";
+ }
+ string += "End object\n";
+ }
+
+ dump(string + "\n");
+ _log(string, Components.interfaces.nsIScriptError.infoFlag);
+}
+
+/**
+ * Dumps a warning to both console and js console.
+ *
+ * @param aMessage warning message
+ */
+function WARN(aMessage) {
+ dump("Warning: " + aMessage + "\n");
+ _log(aMessage, Components.interfaces.nsIScriptError.warningFlag);
+}
+
+/**
+ * Dumps an error to both console and js console.
+ *
+ * @param aMessage error message
+ */
+function ERROR(aMessage) {
+ dump("Error: " + aMessage + "\n");
+ _log(aMessage, Components.interfaces.nsIScriptError.errorFlag);
+}
+
+/**
+ * Returns a string describing the current js-stack with filename and line
+ * numbers.
+ *
+ * @param aDepth (optional) The number of frames to include. Defaults to 5.
+ * @param aSkip (optional) Number of frames to skip
+ */
+function STACK(aDepth, aSkip) {
+ let depth = aDepth || 10;
+ let skip = aSkip || 0;
+ let stack = "";
+ let frame = Components.stack.caller;
+ for (let i = 1; i <= depth + skip && frame; i++) {
+ if (i > skip) {
+ stack += i + ": [" + frame.filename + ":" +
+ frame.lineNumber + "] " + frame.name + "\n";
+ }
+ frame = frame.caller;
+ }
+ return stack;
+}
+
+/**
+ * Logs a message and the current js-stack, if aCondition fails
+ *
+ * @param aCondition the condition to test for
+ * @param aMessage the message to report in the case the assert fails
+ * @param aCritical if true, throw an error to stop current code execution
+ * if false, code flow will continue
+ * may be a result code
+ */
+function ASSERT(aCondition, aMessage, aCritical) {
+ if (aCondition) {
+ return;
+ }
+
+ let string = "Assert failed: " + aMessage + "\n" + STACK(0, 1);
+ if (aCritical) {
+ throw new Components.Exception(string,
+ aCritical === true ? Components.results.NS_ERROR_UNEXPECTED : aCritical);
+ } else {
+ Components.utils.reportError(string);
+ }
+}
+
+/**
+ * Uses the prompt service to display an error message.
+ * This function cannot be migrated into a module file, because it relies on an outer window object.
+ *
+ * @param aMsg The message to be shown
+ */
+function showError(aMsg) {
+ let wnd = window || null;
+ if (wnd) {
+ Services.prompt.alert(wnd, calGetString("calendar", "genericErrorTitle"), aMsg);
+ }
+}
+
+/**
+ * Pick whichever of "black" or "white" will look better when used as a text
+ * color against a background of bgColor.
+ *
+ * @param bgColor the background color as a "#RRGGBB" string
+ */
+function getContrastingTextColor(bgColor) {
+ let calcColor = bgColor.replace(/#/g, "");
+ let red = parseInt(calcColor.substring(0, 2), 16);
+ let green = parseInt(calcColor.substring(2, 4), 16);
+ let blue = parseInt(calcColor.substring(4, 6), 16);
+
+ // Calculate the brightness (Y) value using the YUV color system.
+ let brightness = (0.299 * red) + (0.587 * green) + (0.114 * blue);
+
+ // Consider all colors with less than 56% brightness as dark colors and
+ // use white as the foreground color, otherwise use black.
+ if (brightness < 144) {
+ return "white";
+ }
+
+ return "black";
+}
+
+/**
+ * Returns the property name used for the start date of an item, ie either an
+ * event's start date or a task's entry date.
+ */
+function calGetStartDateProp(aItem) {
+ if (isEvent(aItem)) {
+ return "startDate";
+ } else if (isToDo(aItem)) {
+ return "entryDate";
+ }
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/**
+ * Returns the property name used for the end date of an item, ie either an
+ * event's end date or a task's due date.
+ */
+function calGetEndDateProp(aItem) {
+ if (isEvent(aItem)) {
+ return "endDate";
+ } else if (isToDo(aItem)) {
+ return "dueDate";
+ }
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/**
+ * Checks whether the passed item fits into the demanded range.
+ *
+ * @param item the item
+ * @param rangeStart (inclusive) range start or null (open range)
+ * @param rangeStart (exclusive) range end or null (open range)
+ * @param returnDtstartOrDue returns item's start (or due) date in case
+ * the item is in the specified Range; null otherwise.
+ */
+function checkIfInRange(item, rangeStart, rangeEnd, returnDtstartOrDue) {
+ let startDate;
+ let endDate;
+ let queryStart = ensureDateTime(rangeStart);
+ if (isEvent(item)) {
+ startDate = item.startDate;
+ if (!startDate) { // DTSTART mandatory
+ // xxx todo: should we assert this case?
+ return null;
+ }
+ endDate = item.endDate || startDate;
+ } else {
+ let dueDate = item.dueDate;
+ startDate = item.entryDate || dueDate;
+ if (!item.entryDate) {
+ if (returnDtstartOrDue) { // DTSTART or DUE mandatory
+ return null;
+ }
+ // 3.6.2. To-do Component
+ // A "VTODO" calendar component without the "DTSTART" and "DUE" (or
+ // "DURATION") properties specifies a to-do that will be associated
+ // with each successive calendar date, until it is completed.
+ let completedDate = ensureDateTime(item.completedDate);
+ dueDate = ensureDateTime(dueDate);
+ return !completedDate || !queryStart ||
+ completedDate.compare(queryStart) > 0 ||
+ (dueDate && dueDate.compare(queryStart) >= 0);
+ }
+ endDate = dueDate || startDate;
+ }
+
+ let start = ensureDateTime(startDate);
+ let end = ensureDateTime(endDate);
+ let queryEnd = ensureDateTime(rangeEnd);
+
+ if (start.compare(end) == 0) {
+ if ((!queryStart || start.compare(queryStart) >= 0) &&
+ (!queryEnd || start.compare(queryEnd) < 0)) {
+ return startDate;
+ }
+ } else if ((!queryEnd || start.compare(queryEnd) < 0) &&
+ (!queryStart || end.compare(queryStart) > 0)) {
+ return startDate;
+ }
+ return null;
+}
+
+/**
+ * This function return the progress state of a task:
+ * completed, overdue, duetoday, inprogress, future
+ *
+ * @param aTask The task to check.
+ * @return The progress atom.
+ */
+function getProgressAtom(aTask) {
+ let nowdate = new Date();
+
+ if (aTask.recurrenceInfo) {
+ return "repeating";
+ }
+
+ if (aTask.isCompleted) {
+ return "completed";
+ }
+
+ if (aTask.dueDate && aTask.dueDate.isValid) {
+ if (cal.dateTimeToJsDate(aTask.dueDate).getTime() < nowdate.getTime()) {
+ return "overdue";
+ } else if (aTask.dueDate.year == nowdate.getFullYear() &&
+ aTask.dueDate.month == nowdate.getMonth() &&
+ aTask.dueDate.day == nowdate.getDate()) {
+ return "duetoday";
+ }
+ }
+
+ if (aTask.entryDate && aTask.entryDate.isValid &&
+ cal.dateTimeToJsDate(aTask.entryDate).getTime() < nowdate.getTime()) {
+ return "inprogress";
+ }
+
+ return "future";
+}
+
+function calInterfaceBag(iid) {
+ this.init(iid);
+}
+calInterfaceBag.prototype = {
+ mIid: null,
+ mInterfaces: null,
+
+ // Iterating the inteface bag iterates the interfaces it contains
+ [Symbol.iterator]: function() { return this.mInterfaces[Symbol.iterator](); },
+
+ // internal:
+ init: function(iid) {
+ this.mIid = iid;
+ this.mInterfaces = [];
+ },
+
+ // external:
+ get size() {
+ return this.mInterfaces.length;
+ },
+
+ get interfaceArray() {
+ return this.mInterfaces;
+ },
+
+ add: function(iface) {
+ if (iface) {
+ let existing = this.mInterfaces.some(obj => {
+ return compareObjects(obj, iface, this.mIid);
+ });
+ if (!existing) {
+ this.mInterfaces.push(iface);
+ }
+ return !existing;
+ }
+ return false;
+ },
+
+ remove: function(iface) {
+ if (iface) {
+ this.mInterfaces = this.mInterfaces.filter((obj) => {
+ return !compareObjects(obj, iface, this.mIid);
+ });
+ }
+ },
+
+ forEach: function(func) {
+ this.mInterfaces.forEach(func);
+ }
+};
+
+function calListenerBag(iid) {
+ this.init(iid);
+}
+calListenerBag.prototype = {
+ __proto__: calInterfaceBag.prototype,
+
+ notify: function(func, args=[]) {
+ function notifyFunc(iface) {
+ try {
+ iface[func](...args);
+ } catch (exc) {
+ let stack = exc.stack || (exc.location ? exc.location.formattedStack : null);
+ Components.utils.reportError(exc + "\nSTACK: " + stack);
+ }
+ }
+ this.mInterfaces.forEach(notifyFunc);
+ }
+};
+
+function sendMailTo(aRecipient, aSubject, aBody, aIdentity) {
+ let msgParams = Components.classes["@mozilla.org/messengercompose/composeparams;1"]
+ .createInstance(Components.interfaces.nsIMsgComposeParams);
+ let composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Components.interfaces.nsIMsgCompFields);
+
+ composeFields.to = aRecipient;
+ composeFields.subject = aSubject;
+ composeFields.body = aBody;
+
+ msgParams.type = Components.interfaces.nsIMsgCompType.New;
+ msgParams.format = Components.interfaces.nsIMsgCompFormat.Default;
+ msgParams.composeFields = composeFields;
+ msgParams.identity = aIdentity;
+
+ MailServices.compose.OpenComposeWindowWithParams(null, msgParams);
+}
+
+/**
+ * This object implements calIOperation and could group multiple sub
+ * operations into one. You can pass a cancel function which is called once
+ * the operation group is cancelled.
+ * Users must call notifyCompleted() once all sub operations have been
+ * successful, else the operation group will stay pending.
+ * The reason for the latter is that providers currently should (but need
+ * not) implement (and return) calIOperation handles, thus there may be pending
+ * calendar operations (without handle).
+ */
+function calOperationGroup(cancelFunc) {
+ this.wrappedJSObject = this;
+ if (calOperationGroup.mOpGroupId === undefined) {
+ calOperationGroup.mOpGroupId = 0;
+ }
+ if (calOperationGroup.mOpGroupPrefix === undefined) {
+ calOperationGroup.mOpGroupPrefix = getUUID() + "-";
+ }
+ this.mCancelFunc = cancelFunc;
+ this.mId = calOperationGroup.mOpGroupPrefix + calOperationGroup.mOpGroupId++;
+ this.mSubOperations = [];
+}
+calOperationGroup.prototype = {
+ mCancelFunc: null,
+ mId: null,
+ mIsPending: true,
+ mStatus: Components.results.NS_OK,
+ mSubOperations: null,
+
+ add: function(aOperation) {
+ if (aOperation && aOperation.isPending) {
+ this.mSubOperations.push(aOperation);
+ }
+ },
+
+ remove: function(aOperation) {
+ if (aOperation) {
+ this.mSubOperations = this.mSubOperations.filter(operation => aOperation.id != operation.id);
+ }
+ },
+
+ get isEmpty() {
+ return (this.mSubOperations.length == 0);
+ },
+
+ notifyCompleted: function(status) {
+ ASSERT(this.isPending, "[calOperationGroup_notifyCompleted] this.isPending");
+ if (this.isPending) {
+ this.mIsPending = false;
+ if (status) {
+ this.mStatus = status;
+ }
+ }
+ },
+
+ toString: function() {
+ return "[calOperationGroup] id=" + this.id;
+ },
+
+ // calIOperation:
+ get id() {
+ return this.mId;
+ },
+
+ get isPending() {
+ return this.mIsPending;
+ },
+
+ get status() {
+ return this.mStatus;
+ },
+
+ cancel: function(status) {
+ if (this.isPending) {
+ if (!status) {
+ status = Components.interfaces.calIErrors.OPERATION_CANCELLED;
+ }
+ this.notifyCompleted(status);
+ let cancelFunc = this.mCancelFunc;
+ if (cancelFunc) {
+ this.mCancelFunc = null;
+ cancelFunc();
+ }
+ let subOperations = this.mSubOperations;
+ this.mSubOperations = [];
+ for (let operation of subOperations) {
+ operation.cancel(Components.interfaces.calIErrors.OPERATION_CANCELLED);
+ }
+ }
+ }
+};
+
+function sameDay(date1, date2) {
+ if (date1 && date2) {
+ if ((date1.day == date2.day) &&
+ (date1.month == date2.month) &&
+ (date1.year == date2.year)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Centralized funtions for accessing prodid and version
+ */
+function calGetProductId() {
+ return "-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN";
+}
+function calGetProductVersion() {
+ return "2.0";
+}
+
+/**
+ * This is a centralized function for setting the prodid and version on an
+ * ical component. This should be used whenever you need to set the prodid
+ * and version on a calIcalComponent object.
+ *
+ * @param
+ * aIcalComponent The ical component to set the prodid and version on.
+ */
+function calSetProdidVersion(aIcalComponent) {
+ // Throw for an invalid parameter
+ aIcalComponent = cal.wrapInstance(aIcalComponent, Components.interfaces.calIIcalComponent);
+ if (!aIcalComponent) {
+ throw Components.results.NS_ERROR_INVALID_ARG;
+ }
+ // Set the prodid and version
+ aIcalComponent.prodid = calGetProductId();
+ aIcalComponent.version = calGetProductVersion();
+}
+
+
+/**
+ * TODO: The following UI-related functions need to move somewhere different,
+ * i.e calendar-ui-utils.js
+ */
+
+/**
+ * applies a value to all children of a Menu. If the respective childnodes define
+ * a command the value is applied to the attribute of thecommand of the childnode
+ *
+ * @param aElement The parentnode of the elements
+ * @param aAttributeName The name of the attribute
+ * @param aValue The value of the attribute
+ */
+function applyAttributeToMenuChildren(aElement, aAttributeName, aValue) {
+ let sibling = aElement.firstChild;
+ do {
+ if (sibling) {
+ let domObject = sibling;
+ let commandName = null;
+ if (sibling.hasAttribute("command")) {
+ commandName = sibling.getAttribute("command");
+ }
+ if (commandName) {
+ let command = document.getElementById(commandName);
+ if (command) {
+ domObject = command;
+ }
+ }
+ domObject.setAttribute(aAttributeName, aValue);
+ sibling = sibling.nextSibling;
+ }
+ } while (sibling);
+}
+
+
+/**
+ * compares the value of a property of an array of objects and returns
+ * true or false if it is same or not among all array members
+ *
+ * @param aObjects An Array of Objects to inspect
+ * @param aProperty Name the name of the Property of which the value is compared
+ */
+function isPropertyValueSame(aObjects, aPropertyName) {
+ let value = null;
+ for (let i = 0; i < aObjects.length; i++) {
+ if (!value) {
+ value = aObjects[0][aPropertyName];
+ }
+ let compValue = aObjects[i][aPropertyName];
+ if (compValue != value) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * returns a parentnode - or the overgiven node - with the given localName,
+ * by "walking up" the DOM-hierarchy.
+ *
+ * @param aChildNode The childnode.
+ * @param aLocalName The localName of the to-be-returned parent
+ * that is looked for.
+ * @return The parent with the given localName or the
+ * given childNode 'aChildNode'. If no appropriate
+ * parent node with aLocalName could be
+ * retrieved it is returned 'null'.
+ */
+function getParentNodeOrThis(aChildNode, aLocalName) {
+ let node = aChildNode;
+ while (node && (node.localName != aLocalName)) {
+ node = node.parentNode;
+ if (node.tagName == undefined) {
+ return null;
+ }
+ }
+ return node;
+}
+
+/**
+ * Returns a parentnode - or the overgiven node - with the given attributevalue
+ * for the given attributename by "walking up" the DOM-hierarchy.
+ *
+ * @param aChildNode The childnode.
+ * @param aAttibuteName The name of the attribute that is to be compared with
+ * @param aAttibuteValue The value of the attribute that is to be compared with
+ * @return The parent with the given attributeName set that has
+ * the same value as the given given attributevalue
+ * 'aAttributeValue'. If no appropriate
+ * parent node can be retrieved it is returned 'null'.
+ */
+function getParentNodeOrThisByAttribute(aChildNode, aAttributeName, aAttributeValue) {
+ let node = aChildNode;
+ while (node && (node.getAttribute(aAttributeName) != aAttributeValue)) {
+ node = node.parentNode;
+ if (node.tagName == undefined) {
+ return null;
+ }
+ }
+ return node;
+}
+
+function setItemProperty(item, propertyName, aValue, aCapability) {
+ let isSupported = (item.calendar.getProperty("capabilities." + aCapability + ".supported") !== false);
+ let value = (aCapability && !isSupported ? null : aValue);
+
+ switch (propertyName) {
+ case "startDate":
+ if ((value.isDate && !item.startDate.isDate) ||
+ (!value.isDate && item.startDate.isDate) ||
+ !compareObjects(value.timezone, item.startDate.timezone) ||
+ value.compare(item.startDate) != 0) {
+ item.startDate = value;
+ }
+ break;
+ case "endDate":
+ if ((value.isDate && !item.endDate.isDate) ||
+ (!value.isDate && item.endDate.isDate) ||
+ !compareObjects(value.timezone, item.endDate.timezone) ||
+ value.compare(item.endDate) != 0) {
+ item.endDate = value;
+ }
+ break;
+ case "entryDate":
+ if (value == item.entryDate) {
+ break;
+ }
+ if ((value && !item.entryDate) ||
+ (!value && item.entryDate) ||
+ value.isDate != item.entryDate.isDate ||
+ !compareObjects(value.timezone, item.entryDate.timezone) ||
+ value.compare(item.entryDate) != 0) {
+ item.entryDate = value;
+ }
+ break;
+ case "dueDate":
+ if (value == item.dueDate) {
+ break;
+ }
+ if ((value && !item.dueDate) ||
+ (!value && item.dueDate) ||
+ value.isDate != item.dueDate.isDate ||
+ !compareObjects(value.timezone, item.dueDate.timezone) ||
+ value.compare(item.dueDate) != 0) {
+ item.dueDate = value;
+ }
+ break;
+ case "isCompleted":
+ if (value != item.isCompleted) {
+ item.isCompleted = value;
+ }
+ break;
+ case "PERCENT-COMPLETE": {
+ let perc = parseInt(item.getProperty(propertyName), 10);
+ if (isNaN(perc)) {
+ perc = 0;
+ }
+ if (perc != value) {
+ item.setProperty(propertyName, value);
+ }
+ break;
+ }
+ case "title":
+ if (value != item.title) {
+ item.title = value;
+ }
+ break;
+ default:
+ if (!value || value == "") {
+ item.deleteProperty(propertyName);
+ } else if (item.getProperty(propertyName) != value) {
+ item.setProperty(propertyName, value);
+ }
+ break;
+ }
+}
+/**
+ * END TODO: The above UI-related functions need to move somewhere different,
+ * i.e calendar-ui-utils.js
+ */
+
+/**
+ * Implements a property bag.
+ */
+function calPropertyBag() {
+ this.mData = {};
+}
+calPropertyBag.prototype = {
+ mData: null,
+
+ setProperty: function(aName, aValue) {
+ return (this.mData[aName] = aValue);
+ },
+ getProperty_: function(aName) {
+ // avoid strict undefined property warning
+ return (aName in this.mData ? this.mData[aName] : undefined);
+ },
+ getProperty: function(aName) {
+ // avoid strict undefined property warning
+ return (aName in this.mData ? this.mData[aName] : null);
+ },
+ getAllProperties: function(aOutKeys, aOutValues) {
+ let keys = [];
+ let values = [];
+ for (let key in this.mData) {
+ keys.push(key);
+ values.push(this.mData[key]);
+ }
+ aOutKeys.value = keys;
+ aOutValues.value = values;
+ },
+ deleteProperty: function(aName) {
+ delete this.mData[aName];
+ },
+ get enumerator() {
+ return new calPropertyBagEnumerator(this);
+ },
+ [Symbol.iterator]: function* () {
+ for (let name of Object.keys(this.mData)) {
+ yield [name, this.mData[name]];
+ }
+ }
+};
+// implementation part of calPropertyBag
+function calPropertyBagEnumerator(bag) {
+ this.mIndex = 0;
+ this.mBag = bag;
+ this.mKeys = Object.keys(bag.mData);
+}
+calPropertyBagEnumerator.prototype = {
+ mIndex: 0,
+ mBag: null,
+ mKeys: null,
+
+ // nsISimpleEnumerator:
+ getNext: function() {
+ if (!this.hasMoreElements()) { // hasMoreElements is called by intention to skip yet deleted properties
+ ASSERT(false, Components.results.NS_ERROR_UNEXPECTED);
+ throw Components.results.NS_ERROR_UNEXPECTED;
+ }
+ let name = this.mKeys[this.mIndex++];
+ return { // nsIProperty:
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProperty]),
+ name: name,
+ value: this.mCurrentValue
+ };
+ },
+ hasMoreElements: function() {
+ while (this.mIndex < this.mKeys.length) {
+ this.mCurrentValue = this.mBag.mData[this.mKeys[this.mIndex]];
+ if (this.mCurrentValue !== undefined) {
+ return true;
+ }
+ ++this.mIndex;
+ }
+ return false;
+ }
+};
+
+/**
+ * Iterates all email identities and calls the passed function with identity and account.
+ * If the called function returns false, iteration is stopped.
+ */
+function calIterateEmailIdentities(func) {
+ let accounts = MailServices.accounts.accounts;
+ for (let i = 0; i < accounts.length; ++i) {
+ let account = accounts.queryElementAt(i, Components.interfaces.nsIMsgAccount);
+ let identities = account.identities;
+ for (let j = 0; j < identities.length; ++j) {
+ let identity = identities.queryElementAt(j, Components.interfaces.nsIMsgIdentity);
+ if (!func(identity, account)) {
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Compare two items by *content*, leaving out any revision information such as
+ * X-MOZ-GENERATION, SEQUENCE, DTSTAMP, LAST-MODIFIED.
+
+ * The format for the parameters to ignore object is:
+ * { "PROPERTY-NAME": ["PARAM-NAME", ...] }
+ *
+ * If aIgnoreProps is not passed, these properties are ignored:
+ * X-MOZ-GENERATION, SEQUENCE, DTSTAMP, LAST-MODIFIED, X-MOZ-SEND-INVITATIONS
+ *
+ * If aIgnoreParams is not passed, these parameters are ignored:
+ * ATTENDEE: CN
+ * ORGANIZER: CN
+ *
+ * @param aFirstItem The item to compare.
+ * @param aSecondItem The item to compare to.
+ * @param aIgnoreProps (optional) An array of parameters to ignore.
+ * @param aIgnoreParams (optional) An object describing which parameters to
+ * ignore.
+ * @return True, if items match.
+ */
+function compareItemContent(aFirstItem, aSecondItem, aIgnoreProps, aIgnoreParams) {
+ let ignoreProps = arr2hash(aIgnoreProps ||
+ ["SEQUENCE", "DTSTAMP", "LAST-MODIFIED", "X-MOZ-GENERATION", "X-MICROSOFT-DISALLOW-COUNTER",
+ "X-MOZ-SEND-INVITATIONS", "X-MOZ-SEND-INVITATIONS-UNDISCLOSED"]);
+
+ let ignoreParams = aIgnoreParams ||
+ { ATTENDEE: ["CN"], ORGANIZER: ["CN"] };
+ for (let x in ignoreParams) {
+ ignoreParams[x] = arr2hash(ignoreParams[x]);
+ }
+
+ function arr2hash(arr) {
+ let hash = {};
+ for (let x of arr) {
+ hash[x] = true;
+ }
+ return hash;
+ }
+
+ // This doesn't have to be super correct rfc5545, it just needs to be
+ // in the same order
+ function normalizeComponent(comp) {
+ let props = [];
+ for (let prop of cal.ical.propertyIterator(comp)) {
+ if (!(prop.propertyName in ignoreProps)) {
+ props.push(normalizeProperty(prop));
+ }
+ }
+ props = props.sort();
+
+ let comps = [];
+ for (let subcomp of cal.ical.subcomponentIterator(comp)) {
+ comps.push(normalizeComponent(subcomp));
+ }
+ comps = comps.sort();
+
+ return comp.componentType + props.join("\r\n") + comps.join("\r\n");
+ }
+
+ function normalizeProperty(prop) {
+ let params = [...cal.ical.paramIterator(prop)]
+ .filter(([k, v]) => !(prop.propertyName in ignoreParams) ||
+ !(k in ignoreParams[prop.propertyName]))
+ .map(([k, v]) => k + "=" + v)
+ .sort();
+
+ return prop.propertyName + ";" +
+ params.join(";") + ":" +
+ prop.valueAsIcalString;
+ }
+
+ return normalizeComponent(aFirstItem.icalComponent) ==
+ normalizeComponent(aSecondItem.icalComponent);
+}
+
+/**
+ * Use the binary search algorithm to search for an item in an array.
+ * function.
+ *
+ * The comptor function may look as follows for calIDateTime objects.
+ * function comptor(a, b) {
+ * return a.compare(b);
+ * }
+ * If no comptor is specified, the default greater-than comptor will be used.
+ *
+ * @param itemArray The array to search.
+ * @param newItem The item to search in the array.
+ * @param comptor A comparation function that can compare two items.
+ * @return The index of the new item.
+ */
+function binarySearch(itemArray, newItem, comptor) {
+ function binarySearchInternal(low, high) {
+ // Are we done yet?
+ if (low == high) {
+ return low + (comptor(newItem, itemArray[low]) < 0 ? 0 : 1);
+ }
+
+ let mid = Math.floor(low + ((high - low) / 2));
+ let cmp = comptor(newItem, itemArray[mid]);
+ if (cmp > 0) {
+ return binarySearchInternal(mid + 1, high);
+ } else if (cmp < 0) {
+ return binarySearchInternal(low, mid);
+ } else {
+ return mid;
+ }
+ }
+
+ if (itemArray.length < 1) {
+ return -1;
+ }
+ if (!comptor) {
+ comptor = function(a, b) {
+ return (a > b) - (a < b);
+ };
+ }
+ return binarySearchInternal(0, itemArray.length - 1);
+}
+
+/**
+ * Insert a new node underneath the given parentNode, using binary search. See binarySearch
+ * for a note on how the comptor works.
+ *
+ * @param parentNode The parent node underneath the new node should be inserted.
+ * @param inserNode The node to insert
+ * @param aItem The calendar item to add a widget for.
+ * @param comptor A comparison function that can compare two items (not DOM Nodes!)
+ * @param discardDuplicates Use the comptor function to check if the item in
+ * question is already in the array. If so, the
+ * new item is not inserted.
+ * @param itemAccessor [optional] A function that receives a DOM node and returns the associated item
+ * If null, this function will be used: function(n) n.item
+ */
+function binaryInsertNode(parentNode, insertNode, aItem, comptor, discardDuplicates, itemAccessor) {
+ let accessor = itemAccessor || binaryInsertNode.defaultAccessor;
+
+ // Get the index of the node before which the inserNode will be inserted
+ let newIndex = binarySearch(Array.map(parentNode.childNodes, accessor), aItem, comptor);
+
+ if (newIndex < 0) {
+ parentNode.appendChild(insertNode);
+ newIndex = 0;
+ } else if (!discardDuplicates ||
+ comptor(accessor(parentNode.childNodes[Math.min(newIndex, parentNode.childNodes.length - 1)]), aItem) >= 0) {
+ // Only add the node if duplicates should not be discarded, or if
+ // they should and the childNode[newIndex] == node.
+ let node = parentNode.childNodes[newIndex];
+ parentNode.insertBefore(insertNode, node);
+ }
+ return newIndex;
+}
+binaryInsertNode.defaultAccessor = n => n.item;
+
+/**
+ * Insert an item into the given array, using binary search. See binarySearch
+ * for a note on how the comptor works.
+ *
+ * @param itemArray The array to insert into.
+ * @param item The item to insert into the array.
+ * @param comptor A comparation function that can compare two items.
+ * @param discardDuplicates Use the comptor function to check if the item in
+ * question is already in the array. If so, the
+ * new item is not inserted.
+ * @return The index of the new item.
+ */
+function binaryInsert(itemArray, item, comptor, discardDuplicates) {
+ let newIndex = binarySearch(itemArray, item, comptor);
+
+ if (newIndex < 0) {
+ itemArray.push(item);
+ newIndex = 0;
+ } else if (!discardDuplicates ||
+ comptor(itemArray[Math.min(newIndex, itemArray.length - 1)], item) != 0) {
+ // Only add the item if duplicates should not be discarded, or if
+ // they should and itemArray[newIndex] != item.
+ itemArray.splice(newIndex, 0, item);
+ }
+ return newIndex;
+}
+
+/**
+ * Gets the cached instance of the composite calendar.
+ *
+ * WARNING: Great care should be taken how this function is called. If it is
+ * called as "cal.getCompositeCalendar()" then it is called through calUtils.jsm
+ * which means there will be one instance per app. If called as
+ * "getCompositeCalendar()" from chrome code, then it will get a window-specific
+ * composite calendar, which is often what is wanted
+ */
+function getCompositeCalendar() {
+ if (getCompositeCalendar.mObject === undefined) {
+ getCompositeCalendar.mObject = Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
+ .createInstance(Components.interfaces.calICompositeCalendar);
+ getCompositeCalendar.mObject.prefPrefix = "calendar-main";
+
+ try {
+ if (gCalendarStatusFeedback) {
+ // If we are in a window that has calendar status feedback, set up
+ // our status observer.
+ let chromeWindow = window.QueryInterface(Components.interfaces.nsIDOMChromeWindow);
+ getCompositeCalendar.mObject.setStatusObserver(gCalendarStatusFeedback, chromeWindow);
+ }
+ } catch (exc) { // catch errors in case we run in contexts without status feedback
+ }
+ }
+ return getCompositeCalendar.mObject;
+}
+
+/**
+ * Search for already open item dialog or tab.
+ *
+ * @param aItem The item of the dialog or tab to search for.
+ */
+function findItemWindow(aItem) {
+ // check for existing dialog windows
+ let list = Services.wm.getEnumerator("Calendar:EventDialog");
+ while (list.hasMoreElements()) {
+ let dlg = list.getNext();
+ if (dlg.arguments[0] &&
+ dlg.arguments[0].mode == "modify" &&
+ dlg.arguments[0].calendarEvent &&
+ dlg.arguments[0].calendarEvent.hashId == aItem.hashId) {
+ return dlg;
+ }
+ }
+ // check for existing summary windows
+ list = Services.wm.getEnumerator("Calendar:EventSummaryDialog");
+ while (list.hasMoreElements()) {
+ let dlg = list.getNext();
+ if (dlg.calendarItem &&
+ dlg.calendarItem.hashId == aItem.hashId) {
+ return dlg;
+ }
+ }
+ return null;
+}
diff --git a/calendar/base/src/calWeekInfoService.js b/calendar/base/src/calWeekInfoService.js
new file mode 100644
index 000000000..724b740b4
--- /dev/null
+++ b/calendar/base/src/calWeekInfoService.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+function calWeekInfoService() {
+ this.wrappedJSObject = this;
+}
+var calWeekInfoServiceClassID = Components.ID("{6877bbdd-f336-46f5-98ce-fe86d0285cc1}");
+var calWeekInfoServiceInterfaces = [Components.interfaces.calIWeekInfoService];
+calWeekInfoService.prototype = {
+ classID: calWeekInfoServiceClassID,
+ QueryInterface: XPCOMUtils.generateQI(calWeekInfoServiceInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calWeekInfoServiceClassID,
+ contractID: "@mozilla.org/calendar/weekinfo-service;1",
+ classDescription: "Calendar WeekInfo Service",
+ interfaces: calWeekInfoServiceInterfaces,
+ flags: Components.interfaces.nsIClassInfo.SINGLETON
+ }),
+
+ // calIWeekInfoService:
+ getWeekTitle: function(aDateTime) {
+ /**
+ * This implementation is based on the ISO 8601 standard.
+ * ISO 8601 defines week one as the first week with at least 4
+ * days, and defines Monday as the first day of the week.
+ * Equivalently, the week one is the week with the first Thursday.
+ *
+ * This implementation uses the second definition, because it
+ * enables the user to set a different start-day of the week
+ * (Sunday instead of Monday is a common setting). If the first
+ * definition was used, all week-numbers could be off by one
+ * depending on the week start day. (For example, if weeks start
+ * on Sunday, a year that starts on Thursday has only 3 days
+ * [Thu-Sat] in that week, so it would be part of the last week of
+ * the previous year, but if weeks start on Monday, the year would
+ * have four days [Thu-Sun] in that week, so it would be counted
+ * as week 1.)
+ */
+
+ // The week number is the number of days since the start of week 1,
+ // divided by 7 and rounded up. Week 1 is the week containing the first
+ // Thursday of the year.
+ // Thus, the week number of any day is the same as the number of days
+ // between the Thursday of that week and the Thursday of week 1, divided
+ // by 7 and rounded up. (This takes care of days at end/start of a year
+ // which may be part of first/last week in the other year.)
+ // The Thursday of a week is the Thursday that follows the first day
+ // of the week.
+ // The week number of a day is the same as the week number of the first
+ // day of the week. (This takes care of days near the start of the year,
+ // which may be part of the week counted in the previous year.) So we
+ // need the startWeekday.
+ const SUNDAY = 0;
+ let startWeekday = Preferences.get("calendar.week.start", SUNDAY); // default to monday per ISO8601 standard.
+
+ // The number of days since the start of the week.
+ // Notice that the result of the substraction might be negative.
+ // We correct for that by adding 7, and then using the remainder operator.
+ let sinceStartOfWeek = (aDateTime.weekday - startWeekday + 7) % 7;
+
+ // The number of days to Thursday is the difference between Thursday
+ // and the start-day of the week (again corrected for negative values).
+ const THURSDAY = 4;
+ let startToThursday = (THURSDAY - startWeekday + 7) % 7;
+
+ // The yearday number of the Thursday this week.
+ let thisWeeksThursday = aDateTime.yearday - sinceStartOfWeek + startToThursday;
+
+ if (thisWeeksThursday < 1) {
+ // For the first few days of the year, we still are in week 52 or 53.
+ let lastYearDate = aDateTime.clone();
+ lastYearDate.year -= 1;
+ thisWeeksThursday += lastYearDate.endOfYear.yearday;
+ } else if (thisWeeksThursday > aDateTime.endOfYear.yearday) {
+ // For the last few days of the year, we already are in week 1.
+ thisWeeksThursday -= aDateTime.endOfYear.yearday;
+ }
+
+ let weekNumber = Math.ceil(thisWeeksThursday / 7);
+ return weekNumber;
+ },
+
+ /**
+ * gets the first day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate a date time object
+ * @return a dateTime-object denoting the first day of the week
+ */
+ getStartOfWeek: function(aDate) {
+ let date = aDate.clone();
+ date.isDate = true;
+ let offset = Preferences.get("calendar.week.start", 0) - aDate.weekday;
+ date.day += offset;
+ if (offset > 0) {
+ date.day -= 7;
+ }
+ return date;
+ },
+
+ /**
+ * gets the last day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate a date time object
+ * @return a dateTime-object denoting the last day of the week
+ */
+ getEndOfWeek: function(aDate) {
+ let date = this.getStartOfWeek(aDate);
+ date.day += 6;
+ return date;
+ }
+};
diff --git a/calendar/base/src/moz.build b/calendar/base/src/moz.build
new file mode 100644
index 000000000..51d772374
--- /dev/null
+++ b/calendar/base/src/moz.build
@@ -0,0 +1,58 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'calInternalInterfaces.idl',
+]
+
+XPIDL_MODULE = 'calbaseinternal'
+
+EXTRA_COMPONENTS += [
+ 'calDefaultACLManager.js',
+ 'calDefaultACLManager.manifest',
+ 'calItemModule.js',
+ 'calItemModule.manifest',
+ 'calSleepMonitor.js',
+ 'calSleepMonitor.manifest',
+ 'calTimezoneService.js',
+ 'calTimezoneService.manifest',
+]
+
+FINAL_TARGET_FILES['calendar-js'] += [
+ 'calAlarm.js',
+ 'calAlarmMonitor.js',
+ 'calAlarmService.js',
+ 'calAttachment.js',
+ 'calAttendee.js',
+ 'calCachedCalendar.js',
+ 'calCalendarManager.js',
+ 'calCalendarSearchService.js',
+ 'calDateTimeFormatter.js',
+ 'calDeletedItems.js',
+ 'calEvent.js',
+ 'calFilter.js',
+ 'calFreeBusyService.js',
+ 'calIcsParser.js',
+ 'calIcsSerializer.js',
+ 'calItemBase.js',
+ 'calItipItem.js',
+ 'calProtocolHandler.js',
+ 'calRecurrenceDate.js',
+ 'calRecurrenceInfo.js',
+ 'calRelation.js',
+ 'calStartupService.js',
+ 'calTimezone.js',
+ 'calTodo.js',
+ 'calTransactionManager.js',
+ 'calUtils.js',
+ 'calWeekInfoService.js',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Calendar', 'Internal Components')
+
+with Files('calAlarm*'):
+ BUG_COMPONENT = ('Calendar', 'Alarms')
+
diff --git a/calendar/base/themes/common/calendar-alarms.css b/calendar/base/themes/common/calendar-alarms.css
new file mode 100644
index 000000000..3ea33439c
--- /dev/null
+++ b/calendar/base/themes/common/calendar-alarms.css
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Reminder icons (used from the event dialog, reminder dialog, views, ...)
+ */
+.reminder-icon {
+ /* Initially hide the image, overwrite by setting a correct image region */
+ list-style-image: url(chrome://calendar-common/skin/alarm-icons.png);
+ -moz-image-region: rect(0px 1px 1px 0px);
+}
+
+.reminder-icon[value="DISPLAY"] {
+ -moz-image-region: rect(0px 30px 11px 17px);
+}
+
+.alarm-icons-box[suppressed="true"] > .reminder-icon[value="DISPLAY"] {
+ -moz-image-region: rect(0px 44px 11px 31px);
+}
+
+.reminder-icon[value="EMAIL"] {
+ -moz-image-region: rect(0px 16px 11px 0px);
+}
+
+.alarm-icons-box[flashing="true"] > .reminder-icon[value="DISPLAY"] {
+ list-style-image: url(chrome://calendar-common/skin/alarm-flashing.png);
+ -moz-image-region: auto;
+}
+
+/**
+ * Reminder dialog (i.e "custom" alarm in the event dialog)
+ * Please make sure rules added here are very specific and won't hurt other
+ * dialogs.
+ */
+#reminder-relative-radio > .radio-label-center-box > .radio-label-box,
+#reminder-absolute-radio > .radio-label-center-box > .radio-label-box {
+ display: none;
+}
+
+#reminder-actions-menulist > menupopup > menuitem > .menu-iconic-left {
+ display: -moz-box;
+}
+
+#reminder-notifications {
+ overflow-y: visible;
+}
+
+#reminder-notifications > notification {
+ background-color: transparent;
+}
+#reminder-notifications > notification > .notification-inner {
+ border: 0;
+}
+#reminder-notifications > notification[type="warning"] {
+ list-style-image: url(chrome://global/skin/icons/Warning.png);
+}
+
+#reminder-actions-caption,
+#reminder-details-caption,
+#calendar-event-dialog-reminder > .dialog-button-box {
+ padding-top: 20px;
+}
+
+.reminder-icon > .menu-iconic-left > .menu-iconic-icon {
+ width: auto;
+ height: auto;
+}
diff --git a/calendar/base/themes/common/calendar-attendees.css b/calendar/base/themes/common/calendar-attendees.css
new file mode 100644
index 000000000..390503278
--- /dev/null
+++ b/calendar/base/themes/common/calendar-attendees.css
@@ -0,0 +1,264 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* this is for attendee and organizer decoration in summary and event dialog */
+
+#item-attendees-box {
+ -moz-appearance: listbox;
+ margin: 2px 4px 0;
+ overflow-y: auto;
+ min-height: 54px; /*at least two rows - otherwise a scrollbar (if required) wouldn't appear*/
+}
+
+#calendar-summary-dialog #item-attendees,
+#calendar-event-summary-dialog #item-attendees,
+#calendar-task-summary-dialog #item-attendees {
+ max-height: 135px; /* displays up to four rows of attendees*/
+}
+
+.item-attendees-cell {
+ padding: 2px;
+}
+
+#calendar-event-dialog-inner .item-attendees-cell {
+ -moz-user-focus: normal;
+ margin-bottom: 1px;
+ margin-inline-end: 1px;
+}
+
+#calendar-event-dialog-inner .item-attendees-cell:focus {
+ background-color: Highlight;
+ color: Highlighttext;
+}
+
+.item-attendees-cell-label {
+ border: 0px;
+ margin: 0px 3px;
+ padding: 0px;
+}
+
+.item-organizer-cell {
+ padding: 0px;
+ margin-left: 6px;
+}
+
+/* this is for the itip icon setup in calendar */
+
+.itip-icon {
+ --itip-icon-partstat: -16px -16px; /* default: NEEDS-ACTION */
+ --itip-icon-role: 0px; /* default: REQ-PARTICIPANT */
+ --itip-icon-usertype: -32px; /* default: INDIVIDUAL */
+ width: 16px;
+ height: 16px;
+ max-height: 16px;
+ background-image: url(chrome://calendar-common/skin/calendar-itip-icons.svg),
+ url(chrome://calendar-common/skin/calendar-itip-icons.svg);
+ background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role);
+}
+.itip-icon[partstat="ACCEPTED"] {
+ --itip-icon-partstat: 0px 0px;
+}
+.itip-icon[partstat="DECLINED"] {
+ --itip-icon-partstat: 0px -16px;
+}
+.itip-icon[partstat="DELEGATED"] {
+ --itip-icon-partstat: 0px -32px;
+}
+.itip-icon[partstat="TENTATIVE"] {
+ --itip-icon-partstat: -16px 0px;
+}
+.itip-icon[usertype="INDIVIDUAL"] {
+ --itip-icon-usertype: -32px;
+}
+.itip-icon[usertype="GROUP"] {
+ --itip-icon-usertype: -48px;
+}
+.itip-icon[usertype="RESOURCE"] {
+ --itip-icon-usertype: -64px;
+}
+.itip-icon[usertype="ROOM"] {
+ --itip-icon-usertype: -80px;
+}
+.itip-icon[usertype="UNKNOWN"] {
+ --itip-icon-usertype: -96px;
+}
+.itip-icon[role="REQ-PARTICIPANT"] {
+ --itip-icon-role: 0px;
+}
+.itip-icon[role="OPT-PARTICIPANT"] {
+ --itip-icon-role: -16px;
+}
+.itip-icon[role="NON-PARTICIPANT"] {
+ --itip-icon-role: -32px;
+}
+.itip-icon[role="CHAIR"] {
+ --itip-icon-role: -32px;
+ --itip-icon-usertype: -16px;
+}
+
+/* the following will get obsolete once porting to new itip icons is complete */
+
+.status-icon > .menu-iconic-left,
+.role-icon > .menu-iconic-left,
+.usertype-icon > .menu-iconic-left {
+ visibility: inherit;
+}
+
+.status-icon {
+ margin: 0 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 48px 14px 36px);
+}
+
+.status-icon[disabled="true"] {
+ -moz-image-region: rect(14px 48px 28px 36px);
+}
+
+.status-icon[status="ACCEPTED"] {
+ -moz-image-region: rect(0px 12px 14px 0px);
+}
+.status-icon[status="ACCEPTED"][disabled="true"] {
+ -moz-image-region: rect(14px 12px 28px 0px);
+}
+
+.status-icon[status="DECLINED"] {
+ -moz-image-region: rect(0px 24px 14px 12px);
+}
+.status-icon[status="DECLINED"][disabled="true"] {
+ -moz-image-region: rect(14px 24px 28px 12px);
+}
+
+.status-icon[status="NEEDS-ACTION"] {
+ -moz-image-region: rect(0px 36px 14px 24px);
+}
+.status-icon[status="NEEDS-ACTION"][disabled="true"] {
+ -moz-image-region: rect(14px 36px 28px 24px);
+}
+
+.status-icon[status="TENTATIVE"] {
+ -moz-image-region: rect(0px 48px 14px 36px);
+}
+.status-icon[status="TENTATIVE"][disabled="true"] {
+ -moz-image-region: rect(14px 48px 28px 36px);
+}
+
+.role-icon {
+ margin: 0 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 159px 16px 138px);
+}
+
+.role-icon[disabled="true"] {
+ -moz-image-region: rect(0px 159px 16px 138px);
+}
+
+.role-icon[role="REQ-PARTICIPANT"] {
+ -moz-image-region: rect(0px 159px 16px 138px);
+}
+.role-icon[role="REQ-PARTICIPANT"][disabled="true"] {
+ -moz-image-region: rect(0px 159px 16px 138px);
+}
+
+.role-icon[role="OPT-PARTICIPANT"] {
+ -moz-image-region: rect(0px 180px 16px 159px);
+}
+.role-icon[role="OPT-PARTICIPANT"][disabled="true"] {
+ -moz-image-region: rect(0px 180px 16px 159px);
+}
+
+.role-icon[role="CHAIR"] {
+ -moz-image-region: rect(0px 201px 16px 180px);
+}
+.role-icon[role="CHAIR"][disabled="true"] {
+ -moz-image-region: rect(0px 201px 16px 180px);
+}
+
+.role-icon[role="NON-PARTICIPANT"] {
+ -moz-image-region: rect(0px 222px 16px 201px);
+}
+.role-icon[role="NON-PARTICIPANT"][disabled="true"] {
+ -moz-image-region: rect(0px 222px 16px 201px);
+}
+
+.usertype-icon,
+.usertype-icon[cutype="INDIVIDUAL"] {
+ margin: 0 3px;
+ list-style-image: url(chrome://calendar-common/skin/attendee-icons.png);
+ -moz-image-region: rect(0px 16px 16px 0px);
+}
+.usertype-icon[disabled="true"],
+.usertype-icon[cutype="INDIVIDUAL"][disabled="true"] {
+ -moz-image-region: rect(16px 16px 32px 0px);
+}
+
+.usertype-icon[cutype="GROUP"] {
+ -moz-image-region: rect(0px 32px 16px 16px);
+}
+.usertype-icon[cutype="GROUP"][disabled="true"] {
+ -moz-image-region: rect(16px 32px 32px 16px);
+}
+
+.usertype-icon[cutype="RESOURCE"] {
+ -moz-image-region: rect(0px 48px 16px 32px);
+}
+.usertype-icon[cutype="RESOURCE"][disabled="true"] {
+ -moz-image-region: rect(16px 48px 32px 32px);
+}
+
+.usertype-icon[cutype="ROOM"] {
+ -moz-image-region: rect(0px 64px 16px 48px);
+}
+.usertype-icon[cutype="ROOM"][disabled="true"] {
+ -moz-image-region: rect(16px 64px 32px 48px);
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-vista),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win7),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #calendar-event-dialog-inner .item-attendees-cell {
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ --attendees-currentColor: rgb(125, 162, 206);
+ }
+
+ #calendar-event-dialog-inner .item-attendees-cell:focus {
+ color: -moz-FieldText;
+ background-color: transparent;
+ -moz-border-top-colors: var(--attendees-focusBorder);
+ -moz-border-right-colors: var(--attendees-focusBorder);
+ -moz-border-left-colors: var(--attendees-focusBorder);
+ -moz-border-bottom-colors: var(--attendees-focusBottomBorder);
+ background-image: var(--attendees-focusImage);
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-vista),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win7) {
+ .item-attendees-cell {
+ border: 2px solid transparent;
+ border-radius: 3px;
+ --attendees-2ndBorderColor: rgba(255, 255, 255, .4);
+ --attendees-2ndBottomBorderColor: rgba(255, 255, 255, .6);
+ --attendees-focusBorder: var(--attendees-currentColor)
+ var(--attendees-2ndBorderColor);
+ --attendees-focusBottomBorder: var(--attendees-currentColor)
+ var(--attendees-2ndBottomBorderColor);
+ --attendees-focusImage: linear-gradient(rgba(131, 183, 249, .28),
+ rgba(131, 183, 249, .5));
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #calendar-event-dialog-inner .item-attendees-cell {
+ padding: 1px;
+ border: 1px solid transparent;
+ --attendees-focusColor: rgb(123, 195, 255);
+ --attendees-focusBorder: var(--attendees-focusColor);
+ --attendees-focusBottomBorder: var(--attendees-focusColor);
+ --attendees-focusImage: linear-gradient(rgb(205, 232, 255),
+ rgb(205, 232, 255));
+ }
+}
diff --git a/calendar/base/themes/common/calendar-creation-wizard.css b/calendar/base/themes/common/calendar-creation-wizard.css
new file mode 100644
index 000000000..b8f0948df
--- /dev/null
+++ b/calendar/base/themes/common/calendar-creation-wizard.css
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#customize-rows > row {
+ min-height: 26px;
+}
+
+.checkbox-no-label > .checkbox-label-box {
+ display: none;
+}
+
+#calendar-uri > .textbox-input-box > .textbox-search-icons {
+ display: none;
+}
diff --git a/calendar/base/themes/common/calendar-daypicker.css b/calendar/base/themes/common/calendar-daypicker.css
new file mode 100644
index 000000000..d86113d37
--- /dev/null
+++ b/calendar/base/themes/common/calendar-daypicker.css
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+daypicker {
+ -moz-binding: url(chrome://calendar/content/calendar-daypicker.xml#daypicker);
+ background-image: linear-gradient(rgba(0, 0, 0, .0) 5%, rgba(0, 0, 0, .20));
+ background-color: -moz-Field;
+ text-align: center;
+}
+
+daypicker[mode="monthly-days"] {
+ width: 32px;
+ height: 15px;
+}
+
+daypicker[mode="daypicker-weekday"] {
+ min-width: 36px;
+ height: 32px;
+}
+
+daypicker[mode="monthly-days"][bottom="true"][right="true"] {
+ width: 128px;
+ height: 15px;
+}
+
+daypicker:hover {
+ background-image: linear-gradient(rgba(255, 255, 255, .0), rgba(0, 0, 0, .10) 90%);
+ cursor: pointer;
+}
+
+daypicker:hover:active,
+daypicker[open="true"] {
+ background-image: linear-gradient(rgba(0, 0, 0, .15), rgba(0, 0, 0, .01) 15%);
+ cursor: pointer;
+}
+
+daypicker[disabled="true"],
+daypicker[disabled="true"][checked="true"],
+daypicker[disabled="true"]:hover,
+daypicker[disabled="true"]:hover:active,
+daypicker[disabled="true"][open="true"] {
+ background-image: linear-gradient(rgba(0, 0, 0, .0) 5%, rgba(0, 0, 0, .20));
+ color: GrayText;
+ cursor: default;
+ background-color: -moz-Dialog;
+}
+
+daypicker[checked="true"] {
+ background-image: linear-gradient(rgba(0, 0, 0, .30), rgba(255, 255, 255, .0) 35%);
+ background-color: Highlight;
+ color: HighlightText;
+}
diff --git a/calendar/base/themes/common/calendar-itip-icons.svg b/calendar/base/themes/common/calendar-itip-icons.svg
new file mode 100644
index 000000000..75b4655b3
--- /dev/null
+++ b/calendar/base/themes/common/calendar-itip-icons.svg
@@ -0,0 +1,121 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 112 48" width="112" height="48">
+
+ <!-- definitions -->
+ <style>
+ .req {
+ fill: #ffcc00;
+ stroke: #000000;
+ stroke-width: 0.5;
+ }
+ .opt {
+ fill: #cccccc;
+ stroke: #000000;
+ stroke-width: 0.5;
+ }
+ .non {
+ fill: #ffffff;
+ stroke: #000000;
+ stroke-width: 0.5;
+ }
+ .status {
+ fill:#ffffff;
+ stroke:#ffffff;
+ stroke-width:0.5;
+ }
+ </style>
+ <clipPath id="cut-off-bottom">
+ <rect x="0" y="0" width="16" height="16" />
+ </clipPath>
+ <symbol id="chairsymbol" clip-path="url(#cut-off-bottom)">
+ <ellipse cx="6" cy="16" rx="5.5" ry="8" fill="#000000" />
+ <circle cx="6" cy="6.25" r="4" fill="#ffffff"/>
+ <ellipse cx="10" cy="16" rx="5.5" ry="8" fill="#000000" />
+ <circle cx="10" cy="6.25" r="4" fill="#ffffff"/>
+ <ellipse cx="8" cy="16" rx="6.5" ry="9" fill="#ffcc00" />
+ <circle cx="8" cy="4.5" r="4" fill="#ffcc00"/>
+ <line x1="0.5" y1="15.75" x2="15.5" y2="15.75" />
+ </symbol>
+ <symbol id="individual" clip-path="url(#cut-off-bottom)">
+ <ellipse cx="8" cy="16" rx="7.5" ry="8.5" />
+ <circle cx="8" cy="5" r="4.5"/>
+ <line x1="0.5" y1="15.75" x2="15.5" y2="15.75" />
+ </symbol>
+ <symbol id="group" clip-path="url(#cut-off-bottom)">
+ <ellipse cx="5.75" cy="16" rx="5.5" ry="8" />
+ <circle cx="5.75" cy="6.5" r="4"/>
+ <ellipse cx="7.5" cy="16" rx="6.0" ry="8.5" />
+ <circle cx="7.5" cy="5.5" r="4"/>
+ <ellipse cx="9.25" cy="16" rx="6.25" ry="9" />
+ <circle cx="9.25" cy="4.5" r="4"/>
+ <line x1="0.25" y1="15.75" x2="15.55" y2="15.75" />
+ </symbol>
+ <symbol id="resource">
+ <rect x="5.25" y="0.5" rx="1" ry="1" width="10.25" height="12" />
+ <rect x="0.25" y="7" rx="1" ry="1" width="13" height="8" />
+ <circle cx="4.25" cy="11" r="2.5" style="fill:#ffffff" />
+ <line x1="8.25" y1="9" x2="12" y2="9" />
+ <line x1="8.25" y1="11" x2="12" y2="11" />
+ <line x1="8.25" y1="13" x2="12" y2="13" />
+ <rect x="1.25" y="15" width="4" height="1" style="fill:#000000; stroke-width:0" />
+ <rect x="8" y="15" width="4" height="1" style="fill:#000000; stroke-width:0" />
+ </symbol>
+ <symbol id="room">
+ <rect x="4" y="0.5" rx="0.25" ry="0.25" width="3" height="2" />
+ <rect x="8.5" y="0.5" rx="0.25" ry="0.25" width="3" height="2" />
+ <rect x="0.25" y="4.5" rx="0.25" ry="0.25" width="2" height="3" />
+ <rect x="0.25" y="8.5" rx="0.25" ry="0.25" width="2" height="3" />
+ <rect x="3.5" y="4" rx="1" ry="1" width="8.75" height="8" />
+ <rect x="13.5" y="4.5" rx="0.25" ry="0.25" width="2" height="3" />
+ <rect x="13.5" y="8.5" rx="0.25" ry="0.25" width="2" height="3" />
+ <rect x="4" y="13.5" rx="0.25" ry="0.25" width="3" height="2" />
+ <rect x="8.5" y="13.5" rx="0.25" ry="0.25" width="3" height="2" />
+ </symbol>
+ <symbol id="unknown">
+ <path d="m 7.8339844,11.558594 -2.4902344,0 q -0.00977,-0.53711 -0.00977,-0.654297 0,-1.2109376 0.4003906,-1.9921876 Q 6.1347656,8.1308594 7.3359375,7.1542969 8.5371094,6.1777344 8.7714844,5.875 9.1328125,5.3964844 9.1328125,4.8203125 q 0,-0.8007813 -0.6445312,-1.3671875 -0.6347657,-0.5761719 -1.7187501,-0.5761719 -1.0449218,0 -1.7480468,0.5957032 -0.703125,0.5957031 -0.9667969,1.8164062 l -2.5195313,-0.3125 Q 1.6425781,3.2285156 3.0195313,2.0078125 4.40625,0.78710937 6.6523437,0.78710937 q 2.3632813,0 3.7597653,1.24023443 1.396485,1.2304687 1.396485,2.8710937 0,0.9082031 -0.517578,1.71875 Q 10.783203,7.4277344 9.1035156,8.8242188 8.234375,9.546875 8.0195313,9.9863281 7.8144531,10.425781 7.8339844,11.558594 Z M 5.34375,15.25 l 0,-2.744141 2.7441406,0 0,2.744141 -2.7441406,0 z" />
+ </symbol>
+ <symbol id="status" style="stroke-width:0.25;">
+ <circle cx="11.5" cy="11.5" r="4.5" style="fill:#000000; stroke:#ffffff" />
+ <circle cx="11.5" cy="11.5" r="4.25" style="stroke:#000000" />
+ </symbol>
+
+ <!-- status icons -->
+ <g id="accepted" class="status">
+ <use style="fill:#00a000;" xlink:href="#status" x="0" y="0" />
+ <rect x="9" y="11" width="5" height="1" />
+ <rect x="11" y="9" width="1" height="5" />
+ </g>
+ <g id="tentative" class="status">
+ <use style="fill:#0000ff;" xlink:href="#status" x="16" y="0" />
+ <path d="m 27.933594,13 -0.996094,0 q -0.0039,-0.214844 -0.0039,-0.261719 0,-0.484375 0.160156,-0.796875 0.160156,-0.3125 0.640625,-0.703125 0.480469,-0.390625 0.574219,-0.511719 0.144531,-0.191406 0.144531,-0.421875 0,-0.3203125 -0.257813,-0.546875 -0.253906,-0.2304687 -0.6875,-0.2304687 -0.417968,0 -0.699218,0.2382812 -0.28125,0.2382813 -0.386719,0.7265625 l -1.007813,-0.125 q 0.04297,-0.6992187 0.59375,-1.1875 0.554688,-0.4882812 1.453125,-0.4882812 0.945313,0 1.503907,0.4960937 0.558593,0.4921875 0.558593,1.1484375 0,0.363281 -0.207031,0.6875 -0.203125,0.324219 -0.875,0.882813 -0.347656,0.289062 -0.433594,0.464843 -0.08203,0.175782 -0.07422,0.628907 z M 26.9375,14.25 l 0,-1.097656 1.097656,0 0,1.097656 -1.097656,0 z" />
+ </g>
+ <g id="declined" class="status">
+ <use style="fill:#ee0000;" xlink:href="#status" x="0" y="16" />
+ <rect x="9.5" y="26.75" width="4.25" height="1.5" />
+ </g>
+ <use id="needs-action" style="stroke-width:0.5; fill:#f4f444;" xlink:href="#status" x="16" y="16" />
+ <g id="delegated" class="status">
+ <use style="fill:#444444;" xlink:href="#status" x="0" y="32" />
+ <rect x="9" y="43" width="2.5" height="1" />
+ <polygon points="11.5,41, 11.5,46, 14.5,43.5" />
+ </g>
+
+ <!-- role/partstat icons -->
+ <use id="chair" class="req" xlink:href="#chairsymbol" x="16" y="32" />
+ <use id="individual-reqparticipant" class="req" xlink:href="#individual" x="32" y="0" />
+ <use id="group-reqparticipant" class="req" xlink:href="#group" x="48" y="0" />
+ <use id="resource-reqparticipant" class="req" xlink:href="#resource" x="64" y="0" />
+ <use id="room-reqparticipant" class="req" xlink:href="#room" x="80" y="0" />
+ <use id="unknown-reqparticipant" class="req" xlink:href="#unknown" x="96" y="0" />
+ <use id="individual-optparticipant" class="opt" xlink:href="#individual" x="32" y="16" />
+ <use id="group-optparticipant" class="opt" xlink:href="#group" x="48" y="16" />
+ <use id="resource-optparticipant" class="opt" xlink:href="#resource" x="64" y="16" />
+ <use id="room-optparticipant" class="opt" xlink:href="#room" x="80" y="16" />
+ <use id="unknown-optparticipant" class="opt" xlink:href="#unknown" x="96" y="16" />
+ <use id="individual-nonparticipant" class="non" xlink:href="#individual" x="32" y="32" />
+ <use id="group-nonparticipant" class="non" xlink:href="#group" x="48" y="32" />
+ <use id="resource-nonparticipant" class="non" xlink:href="#resource" x="64" y="32" />
+ <use id="room-nonparticipant" class="non" xlink:href="#room" x="80" y="32" />
+ <use id="unknown-nonparticipant" class="non" xlink:href="#unknown" x="96" y="32" />
+</svg> \ No newline at end of file
diff --git a/calendar/base/themes/common/calendar-management.css b/calendar/base/themes/common/calendar-management.css
new file mode 100644
index 000000000..ccc82d6ef
--- /dev/null
+++ b/calendar/base/themes/common/calendar-management.css
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+calendar-list-tree > tree > treechildren::-moz-tree-cell(color-treecol, color-default) {
+ background-color: #a8c2e1;
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-cell(color-treecol) {
+ margin: 1px;
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-cell(calendarname-treecol) {
+ margin-inline-start: 1px;
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(status-treecol, readonly) {
+ list-style-image: url(chrome://calendar-common/skin/calendar-status.png);
+ -moz-image-region: rect(0px, 14px, 14px, 0px);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(status-treecol, readfailed) {
+ list-style-image: url(chrome://calendar-common/skin/calendar-status.png);
+ -moz-image-region: rect(0px, 28px, 14px, 14px);
+}
+
+calendar-list-tree > tree {
+ border: none;
+ padding: 0;
+ margin: 4px 0;
+ -moz-border-top-colors: none;
+ -moz-border-right-colors: none;
+ -moz-border-bottom-colors: none;
+ -moz-border-left-colors: none;
+ -moz-appearance: none;
+}
+
+calendar-list-tree > tree > treecols > treecol[hideheader="true"],
+calendar-list-tree > tree > treecols > treecol[hideheader="true"] {
+ font-size: 0px;
+ border: none;
+ padding: 0;
+ max-height: 0px;
+ height: 0px;
+}
+
+calendar-list-tree > tree > treecols > treecol[anonid="scrollbar-spacer"]:-moz-system-metric(overlay-scrollbars) {
+ display: none;
+}
diff --git a/calendar/base/themes/common/calendar-occurrence-prompt.css b/calendar/base/themes/common/calendar-occurrence-prompt.css
new file mode 100644
index 000000000..21def598e
--- /dev/null
+++ b/calendar/base/themes/common/calendar-occurrence-prompt.css
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-occurrence-prompt {
+ padding: 0;
+ width: 25em;
+ height: 34ex;
+ min-width: 25em;
+ min-height: 34ex;
+ -moz-user-focus: ignore;
+}
+
+#occurrence-prompt-header {
+ height: 50px;
+ padding: 0 15px;
+ border-bottom: 2px groove ThreeDFace;
+ background-color: window;
+ color: windowtext;
+}
+
+#title-label {
+ font-weight: bold;
+}
+
+#accept-buttons-box {
+ padding: 0 18px;
+ border-bottom: 2px groove ThreeDFace;
+}
+
+
+.occurrence-accept-buttons {
+ list-style-image: url(chrome://calendar/skin/calendar-occurrence-prompt.png);
+ margin: 10px 0px;
+}
+
+.occurrence-accept-buttons > .button-box > .button-text {
+ margin: 0 3px !important;
+}
+
+#accept-buttons-box[action="edit"] > #accept-occurrence-button {
+ -moz-image-region: rect(0 20px 20px 0);
+}
+
+#accept-buttons-box[action="edit"] > #accept-parent-button {
+ -moz-image-region: rect(0 40px 20px 20px);
+}
+
+#accept-buttons-box[action="edit"] > #accept-allfollowing-button {
+ -moz-image-region: rect(0 60px 20px 40px);
+}
+
+#accept-buttons-box[action="delete"] > #accept-occurrence-button {
+ -moz-image-region: rect(0 80px 20px 60px);
+}
+
+#accept-buttons-box[action="delete"] > #accept-parent-button {
+ -moz-image-region: rect(0 100px 20px 80px);
+}
+
+#accept-buttons-box[action="delete"] > #accept-allfollowing-button {
+ -moz-image-region: rect(0 120px 20px 100px);
+}
+
+@media (-moz-os-version: windows-vista),
+ (-moz-os-version: windows-win7),
+ (-moz-os-version: windows-win8),
+ (-moz-os-version: windows-win10) {
+ .occurrence-accept-buttons {
+ list-style-image: url(chrome://calendar/skin/calendar-occurrence-prompt-aero.png);
+ }
+}
diff --git a/calendar/base/themes/common/calendar-printing.css b/calendar/base/themes/common/calendar-printing.css
new file mode 100644
index 000000000..4a0ae9a25
--- /dev/null
+++ b/calendar/base/themes/common/calendar-printing.css
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.main-table {
+ font-size: 26px;
+ font-weight: bold;
+}
+
+.day-name {
+ border: 1px solid #000;
+ background-color: #e0e0e0;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.day-box {
+ border: 1px solid black;
+ vertical-align: top;
+}
+
+.out-of-month {
+ background-color: gray !important;
+}
+
+.day-off {
+ background-color: #D3D3D3 !important;
+}
+
+.taskItem {
+ border-bottom: 1px solid #F3F3F3;
+}
+
+.tasks {
+ margin: 5px;
+}
+
+.taskList {
+ margin: 0px;
+ list-style-image: none;
+ list-style-type: none;
+ padding: 0px;
+}
diff --git a/calendar/base/themes/common/calendar-providerUninstall-dialog.css b/calendar/base/themes/common/calendar-providerUninstall-dialog.css
new file mode 100644
index 000000000..f788899c8
--- /dev/null
+++ b/calendar/base/themes/common/calendar-providerUninstall-dialog.css
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#provider-name-label {
+ font-weight: bold;
+ margin-inline-start: 3em;
+}
diff --git a/calendar/base/themes/common/calendar-task-tree.css b/calendar/base/themes/common/calendar-task-tree.css
new file mode 100644
index 000000000..846bbe309
--- /dev/null
+++ b/calendar/base/themes/common/calendar-task-tree.css
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.calendar-task-tree {
+ -moz-appearance: none;
+ background-color: -moz-Field;
+ color: -moz-FieldText;
+ border: 0;
+ margin: 0;
+}
+
+/* align the treechildren text */
+.calendar-task-tree > treechildren::-moz-tree-cell-text {
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(selected, focus) {
+ background-color: Highlight;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(inprogress) {
+ color: green;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(inprogress, selected, focus) {
+ background-color: green;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(inprogress, selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(overdue) {
+ color: red;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(overdue, selected, focus) {
+ background-color: red;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(overdue, selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(duetoday) {
+ color: WindowText;
+ font-weight: bold;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(duetoday, selected, focus) {
+ background-color: Highlight;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(duetoday, selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(future) {
+ color: WindowText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(future, selected, focus) {
+ background-color: Highlight;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(future, selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(completed) {
+ text-decoration: line-through;
+ font-style: italic;
+ color: WindowText;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-row(completed, selected, focus) {
+ background-color: Highlight;
+}
+
+.calendar-task-tree > treechildren::-moz-tree-cell-text(completed, selected, focus) {
+ color: HighlightText;
+}
+
+.calendar-task-tree-col-priority {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-priority, normalpriority),
+.todo-due-image-class {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-priority, highpriority),
+.todo-due-image-class[highpriority="true"] {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 52px 13px 39px);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-priority, lowpriority),
+.todo-due-image-class[lowpriority="true"] {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 26px 13px 13px);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-priority, selected, highpriority) {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 65px 13px 52px);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-priority, selected, lowpriority) {
+ list-style-image: url(chrome://calendar-common/skin/task-images.png);
+ -moz-image-region: rect(0 39px 13px 26px);
+}
+
+/* ::::: tree progress meter ::::: */
+
+.calendar-task-tree > treechildren::-moz-tree-progressmeter {
+ border: 1px solid ThreeDShadow;
+ color: Highlight;
+ background-color: -moz-field;
+}
+
+@media all and (-moz-windows-default-theme) {
+ .calendar-task-tree > treechildren::-moz-tree-progressmeter(hover),
+ .calendar-task-tree > treechildren::-moz-tree-progressmeter(selected) {
+ margin: 1px 4px;
+ }
+}
diff --git a/calendar/base/themes/common/calendar-task-view.css b/calendar/base/themes/common/calendar-task-view.css
new file mode 100644
index 000000000..13640a165
--- /dev/null
+++ b/calendar/base/themes/common/calendar-task-view.css
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-header-name-column,
+#calendar-task-details-attachment-row > hbox {
+ padding-inline-start: 0.1em;
+}
+
+#calendar-task-details-grid {
+ padding-top: 1px;
+ padding-inline-start: 0;
+ padding-inline-end: 0;
+ padding-bottom: 0.2em;
+}
+
+#other-actions-box {
+ padding-bottom: 0.3em;
+}
+
+#calendar-task-details {
+ min-height: 6ex;
+}
+
+#task-addition-box {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+#calendar-task-details-description {
+ -moz-appearance: textfield;
+ border: 1px solid;
+ margin: 0;
+ font-family: serif;
+ font-size: 16px;
+}
+
+.task-details-name {
+ text-align: right;
+ background-color: transparent;
+ border: none;
+}
+
+#calendar-task-details-grid > rows > .item-date-row > .headline {
+ font-weight: normal;
+}
+
+#calendar-task-details-attachment-row {
+ margin-top: 3px;
+}
+
+#calendar-task-details-attachment-rows {
+ max-height: 60px;
+}
+
+.task-details-value {
+ text-align: left;
+ background-color: transparent;
+ border: none;
+}
+
+#calendar-task-tree {
+ min-height: 98px;
+}
+
+#calendar-task-tree-detail {
+ border-top: 1px solid ThreeDShadow;
+ margin: 3px 0;
+}
+
+#view-task-edit-field {
+ margin: 5px;
+}
+
+.task-edit-field[readonly="true"] {
+ color: GrayText;
+}
+
+#calendar-task-details-title {
+ font-weight: bold;
+}
+
+#unifinder-task-edit-field {
+ margin: 3px;
+}
+
+#unifinder-todo-tree > .calendar-task-tree {
+ margin-bottom: 3px;
+}
+
+/* ::::: task actions toolbar ::::: */
+
+#task-actions-toolbox {
+ border: none;
+}
+
+#task-actions-toolbar {
+ -moz-appearance: none;
+ -moz-box-pack: end;
+ border: none;
+}
+
+#task-actions-toolbar toolbarpaletteitem toolbarseparator,
+#task-actions-toolbar toolbarseparator {
+ height: 26px;
+}
+
+#task-actions-toolbar toolbarspacer {
+ height: 20px;
+}
+
+window[toolboxId="task-actions-toolbox"] #wrapper-spring {
+ display: none;
+}
+
+window[toolboxId="task-actions-toolbox"] #smallicons,
+window[toolboxId="task-actions-toolbox"] button[icon="add"] {
+ display: none;
+}
+
+window[toolboxId="task-actions-toolbox"] #modelist menuitem:first-child {
+ display: none;
+}
+
+#task-actions-toolbox[doCustomization] {
+ background: grey;
+}
+
+#task-actions-toolbox[doCustomization] #task-actions-toolbar {
+ min-width: 100px;
+ min-height: 24px;
+}
+
+#calendar-add-task-button {
+ margin-inline-start: 5px;
+}
+
+#calendar-add-task-button > .toolbarbutton-text {
+ padding-inline-start: 5px;
+}
diff --git a/calendar/base/themes/common/calendar-toolbar-osxlion.svg b/calendar/base/themes/common/calendar-toolbar-osxlion.svg
new file mode 100644
index 000000000..c8c56c331
--- /dev/null
+++ b/calendar/base/themes/common/calendar-toolbar-osxlion.svg
@@ -0,0 +1,64 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 18 18">
+ <style>
+ path {
+ display: none;
+ }
+ path:target {
+ display: block;
+ stroke-width: 1;
+ }
+ .normal {
+ fill: url(#osx1);
+ fill-opacity: 1;
+ stroke: url(#osx2);
+ }
+ </style>
+ <defs>
+ <linearGradient
+ id="osx1"
+ x1="8"
+ y1="1"
+ x2="8"
+ y2="15"
+ gradientUnits="userSpaceOnUse">
+ <stop stop-color="#4f4f4f" stop-opacity="0.76" offset="0"/>
+ <stop stop-color="#717171" stop-opacity="0.6" offset="1"/>
+ </linearGradient>
+ <linearGradient
+ id="osx2"
+ x1="8"
+ y1="1"
+ x2="8"
+ y2="15"
+ gradientUnits="userSpaceOnUse">
+ <stop stop-color="#252525" stop-opacity="0.88" offset="0"/>
+ <stop stop-color="#505050" stop-opacity="0.68" offset="1"/>
+ </linearGradient>
+ </defs>
+ <path id="calendar-tab" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 4,2 6.5,0 -3,7 -2.5,0 2,-5 -3,0 z"/>
+ <path id="task-tab" class="normal" d="m 4.5,2.5 -2,0 0,14 13,0 0,-14 -2,0 0,2 1,0 0,11 -11,0 0,-11 1,0 z m 2,-1 5,0 0,3 -5,0 z M 5.1,10.8 6.7,9.2 8.5,10.7 12,7 l 1.5,1.5 -5,5 z"/>
+ <path id="synchronize" class="normal" d="m 11.5,1.9 0,3.7 c 3.7,1.8 1.7,5.5 -1,6.8 l -2,-1.9 0,6 5.7,0 -1.6,-1.6 C 18.5,12.5 18.2,2.7 11.5,1.9 Z m -3.8,3.8 1.8,1.8 0,-6 -5.8,0 L 5.3,3 C -0.9,6.3 0.4,15 6.5,16.1 l 0,-3.6 C 3,10.7 4.5,7 7.7,5.7 Z"/>
+ <path id="newevent" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 6,2 3,0 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 z"/>
+ <path id="newtask" class="normal" d="m 4.5,2.5 -2,0 0,14 13,0 0,-14 -2,0 0,2 1,0 0,11 -11,0 0,-11 1,0 z m 2,-1 5,0 0,3 -5,0 z m 1,5 3,0 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 z"/>
+ <path id="edit" class="normal" d="M 3.6,11.5 2.2,15.7 6.5,14.3 15.8,5 13,2.2 Z m 0,0 L 4.1,11 7,13.8 6.5,14.3 2.2,15.7 Z"/>
+ <path id="delete" class="normal" d="m 9,1.5 c 4.2,0 7.5,3.3 7.5,7.5 0,4.1 -3.3,7.5 -7.5,7.5 C 4.9,16.5 1.5,13.2 1.5,9 1.5,4.8 4.9,1.5 9,1.5 Z m -3,3 7.5,7.5 C 16.3,7.1 11,1.7 6,4.5 Z M 4.5,6 c -3.1,5.4 2.9,10 7.5,7.5 z"/>
+ <path id="today" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 6,2 3,0 0,4 2,0 -3.5,3 -3.5,-3 2,0 z"/>
+ <path id="print" class="normal" d="m 4.5,2.5 0,4 -1.5,0 c -0.8,0 -1.5,1.1 -1.5,2 l 0,3 c 0,1 1.4,2 2.5,2 l 1.5,0 0,-1 7,0 0,1 1.5,0 c 1,0 2.5,-1 2.5,-2 l 0,-3 c 0,-0.8 -0.6,-2 -1.5,-2 l -1.5,0 0,-4 z m 0,0 9,0 0,6 -9,0 z m 0,6 2,0 0,2 -2,0 z m 0,5.4 0,1.6 m -2.5,0 14,0 m -2.5,0 0,-1.6"/>
+ <path id="find" class="normal" d="M 8,2.5 C 5,2.5 2.5,5 2.5,8 c 0,3 2.5,5.5 5.5,5.5 3,0 5.5,-2.4 5.5,-5.5 C 13.5,5 11,2.5 8,2.5 Z m 0,2 c 2,0 3.5,1.5 3.5,3.5 0,2 -1.5,3.5 -3.5,3.5 C 6,11.5 4.5,10 4.5,8 4.5,6 6,4.5 8,4.5 Z m 4.5,7 -1,1 3,3 1,-1 z"/>
+ <path id="category" class="normal" d="M 4 2.5 C 3.3 2.5 2.5 3.3 2.5 4 L 2.5 8 L 10 15.5 L 15.5 10 L 8 2.5 L 4 2.5 z M 6 4 A 2 2 0 0 1 8 6 A 2 2 0 0 1 6 8 A 2 2 0 0 1 4 6 A 2 2 0 0 1 6 4 z"/>
+ <path id="complete" class="normal" d="M 1.5,10.5 3,8.5 6.7,11.7 C 9,7.8 12.6,5.6 16.5,3.5 l 0,1 c -4,3.1 -7,6.7 -9,11.2 z"/>
+ <path id="priority" class="normal" d="m 7.5,2.5 3,0 0,8 -3,0 z M 10.5,14 A 1.5,1.5 0 0 1 9,15.5 1.5,1.5 0 0 1 7.5,14 1.5,1.5 0 0 1 9,12.5 1.5,1.5 0 0 1 10.5,14 Z"/>
+ <path id="pane" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z"/>
+ <path id="save" class="normal" d="m 2.5,2.5 0,13 2,0 0,-6 8,0 0,6 3,0 0,-13 z m 2,0 9,0 0,5 -9,0 z m 2,9 4,0 0,4 -4,0 z"/>
+ <path id="save-close" class="normal" d="m 4.5,2.5 4.5,0 4.5,0 z m -2,0 0,8.2 3,-3.2 0.2,0 2.1,2 1.4,0 2,-2 -6.7,0 0,-5 z m 11,0 0,2.7 0.4,-0.4 1.6,0 0,-2.3 z M 15,7 8.5,13.2 5.5,10.4 4,12 l 4.5,4.5 8,-8 z m 0.5,5.3 -3,3 0,0.2 3,0 z m -13,1 0,2.2 2,0 0,-0.2 z"/>
+ <path id="address" class="normal" d="m 2.5,15.5 13,0 c 0,-3.4 -2.2,-3 -5,-3 l 0,-1 C 12.5,10.4 12.7,9.4 13,8 13,7.7 13.5,7.4 13.5,7 13.5,6.6 13.1,6.3 13,6 12.8,5.6 12.9,3.7 12,3 10.4,1.8 7.56,1.8 6,3 5,3.7 5.1,5.6 5,6 4.8,6.3 4.5,6.6 4.5,7 c 0,0.3 0.4,0.6 0.5,1 0.2,1.5 0.5,2.3 2.5,3.5 l 0,1 c -2.5,0 -5,-0.5 -5,3 z"/>
+ <path id="security" class="normal" d="m 9,2.5 c -3,0 -5,1 -5.5,5 l 0,1 -1,0 0,7 13,0 0,-7 -1,0 0,-1 c 0,-4 -3,-5 -5,-5 z m 0,2 c 3,0 3.5,1 3.5,4 l -7,0 c 0,-3 1,-4 3.5,-4 z"/>
+ <path id="attach" class="normal" d="m 13.5,2.5 0,10.5 -4,3.5 -4,-3.5 0,-11.5 6,0 0,10 -2,2 -2,-2 0,-8 2,0 0,6.5" style="fill:none; stroke-opacity:1"/>
+ <path id="status" class="normal" d="M 16.5,9 A 7.5,7.5 0 0 1 9,16.5 7.5,7.5 0 0 1 1.5,9 7.5,7.5 0 0 1 9,1.5 7.5,7.5 0 0 1 16.5,9 Z M 12.5,9 A 3.5,3.5 0 0 1 9,12.5 3.5,3.5 0 0 1 5.5,9 3.5,3.5 0 0 1 9,5.5 3.5,3.5 0 0 1 12.5,9 Z" style="fill-opacity:0.5"/>
+ <path id="freebusy" class="normal" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z m 0,0 c 0.7,0 1.5,0 2.5,0.5 l 0,4.5 4,0 0,5 -6,0 0,5 C 4.5,16.3 1.5,12.5 1.5,9 1.5,5.3 4.2,1.6 9,1.5 Z m -0.5,2 1,0 0,5 4,0 0,1 -5,0 z"/>
+ <path id="timezones" class="normal" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z M 7,3.8 6.3,4.6 6.3,5.7 7.1,6.8 7.8,8.2 9.4,8.3 10.4,6.1 9.8,3.8 11.3,1.9 c 1.5,0.5 3,1.5 3.8,3 l -3.4,2.5 1.6,-0.1 1.3,2 -0.3,1.8 -0.8,1.4 0.7,1.8 c -0.5,0.7 -1.5,1.3 -2.6,1.7 L 9.7,14 11.2,11.1 9.7,9.9 8.3,9 6.8,10.2 5.9,12.2 6,14 7,16 C 4.7,15.4 3.3,14 2.3,12.2 L 3.5,10.4 3.4,8.9 4.3,8 3.7,7.1 3.7,5.4 2.8,4.7 C 4,3 5.8,1.8 7.8,1.6 Z"/>
+ <path id="decline" class="normal" d="M 2.5,5 5,2.5 l 4,4 4,-4 2.5,2.5 -4,4 4,4 -2.5,2.5 -4,-4 -4,4 -2.5,-2.5 4,-4 z"/>
+ <path id="tentative" class="normal" d="m 8.2,6.5 -2.7,0 c 0,-1.4 0.5,-4 3.5,-4 2.3,0 4.4,1 4.5,4 0,2.8 -3,2.5 -3,5 l -3,0 C 7.4,8 10.3,8.5 10.2,6.5 10,5.5 9.3,5.5 9,5.5 c 0,0 -0.8,0 -0.8,1 z m -0.7,7 3,0 0,2 -3,0 z"/>
+</svg>
diff --git a/calendar/base/themes/common/calendar-toolbar.svg b/calendar/base/themes/common/calendar-toolbar.svg
new file mode 100644
index 000000000..8be505cf4
--- /dev/null
+++ b/calendar/base/themes/common/calendar-toolbar.svg
@@ -0,0 +1,151 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 18 18">
+ <style>
+ path {
+ display: none;
+ }
+ path:target {
+ display: block;
+ stroke-width: 1;
+ }
+ .normal {
+ fill: #505050;
+ fill-opacity: 0.8;
+ stroke: #404040;
+ }
+ @media (-moz-windows-default-theme) and (-moz-os-version: windows-win8) {
+ .normal {
+ fill: #797c80;
+ fill-opacity: 1;
+ stroke: #797c80;
+ stroke-opacity: 0;
+ }
+ }
+ @media (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ .normal {
+ fill: #4c4c4c;
+ fill-opacity: 1;
+ stroke: #4c4c4c;
+ stroke-opacity: 0;
+ }
+ }
+ @media (-moz-windows-default-theme) and (-moz-os-version: windows-vista),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win7) {
+ .normal {
+ fill: url(#win1);
+ fill-opacity: 1;
+ stroke: url(#win2);
+ }
+ }
+ @media (-moz-mac-yosemite-theme) {
+ .normal {
+ fill: #4d4d4d;
+ fill-opacity: 1;
+ stroke-opacity: 0;
+ }
+ }
+ .inverted {
+ fill: #fff;
+ fill-opacity: 1;
+ stroke: #111922;
+ }
+ .unread {
+ fill: #3971c3;
+ fill-opacity: 1;
+ stroke: #115174;
+ }
+ </style>
+ <defs>
+ <linearGradient
+ id="win1"
+ x1="8"
+ y1="1"
+ x2="8"
+ y2="15"
+ gradientUnits="userSpaceOnUse">
+ <stop stop-color="#3f4f5a" offset="0"/>
+ <stop stop-color="#7e8c97" offset="1"/>
+ </linearGradient>
+ <linearGradient
+ id="win2"
+ x1="8"
+ y1="1"
+ x2="8"
+ y2="15"
+ gradientUnits="userSpaceOnUse">
+ <stop stop-color="#0c1b25" offset="0"/>
+ <stop stop-color="#4f585f" stop-opacity="0.9" offset="1"/>
+ </linearGradient>
+ </defs>
+ <path id="calendar-tab" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 4,2 6.5,0 -3,7 -2.5,0 2,-5 -3,0 z"/>
+ <path id="calendar-tab-flat" class="normal" d="M 2,3 2,16 16,16 16,3 14,3 14,5 13,5 13,2 12,2 12,5 11,5 11,3 7,3 7,5 6,5 6,2 5,2 5,5 4,5 4,3 Z m 1,3 12,0 0,9 -12,0 z m 3,2 6,0 -3,6 -2.5,0 2,-4 -2.5,0 z"/>
+ <path id="calendar-tab-inverted" class="inverted" d="m 1.5,3.5 0,13 15,0 0,-13 -3,0 0,1 -3,0 0,-1 -3,0 0,1 -3,0 0,-1 z m 3,3 9,0 0,7 -9,0 z m 0,-5 0,3 3,0 0,-3 z m 6,0 0,3 3,0 0,-3 z m -5,5 7,0 -3,7 -3,0 1.7,-4 -2.7,0 z"/>
+ <path id="task-tab" class="normal" d="m 4.5,2.5 -2,0 0,14 13,0 0,-14 -2,0 0,2 1,0 0,11 -11,0 0,-11 1,0 z m 2,-1 5,0 0,3 -5,0 z M 5.1,10.8 6.7,9.2 8.5,10.7 12,7 l 1.5,1.5 -5,5 z"/>
+ <path id="task-tab-flat" class="normal" d="m 5,3 -2,0 0,13 12,0 0,-13 -2,0 0,3 1,0 0,9 L 4,15 4,6 5,6 Z M 6,2 12,2 12,6 6,6 Z M 5.4,11.3 6.7,9.7 8.5,11.2 11.5,7.5 13,9 8.5,14 Z"/>
+ <path id="task-tab-inverted" class="inverted" d="m 5.5,2.5 -3,0 0,14 13,0 0,-14 -3,0 0,3 0,0 0,8 -7,0 0,-8 0,0 z m 0,-1 7,0 0,4 -7,0 z m 0.1,9.3 1.5,-2.1 1.3,1.2 2.4,-3 1.7,1.6 -4,4.8 z"/>
+ <path id="synchronize" class="normal" d="m 11.5,1.9 0,3.7 c 3.7,1.8 1.7,5.5 -1,6.8 l -2,-1.9 0,6 5.7,0 -1.6,-1.6 C 18.5,12.5 18.2,2.7 11.5,1.9 Z m -3.8,3.8 1.8,1.8 0,-6 -5.8,0 L 5.3,3 C -0.9,6.3 0.4,15 6.5,16.1 l 0,-3.6 C 3,10.7 4.5,7 7.7,5.7 Z"/>
+ <path id="synchronize-flat" class="normal" d="m 11,2.4 0,3.7 c 3,1.6 2.4,4.4 0,5.8 L 9,10 l 0,6 5.7,0 -1.6,-1.6 C 17.6,12 17,3.2 11,2.4 Z M 7.2,6.2 9,8 9,2 3.2,2 4.8,3.5 C -0.4,6.7 2.3,14.4 7,15.6 L 7,12 C 4.1,10.2 4.3,7.3 7.2,6.2 Z"/>
+ <path id="synchronize-inverted" class="inverted" d="m 11.5,1.9 0,3.7 c 3.6,1.2 2.1,5.4 -0.7,6.5 L 8.5,9.5 l 0,7 6.7,0 -1.9,-2 C 18.6,11.6 18,2.8 11.5,1.9 Z M 7,6 9.5,8.5 l 0,-7 -6.8,0 2.2,2.1 C -0.9,6.8 0.6,14.8 6.5,16.1 l 0,-3.6 C 3.5,11 4,7.3 7,6 Z"/>
+ <path id="newevent" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 6,2 3,0 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 z"/>
+ <path id="newevent-flat" class="normal" d="M 2,3 2,16 16,16 16,3 14,3 14,5 13,5 13,2 12,2 12,5 11,5 11,3 7,3 7,5 6,5 6,2 5,2 5,5 4,5 4,3 Z m 1,3 12,0 0,9 -12,0 z m 5,2 2,0 0,2 2,0 0,2 -2,0 0,2 -2,0 0,-2 -2,0 0,-2 2,0 z"/>
+ <path id="newevent-inverted" class="inverted" d="m 1.5,3.5 0,13 15,0 0,-13 -3,0 0,1 -3,0 0,-1 -3,0 0,1 -3,0 0,-1 z m 3,3 9,0 0,7 -9,0 z m 0,-5 0,3 3,0 0,-3 z m 6,0 0,3 3,0 0,-3 z m 0,5 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 0,-2 z"/>
+ <path id="newtask" class="normal" d="m 4.5,2.5 -2,0 0,14 13,0 0,-14 -2,0 0,2 1,0 0,11 -11,0 0,-11 1,0 z m 2,-1 5,0 0,3 -5,0 z m 1,5 3,0 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 z"/>
+ <path id="newtask-flat" class="normal" d="m 5,3 -2,0 0,13 12,0 0,-13 -2,0 0,3 1,0 0,9 -10,0 0,-9 1,0 z m 1,-1 6,0 0,4 -6,0 z m 2,6 2,0 0,2 2,0 0,2 -2,0 0,2 -2,0 0,-2 -2,0 0,-2 2,0 z"/>
+ <path id="newtask-inverted" class="inverted" d="m 5.5,2.5 -3,0 0,14 13,0 0,-14 -3,0 0,3 0,0 0,8 -7,0 0,-8 0,0 z m 0,-1 7,0 0,4 -7,0 z m 2,5 3,0 0,2 2,0 0,3 -2,0 0,2 -3,0 0,-2 -2,0 0,-3 2,0 z"/>
+ <path id="edit" class="normal" d="M 3.6,11.5 2.2,15.7 6.5,14.3 15.8,5 13,2.2 Z m 0,0 L 4.1,11 7,13.8 6.5,14.3 2.2,15.7 Z"/>
+ <path id="edit-flat" class="normal" d="M 4.2,11 2.5,15.5 7,13.8 Z M 7.7,13 15.5,5.3 12.7,2.5 5,10.3 Z"/>
+ <path id="edit-inverted" class="inverted" d="M 3.6,11.5 2.2,15.7 6.5,14.3 15.8,4.9 13,2.1 Z m 1,-1 0.4,-0.4 2.8,2.8 -0.4,0.4 z"/>
+ <path id="delete" class="normal" d="m 9,1.5 c 4.2,0 7.5,3.3 7.5,7.5 0,4.1 -3.3,7.5 -7.5,7.5 C 4.9,16.5 1.5,13.2 1.5,9 1.5,4.8 4.9,1.5 9,1.5 Z m -3,3 7.5,7.5 C 16.3,7.1 11,1.7 6,4.5 Z M 4.5,6 c -3.1,5.4 2.9,10 7.5,7.5 z"/>
+ <path id="delete-inverted" class="inverted" d="m 9,1.5 c 4.2,0 7.5,3.3 7.5,7.5 0,4.1 -3.3,7.5 -7.5,7.5 C 4.9,16.5 1.5,13.2 1.5,9 1.5,4.8 4.9,1.5 9,1.5 Z M 7,5 13,11 C 14.5,6.6 11.2,3.6 7,5 Z M 5,7 c -1.6,4.3 1.5,7.7 6,6 z"/>
+ <path id="today" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z m 6,2 3,0 0,4 2,0 -3.5,3 -3.5,-3 2,0 z"/>
+ <path id="today-flat" class="normal" d="M 2,3 2,16 16,16 16,3 14,3 14,5 13,5 13,2 12,2 12,5 11,5 11,3 7,3 7,5 6,5 6,2 5,2 5,5 4,5 4,3 Z m 1,3 12,0 0,9 -12,0 z m 5,1 2,0 0,3 2,0 -3,4 -3,-4 2,0 z"/>
+ <path id="today-inverted" class="inverted" d="m 1.5,3.5 0,13 15,0 0,-13 -3,0 0,1 -3,0 0,-1 -3,0 0,1 -3,0 0,-1 z m 3,3 9,0 0,7 -9,0 z m 0,-5 0,3 3,0 0,-3 z m 6,0 0,3 3,0 0,-3 z m -3,5 3,0 0,3 3,0 -4.5,4 -4.5,-4 3,0 z"/>
+ <path id="print" class="normal" d="m 4.5,2.5 0,4 -1.5,0 c -0.8,0 -1.5,1.1 -1.5,2 l 0,3 c 0,1 1.4,2 2.5,2 l 1.5,0 0,-1 7,0 0,1 1.5,0 c 1,0 2.5,-1 2.5,-2 l 0,-3 c 0,-0.8 -0.6,-2 -1.5,-2 l -1.5,0 0,-4 z m 0,0 9,0 0,6 -9,0 z m 0,6 2,0 0,2 -2,0 z m 0,5.4 0,1.6 m -2.5,0 14,0 m -2.5,0 0,-1.6"/>
+ <path id="print-flat" class="normal" d="M 5,3 5,7 4,7 C 3.2,7 2,7.6 2,8.5 l 0,3 c 0,1 0.9,1.5 2,1.5 l 1,0 0,1 -1,0 0,1 10,0 0,-1 -1,0 0,-1 1,0 c 1,0 2,-0.5 2,-1.5 l 0,-3 C 16,7.7 14.8,7 14,7 l -1,0 0,-4 z m 1,1 6,0 0,4 -6,0 z m -1,5 1,0 0,1 -1,0 z m 1,3 6,0 0,2 -6,0 z"/>
+ <path id="print-inverted" class="inverted" d="m 3.5,2.5 0,5 -0.5,0 c -0.8,0 -1.5,1.1 -1.5,2 l 0,2 c 0,0.8 0.4,2 2.5,2 l -1.5,0 0,2 13,0 0,-2 -1.5,0 c 2,0 2.5,-1.1 2.5,-2 l 0,-2 c 0,-0.8 -0.6,-2 -1.5,-2 l -0.5,0 0,-5 z m 2,2 7,0 0,3 -7,0 z m -1,5 2,0 0,1 -2,0 z m 1,3 7,0 0,1 -7,0 z"/>
+ <path id="find" class="normal" d="M 8,2.5 C 5,2.5 2.5,5 2.5,8 c 0,3 2.5,5.5 5.5,5.5 3,0 5.5,-2.4 5.5,-5.5 C 13.5,5 11,2.5 8,2.5 Z m 0,2 c 2,0 3.5,1.5 3.5,3.5 0,2 -1.5,3.5 -3.5,3.5 C 6,11.5 4.5,10 4.5,8 4.5,6 6,4.5 8,4.5 Z m 4.5,7 -1,1 3,3 1,-1 z"/>
+ <path id="find-flat" class="normal" d="M 8 3 A 5 5 0 0 0 3 8 A 5 5 0 0 0 8 13 A 5 5 0 0 0 13 8 A 5 5 0 0 0 8 3 z M 8 4 A 4 4 0 0 1 12 8 A 4 4 0 0 1 8 12 A 4 4 0 0 1 4 8 A 4 4 0 0 1 8 4 z m 4,7 -1,1 3,3 1,-1 z"/>
+ <path id="find-inverted" class="inverted" d="M 11,12.7 14.3,16 16,14 12.7,10.9 C 13.1,10 13.5,9 13.5,8 13.5,5 11,2.5 8,2.5 5,2.5 2.5,5 2.5,8 c 0,3 2.5,5.5 5.5,5.5 1,0 2,0 3,-0.8 z M 8,5 c 1.6,0 3,1.3 3,3 0,1.6 -1.3,3 -3,3 C 6.3,11 5,9.6 5,8 5,6.3 6.3,5 8,5 Z"/>
+ <path id="category" class="normal" d="M 4 2.5 C 3.3 2.5 2.5 3.3 2.5 4 L 2.5 8 L 10 15.5 L 15.5 10 L 8 2.5 L 4 2.5 z M 6 4 A 2 2 0 0 1 8 6 A 2 2 0 0 1 6 8 A 2 2 0 0 1 4 6 A 2 2 0 0 1 6 4 z"/>
+ <path id="category-flat" class="normal" d="M 4.5,3 C 3.8,3 3,3.8 3,4.5 L 3,8 10,15.5 15.5,10 8,3 Z m 2,1.5 c 1.1,0 2,0.9 2,2 0,1.1 -0.9,2 -2,2 -1.1,0 -2,-0.9 -2,-2 0,-1.1 0.9,-2 2,-2 z"/>
+ <path id="category-inverted" class="inverted" d="M 4 2.5 C 3.3 2.5 2.5 3.3 2.5 4 L 2.5 8 L 10 15.5 L 15.5 10 L 8 2.5 L 4 2.5 z M 6 4 A 2 2 0 0 1 8 6 A 2 2 0 0 1 6 8 A 2 2 0 0 1 4 6 A 2 2 0 0 1 6 4 z"/>
+ <path id="complete" class="normal" d="M 1.5,10.5 3,8.5 6.7,11.7 C 9,7.8 12.6,5.6 16.5,3.5 l 0,1 c -4,3.1 -7,6.7 -9,11.2 z"/>
+ <path id="complete-flat" class="normal" d="m 2,10.5 1.5,-2 3.7,3 C 9.5,7.6 12.1,5.6 16,3.5 l 0,1 c -4,3.1 -8.5,11 -8.5,11 z"/>
+ <path id="complete-inverted" class="inverted" d="M 1.5,10.5 3,7.5 6.7,10.7 C 9,6.8 12.6,4.6 16.5,2.5 l 0,2 c -4,3.1 -7,6.7 -9,11.2 z"/>
+ <path id="priority" class="normal" d="m 7.5,2.5 3,0 0,8 -3,0 z M 10.5,14 A 1.5,1.5 0 0 1 9,15.5 1.5,1.5 0 0 1 7.5,14 1.5,1.5 0 0 1 9,12.5 1.5,1.5 0 0 1 10.5,14 Z"/>
+ <path id="priority-flat" class="normal" d="m 7,3 4,0 -1,7.9 -2,0 z m 3.5,11 c 0,0.8 -0.6,1.5 -1.5,1.5 -0.8,0 -1.5,-0.6 -1.5,-1.5 0,-0.8 0.6,-1.5 1.5,-1.5 0.8,0 1.5,0.6 1.5,1.5 z"/>
+ <path id="priority-inverted" class="inverted" d="m 7.5,2.5 3,0 0,8 -3,0 z m 0,10 3,0 0,3 -3,0 z"/>
+ <path id="pane" class="normal" d="m 1.5,2.5 0,14 15,0 0,-14 -2,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 -3,0 0,2 -2,0 0,-3.5 0,3.5 -2,0 0,-2 z m 0,3 15,0 0,11 -15,0 z"/>
+ <path id="pane-flat" class="normal" d="M 2,3 2,16 16,16 16,3 14,3 14,5 13,5 13,2 12,2 12,5 11,5 11,3 7,3 7,5 6,5 6,2 5,2 5,5 4,5 4,3 Z m 1,3 12,0 0,9 -12,0 z"/>
+ <path id="pane-inverted" class="inverted" d="m 0.5,2.5 0,15 17,0 0,-15 -4,0 0,1 -3,0 0,-1 -3,0 0,1 -3,0 0,-1 z m 2,3 13,0 0,10 -13,0 z m 2,-5 0,3 3,0 0,-3 z m 6,0 0,3 3,0 0,-3 z"/>
+ <path id="save" class="normal" d="m 2.5,2.5 0,13 2,0 0,-6 8,0 0,6 3,0 0,-13 z m 2,0 9,0 0,5 -9,0 z m 2,9 4,0 0,4 -4,0 z"/>
+ <path id="save-flat" class="normal" d="M 2,2 2,16 16,16 16,2 Z M 4,3 14,3 14,8 4,8 Z m 0,7 7,0 0,5 -7,0 z m 1,1 0,3 2,0 0,-3 z"/>
+ <path id="save-inverted" class="inverted" d="m 2.5,2.5 0,13 2.1,0 0,-5 6.9,0 0,5 4,0 0,-13 z m 2,2 9,0 0,3 -9,0 z m 0.1,6 6.9,0 0,5 -6.9,0 z"/>
+ <path id="save-close" class="normal" d="m 4.5,2.5 4.5,0 4.5,0 z m -2,0 0,8.2 3,-3.2 0.2,0 2.1,2 1.4,0 2,-2 -6.7,0 0,-5 z m 11,0 0,2.7 0.4,-0.4 1.6,0 0,-2.3 z M 15,7 8.5,13.2 5.5,10.4 4,12 l 4.5,4.5 8,-8 z m 0.5,5.3 -3,3 0,0.2 3,0 z m -13,1 0,2.2 2,0 0,-0.2 z"/>
+ <path id="save-close-flat" class="normal" d="M 2,2 2,16 16,16 16,2 Z M 4,3 14,3 14,7 4,7 Z M 4.5,11.5 6,10 8,12 12.5,8 14,9.7 8,15 Z"/>
+ <path id="save-close-inverted" class="inverted" d="m 2.5,2.5 0,13 13,0 0,-13 z m 2,1.9 9,0 0,3.1 -9,0 z m 0.7,7.3 0.7,-0.6 1.7,1.6 4.4,-3.1 0.6,0.8 -5.1,3.6 z"/>
+ <path id="address" class="normal" d="m 2.5,15.5 13,0 c 0,-3.4 -2.2,-3 -5,-3 l 0,-1 C 12.5,10.4 12.7,9.4 13,8 13,7.7 13.5,7.4 13.5,7 13.5,6.6 13.1,6.3 13,6 12.8,5.6 12.9,3.7 12,3 10.4,1.8 7.56,1.8 6,3 5,3.7 5.1,5.6 5,6 4.8,6.3 4.5,6.6 4.5,7 c 0,0.3 0.4,0.6 0.5,1 0.2,1.5 0.5,2.3 2.5,3.5 l 0,1 c -2.5,0 -5,-0.5 -5,3 z"/>
+ <path id="address-flat" class="normal" d="m 3,15 12,0 c 0,-3 -1.2,-3 -4,-3 l 0,-1 C 13,9.9 12.7,9.4 13,8 13,7.7 13.5,7.4 13.5,7 13.5,6.6 13.1,6.3 13,6 12.8,5.6 12.9,3.7 12,3 10.4,1.8 7.56,1.8 6,3 5,3.7 5.1,5.6 5,6 4.8,6.3 4.5,6.6 4.5,7 4.5,7.3 4.9,7.6 5,8 5.2,9.5 5,9.8 7,11 l 0,1 c -2.5,0 -4,0 -4,3 z"/>
+ <path id="address-inverted" class="inverted" d="m 2.5,15.5 13,0 c 0,-3.4 -2.2,-3 -5,-3 l 0,-1 C 12.5,10.4 12.7,9.4 13,8 13,7.7 13.5,7.4 13.5,7 13.5,6.6 13.1,6.3 13,6 12.8,5.6 12.9,3.7 12,3 10.4,1.8 7.56,1.8 6,3 5,3.7 5.1,5.6 5,6 4.8,6.3 4.5,6.6 4.5,7 c 0,0.3 0.4,0.6 0.5,1 0.2,1.5 0.5,2.3 2.5,3.5 l 0,1 c -2.5,0 -5,-0.5 -5,3 z"/>
+ <path id="security" class="normal" d="m 9,2.5 c -3,0 -5,1 -5.5,5 l 0,1 -1,0 0,7 13,0 0,-7 -1,0 0,-1 c 0,-4 -3,-5 -5,-5 z m 0,2 c 3,0 3.5,1 3.5,4 l -7,0 c 0,-3 1,-4 3.5,-4 z"/>
+ <path id="security-flat" class="normal" d="M 9,3 C 6.25,3 4,4 4,7.5 L 4,9 3,9 3,15 15,15 15,9 14,9 14,7.5 C 14,4 11.5,3 9,3 Z m 0,2 c 3,0 3,1 3,4 L 6,9 C 6,6 6,5 9,5 Z"/>
+ <path id="security-inverted" class="inverted" d="m 9,2.5 c -2.75,0 -5.5,0.75 -5.5,5 l 0,1 -1,0 0,7 13,0 0,-7 -1,0 0,-1 c 0,-4 -3,-5 -5.5,-5 z m 0,3 c 2.5,0 2.5,1 2.5,3 l -5,0 c 0,-2 0,-3 2.5,-3 z"/>
+ <path id="attach" class="normal" d="m 13.5,2.5 0,10.5 -4,3.5 -4,-3.5 0,-11.5 6,0 0,10 -2,2 -2,-2 0,-8 2,0 0,6.5" style="fill:none; stroke-opacity:1"/>
+ <path id="attach-inverted" class="inverted" d="m 11.5,3 2,0 0,10 L 9,16.5 4.5,13 l 0,-11.5 7,0 z m 0,0.5 0,8.5 -2.5,2 -2.5,-2 0,-8.5 3,0 0,7.5 L 9,11.4 8.5,11 8.5,4"/>
+ <path id="status" class="normal" d="M 16.5,9 A 7.5,7.5 0 0 1 9,16.5 7.5,7.5 0 0 1 1.5,9 7.5,7.5 0 0 1 9,1.5 7.5,7.5 0 0 1 16.5,9 Z M 12.5,9 A 3.5,3.5 0 0 1 9,12.5 3.5,3.5 0 0 1 5.5,9 3.5,3.5 0 0 1 9,5.5 3.5,3.5 0 0 1 12.5,9 Z" style="fill-opacity:0.5"/>
+ <path id="status-flat" class="normal" d="M 9 2 A 7 7 0 0 0 2 9 A 7 7 0 0 0 9 16 A 7 7 0 0 0 16 9 A 7 7 0 0 0 9 2 z M 9 5 A 4 4 0 0 1 13 9 A 4 4 0 0 1 9 13 A 4 4 0 0 1 5 9 A 4 4 0 0 1 9 5 z M 12,9 A 3,3 0 0 1 9,12 3,3 0 0 1 6,9 3,3 0 0 1 9,6 3,3 0 0 1 12,9 Z"/>
+ <path id="status-inverted" class="inverted" d="M 16.5,9 A 7.5,7.5 0 0 1 9,16.5 7.5,7.5 0 0 1 1.5,9 7.5,7.5 0 0 1 9,1.5 7.5,7.5 0 0 1 16.5,9 Z M 12.5,9 A 3.5,3.5 0 0 1 9,12.5 3.5,3.5 0 0 1 5.5,9 3.5,3.5 0 0 1 9,5.5 3.5,3.5 0 0 1 12.5,9 Z"/>
+ <path id="freebusy" class="normal" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z m 0,0 c 0.7,0 1.5,0 2.5,0.5 l 0,4.5 4,0 0,5 -6,0 0,5 C 4.5,16.3 1.5,12.5 1.5,9 1.5,5.3 4.2,1.6 9,1.5 Z m -0.5,2 1,0 0,5 4,0 0,1 -5,0 z"/>
+ <path id="freebusy-flat" class="normal" d="M 9,2 C 4.8,2 2,4.8 2,9 c 0,4.1 4,7 7,7 4,0 7,-2.8 7,-7 C 16,4.8 13.1,2 9,2 Z m 0,1 c 0.7,0 1,0 2,0.3 l 0,3.7 3,0 0,4 -5,0 0,4 C 5,15 3,11 3,9 3,6 5,3 9,3 Z m -1,1 2,0 0,4 3,0 0,2 -5,0 z"/>
+ <path id="freebusy-inverted" class="inverted" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z m -1.5,3.7 0,-1.7 3,0 0,4 4,0 0,3 -5,0 0,3 C 6.2,13.4 4.4,11.5 4.5,9 4.5,6.4 6,5.2 7.5,5.2 Z m 0,-1.7 3,0 0,4 4,0 0,3 -7,0 z"/>
+ <path id="timezones" class="normal" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z M 7,3.8 6.3,4.6 6.3,5.7 7.1,6.8 7.8,8.2 9.4,8.3 10.4,6.1 9.8,3.8 11.3,1.9 c 1.5,0.5 3,1.5 3.8,3 l -3.4,2.5 1.6,-0.1 1.3,2 -0.3,1.8 -0.8,1.4 0.7,1.8 c -0.5,0.7 -1.5,1.3 -2.6,1.7 L 9.7,14 11.2,11.1 9.7,9.9 8.3,9 6.8,10.2 5.9,12.2 6,14 7,16 C 4.7,15.4 3.3,14 2.3,12.2 L 3.5,10.4 3.4,8.9 4.3,8 3.7,7.1 3.7,5.4 2.8,4.7 C 4,3 5.8,1.8 7.8,1.6 Z"/>
+ <path id="timezones-flat" class="normal" d="M 9,2 C 4.8,2 2,4.8 2,9 c 0,4.1 3,7 7,7 4,0 7,-2.8 7,-7 C 16,4.8 13.1,2 9,2 Z m -2,2.3 -0.7,0.8 0,1.1 0.8,1.1 0.7,0.7 1.6,0 1,-1.4 -0.6,-2.3 1.4,-1 c 0,0 1,0 2.4,1.6 l -2.4,2.5 1.3,0.5 1.6,1.4 -0.3,1.8 -1,1.2 1,0.8 -2,1.4 -1.9,-1 L 11.5,11 8.3,9 5.9,11.8 6,13.1 7,14.5 C 6,14 4,13 3.9,12.2 L 4.7,10.5 3.9,8.4 4.8,7.5 4.2,6.6 4.2,5.4 4.3,4.8 C 5,4 6,3 8,3 Z"/>
+ <path id="timezones-inverted" class="inverted" d="M 9,1.5 C 4.8,1.5 1.5,4.8 1.5,9 1.5,13.1 5.2,16.3 9,16.5 13,16.7 16.5,13.2 16.5,9 16.5,4.8 13.1,1.5 9,1.5 Z m -2.8,3.3 0.8,2.8 L 7.5,9 9.9,8.6 10,5.7 9.8,3.6 c 1.5,0.5 1.8,0.1 2.6,1.6 l -1,2 0.3,2.7 2.6,-0.7 C 14.2,12 12.6,13.4 10.2,14.3 L 11.1,13.3 11,11.2 10.1,8.7 7,9 5.8,13.4 8,14.5 c -2.3,-0.6 -3.5,-2 -4.5,-3.8 C 5.6,9.3 6,8 4.3,5.7 5.6,4.3 6,4 7.7,3.5 Z"/>
+ <path id="decline" class="normal" d="M 2.5,5 5,2.5 l 4,4 4,-4 2.5,2.5 -4,4 4,4 -2.5,2.5 -4,-4 -4,4 -2.5,-2.5 4,-4 z"/>
+ <path id="decline-flat" class="normal" d="M 3,5.5 5.5,3 9,6.5 12.5,3 15,5.5 11.5,9 15,12.5 12.5,15 9,11.5 5.5,15 3,12.5 6.5,9 Z"/>
+ <path id="decline-inverted" class="inverted" d="M 2.5,5 5,2.5 l 4,4 4,-4 2.5,2.5 -4,4 4,4 -2.5,2.5 -4,-4 -4,4 -2.5,-2.5 4,-4 z"/>
+ <path id="tentative" class="normal" d="m 8.2,6.5 -2.7,0 c 0,-1.4 0.5,-4 3.5,-4 2.3,0 4.4,1 4.5,4 0,2.8 -3,2.5 -3,5 l -3,0 C 7.4,8 10.3,8.5 10.2,6.5 10,5.5 9.3,5.5 9,5.5 c 0,0 -0.8,0 -0.8,1 z m -0.7,7 3,0 0,2 -3,0 z"/>
+ <path id="tentative-flat" class="normal" d="M 8,7 6,7 C 6,5.6 6,3 9,3 c 2,0 4,0 4,3.5 0,3 -3,3 -3,5.5 L 8,12 C 8,8.5 11.1,8.5 11,6.5 11,5 10,5 9,5 8,5 8,6 8,7 Z m 0,6 2,0 0,2 -2,0 z"/>
+ <path id="tentative-inverted" class="inverted" d="m 8.2,6.5 -2.7,0 c 0,-1.4 0.5,-4 3.5,-4 2.3,0 4.4,1 4.5,4 0,2.8 -3,2.5 -3,5 l -3,0 C 7.4,8 10.3,8.5 10.2,6.5 10,5.5 9.3,5.5 9,5.5 c 0,0 -0.8,0 -0.8,1 z m -0.7,7 3,0 0,2 -3,0 z"/>
+</svg>
diff --git a/calendar/base/themes/common/calendar-unifinder.css b/calendar/base/themes/common/calendar-unifinder.css
new file mode 100644
index 000000000..eb101f2a4
--- /dev/null
+++ b/calendar/base/themes/common/calendar-unifinder.css
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* only format Unifinder lists */
+#unifinder-search-results-tree > treechildren::-moz-tree-cell-text(highpriority) {
+ font-weight: bold;
+}
+
+#unifinder-search-results-tree > treechildren::-moz-tree-cell-text(lowpriority) {
+ font-style: italic;
+ color: GrayText !important;
+ background-color: -moz-field;
+}
+
+/* workaround to avoid Window Flick */
+#unifinder-search-results-tree {
+ -moz-appearance: none;
+ background-color: -moz-Field;
+ color: -moz-FieldText;
+ min-height: 92px;
+ border: 0;
+ -moz-border-bottom-colors: ThreeDHighlight ThreeDLightShadow;
+ border-bottom: 1px solid;
+ margin: 0;
+}
+
+.unifinder-closebutton {
+ list-style-image: url("chrome://global/skin/icons/close.png");
+ -moz-image-region: rect(0 16px 16px 0);
+}
+
+.unifinder-closebutton:hover {
+ -moz-image-region: rect(0 32px 16px 16px);
+}
+
+.unifinder-closebutton:hover:active {
+ -moz-image-region: rect(0 48px 16px 32px);
+}
diff --git a/calendar/base/themes/common/calendar-views.css b/calendar/base/themes/common/calendar-views.css
new file mode 100644
index 000000000..9a5bb8f9e
--- /dev/null
+++ b/calendar/base/themes/common/calendar-views.css
@@ -0,0 +1,955 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+window {
+ --viewColor: #000;
+ --viewBackground: #fff;
+ --viewBorderColor: #d2d2d2;
+ --viewDoubleBorderColor: var(--viewBackground) var(--viewBorderColor);
+ --viewHighlightBorderColor: #67acd8;
+ --viewTodayBorderColor: #7fb9ee;
+ --viewTodayBackground: #e1f0fd;
+ --viewTodayLabelColor: #616163;
+ --viewTodayLabelBackground: #d2e3f3;
+ --viewTodayLabelSelectedColor: #616163;
+ --viewTodayLabelSelectedBackground: #d2e3f3;
+ --viewTodayOffBackground: #d7e8f8;
+ --viewTodayDayLabelBackground: #d2e3f3;
+ --viewTodayWeekendBackground: #e1f0fd;
+ --viewWeekendBackground: #f7ffe3;
+ --viewHeaderSelectedBackground: #fffcd8;
+ --viewDayBoxSelectedBackground: #fffcd8;
+ --viewDayBoxOffSelectedBackground: #f2edb2;
+ --viewDayBoxOtherSelectedBackground: #fffcd8;
+ --viewMonthCurrentBackground: #f7f7f7;
+ --viewMonthOtherBackground: #f3f3f3;
+ --viewMonthDayBoxSelectedColor: #616163;
+ --viewMonthDayBoxSelectedBackground: #f2edb2;
+ --viewMonthDayBoxLabelColor: #616163;
+ --viewMonthDayBoxLabelTexture: none;
+ --viewMonthDayBoxWeekLabel: #aaaaaa;
+ --viewMonthDayOtherBackground: #e8e8e8;
+ --viewMonthDayOtherLabelBackground: #ddd;
+ --viewMonthDayOffLabelBackground: #eaf7ca;
+ --viewOffTimeBackground: #f0f0f0;
+ --viewTimeBoxColor: #6a6969;
+ --viewDayLabelSelectedColor: #000;
+ --viewDayLabelSelectedBackground: #fffabc;
+ --viewDragboxColor: -moz-dialogtext;
+ --viewDragboxBackground: linear-gradient(#fe4b22, #feb822);
+ --viewDropshadowBackground: #ffa47d;
+}
+
+window[systemcolors] {
+ --viewColor: WindowText;
+ --viewBackground: -moz-Field;
+ --viewBorderColor: ThreeDShadow;
+ --viewHighlightBorderColor: Highlight;
+ --viewTodayBorderColor: Highlight;
+ --viewTodayBackground: -moz-Field;
+ --viewTodayLabelColor: Highlight;
+ --viewTodayLabelBackground: rgba(0, 0, 0, .2);
+ --viewTodayLabelSelectedColor: HighlightText;
+ --viewTodayLabelSelectedBackground: Highlight;
+ --viewTodayOffBackground: ButtonFace;
+ --viewTodayDayLabelBackground: ButtonFace;
+ --viewTodayWeekendBackground: ButtonFace;
+ --viewWeekendBackground: rgba(0, 0, 0, .1);
+ --viewHeaderSelectedBackground: ButtonFace;
+ --viewDayBoxSelectedBackground: -moz-Field;
+ --viewDayBoxOffSelectedBackground: rgba(0, 0, 0, .2);
+ --viewDayBoxOtherSelectedBackground: rgba(0, 0, 0, .2);
+ --viewMonthCurrentBackground: -moz-Field;
+ --viewMonthOtherBackground: ButtonFace;
+ --viewMonthDayBoxSelectedColor: HighlightText;
+ --viewMonthDayBoxSelectedBackground: Highlight;
+ --viewMonthDayBoxLabelColor: WindowText;
+ --viewMonthDayBoxLabelTexture: linear-gradient(rgba(0, 0, 0, .05), rgba(0, 0, 0, .05));
+ --viewMonthDayBoxWeekLabel: GrayText;
+ --viewMonthDayOtherBackground: ButtonFace;
+ --viewMonthDayOtherLabelBackground: ButtonFace;
+ --viewMonthDayOffLabelBackground: rgba(0, 0, 0, .1);
+ --viewOffTimeBackground: rgba(0, 0, 0, .1);
+ --viewTimeBoxColor: GrayText;
+ --viewDayLabelSelectedColor: HighlightText;
+ --viewDayLabelSelectedBackground: Highlight;
+ --viewDragboxColor: GrayText;
+ --viewDragboxBackground: Highlight;
+ --viewDropshadowBackground: Highlight !important;
+}
+
+/* Core */
+calendar-category-box:not([categories]) {
+ display: none;
+}
+
+.calendar-category-box-gradient {
+ width: 7px;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.38), transparent) !important;
+ border-left: 1px solid rgba(255, 255, 255, 0.38);
+}
+
+.calendar-item-image {
+ list-style-image: url(chrome://calendar-common/skin/day-box-item-image.png);
+ padding-top: 2px;
+ padding-bottom: 2px;
+ margin-inline-end: 4px;
+ display: none;
+}
+
+.calendar-item-image[itemType="todo"] {
+ -moz-image-region: rect(0px 11px 11px 0px);
+ display: -moz-box;
+}
+
+.calendar-item-image[itemType="todo"][todoType="start"] {
+ -moz-image-region: rect(0px 18px 11px 0px);
+}
+
+.calendar-item-image[itemType="todo"][todoType="end"] {
+ -moz-image-region: rect(0px 36px 11px 18px);
+}
+
+.calendar-item-image[itemType="todo"][progress="completed"] {
+ -moz-image-region: rect(0px 47px 11px 36px);
+}
+
+.calendar-item-image[itemType="todo"][progress="completed"][todoType="start"] {
+ -moz-image-region: rect(0px 54px 11px 36px);
+}
+
+.calendar-item-image[itemType="todo"][progress="completed"][todoType="end"] {
+ -moz-image-region: rect(0px 72px 11px 54px);
+}
+
+/* Multiday view */
+
+/* Margin that allows event creation by click and drag when the time slot is
+ full of events. On the right side in normal view ... */
+.multiday-column-box-stack > .multiday-column-top-box[orient="horizontal"] {
+ margin-inline-end: 5px;
+}
+/* ... and on bottom in rotate view. */
+.multiday-column-box-stack > .multiday-column-top-box[orient="vertical"] {
+ margin-bottom: 5px;
+}
+
+/* .. and on the right side in the header container in normal view */
+calendar-header-container:not([rotated]) {
+ padding-right: 6px;
+ padding-left: 1px;
+}
+/* ... and on the bottom in rotated view. */
+calendar-header-container[rotated] {
+ padding-top: 1px;
+ padding-bottom: 5px;
+}
+
+calendar-event-column {
+ background-color: var(--viewBackground);
+}
+
+calendar-event-column[orient="horizontal"] {
+ border-top: 1px solid var(--viewBorderColor);
+}
+
+calendar-event-column[orient="vertical"] {
+ border-left: 1px solid var(--viewBorderColor);
+}
+
+calendar-event-column[orient="horizontal"][relation="today"] {
+ border-top: 1px solid var(--viewHighlightBorderColor);
+ border-bottom: 1px solid var(--viewHighlightBorderColor);
+ margin-bottom: -1px;
+ position: relative;
+}
+
+calendar-event-column[orient="vertical"][relation="today"] {
+ border-left: 1px solid var(--viewHighlightBorderColor);
+ border-right: 1px solid var(--viewHighlightBorderColor);
+ margin-inline-end: -1px;
+ position: relative;
+}
+
+calendar-header-container {
+ background-color: var(--viewBackground);
+ border-left: 1px solid var(--viewBorderColor);
+}
+
+calendar-header-container[rotated] {
+ max-width: 150px;
+}
+
+calendar-header-container[weekend="true"],
+.calendar-event-column-linebox[weekend="true"] {
+ background-color: var(--viewWeekendBackground);
+}
+
+.calendar-event-column-linebox[off-time="true"] {
+ background-color: var(--viewOffTimeBackground);
+}
+
+.calendar-event-column-linebox[off-time="true"][weekend="true"] {
+ background-color: var(--viewMonthDayOffLabelBackground);
+}
+
+calendar-header-container[relation="today"],
+.calendar-event-column-linebox[relation="today"],
+calendar-day-label[orient][relation="today"] {
+ background-color: var(--viewTodayBackground);
+}
+
+calendar-header-container[relation="today"] {
+ border-left: 1px solid var(--viewTodayBorderColor);
+ border-right: 1px solid var(--viewTodayBorderColor);
+ margin-inline-end: -1px;
+ position: relative;
+}
+
+calendar-header-container[relation="today"][rotated="true"] {
+ border-top: 1px solid var(--viewTodayBorderColor);
+ border-bottom: 1px solid var(--viewTodayBorderColor) !important;
+ border-right: 1px solid var(--viewBorderColor);
+ margin-top: -1px;
+ position: relative;
+}
+
+calendar-header-container[selected="true"],
+.calendar-event-column-linebox[selected="true"] {
+ background-color: var(--viewHeaderSelectedBackground);
+}
+
+calendar-header-container[weekend="true"][relation="today"],
+.calendar-event-column-linebox[weekend="true"][relation="today"] {
+ background-color: var(--viewTodayWeekendBackground);
+}
+
+.calendar-event-column-linebox[off-time="true"][relation="today"] {
+ background-color: var(--viewTodayOffBackground);
+}
+
+.multiday-view-header-day-box[orient="vertical"] .calendar-event-column-header {
+ border-bottom: 1px solid var(--viewBorderColor);
+ min-width: 100px;
+}
+
+calendar-header-container[weekend="true"][selected="true"],
+.calendar-event-column-linebox[weekend="true"][selected="true"] {
+ background-color: var(--viewHeaderSelectedBackground);
+}
+
+.calendar-event-column-linebox[off-time="true"][selected="true"] {
+ background-color: var(--viewDayBoxOffSelectedBackground);
+}
+
+.calendar-event-column-linebox[orient="horizontal"] {
+ border-right: 1px solid var(--viewBorderColor);
+}
+
+.calendar-event-column-linebox[orient="vertical"] {
+ border-bottom: 1px solid var(--viewBorderColor);
+}
+
+.calendar-event-column-linebox[orient="horizontal"][relation="today"]:last-child {
+ border-right: 1px solid var(--viewHighlightBorderColor);
+}
+
+.calendar-event-column-linebox[orient="vertical"][relation="today"]:last-child {
+ border-bottom: 1px solid var(--viewHighlightBorderColor);
+}
+
+/* Make sure we extend the bold line separating scrollable and non-scrollable
+ areas over the timebar. */
+.multiday-view-header-time-spacer[orient="horizontal"] {
+ border-bottom: 2px solid var(--viewBorderColor);
+ border-right: 2px solid;
+ -moz-border-right-colors: var(--viewDoubleBorderColor);
+}
+
+.multiday-view-header-time-spacer[orient="vertical"] {
+ border-right: 2px solid;
+ -moz-border-right-colors: var(--viewDoubleBorderColor);
+}
+
+.multiday-view-label-box[orient="horizontal"] > .multiday-view-label-time-spacer {
+ border-right: 2px solid;
+ -moz-border-right-colors: var(--viewDoubleBorderColor);
+}
+
+.multiday-view-header-day-box[orient="horizontal"] {
+ border-right: 1px solid var(--viewBorderColor);
+ border-bottom: 2px solid var(--viewBorderColor);
+ overflow-x: hidden;
+ overflow-y: auto;
+ max-height: 120px;
+}
+
+.multiday-view-header-day-box[orient="horizontal"][todaylastinview="true"] {
+ border-right: 1px solid var(--viewHighlightBorderColor);
+}
+
+/* Make sure the box for day-labels appears to end before the scrollbar. */
+.multiday-view-label-day-box[orient="horizontal"] {
+ border-top: 1px solid var(--viewBorderColor);
+ border-right: 1px solid var(--viewBorderColor);
+}
+
+.multiday-view-label-day-box[orient="vertical"] {
+ border-top: 1px solid var(--viewBorderColor);
+}
+
+.multiday-view-header-day-box[orient="vertical"] {
+ border-top: 1px solid var(--viewBorderColor);
+ border-right: 2px solid var(--viewBorderColor);
+}
+
+/* Make sure to have a border between the edge of the views and the scrollbar. */
+.multiday-view-day-box {
+ border-right: 1px solid var(--viewBorderColor);
+ border-bottom: 1px solid var(--viewBorderColor);
+}
+
+.fgdragbox {
+ -moz-box-orient: inherit;
+ display: none;
+}
+
+.fgdragbox[dragging="true"] {
+ display: -moz-box;
+ background: var(--viewDragboxBackground);
+ border: 5px var(--viewBackground);
+ opacity: 0.5;
+ min-height: 2px;
+ min-width: 2px;
+}
+
+.fgdragcontainer {
+ -moz-box-orient: inherit;
+ display: none;
+}
+
+.fgdragcontainer[dragging="true"] {
+ display: -moz-box;
+ /* This is a workaround for a stack bug and display: hidden in underlying
+ * elements -- the display: hidden bits get misrendered as being on top.
+ * Setting an opacity here forces a view to be created for this element, too.
+ */
+ opacity: 0.9999;
+}
+
+.fgdragbox-label {
+ font-weight: bold;
+ text-align: center;
+ overflow: hidden;
+ color: var(--viewDragboxColor);
+}
+
+.timeIndicator[orient="vertical"] {
+ min-width: 1px;
+ margin-inline-start: -1px;
+ margin-inline-end: -1px;
+ border-top: 2px solid red;
+ opacity: 0.7;
+}
+
+.timeIndicator[orient="horizontal"] {
+ min-height: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+ border-left: 2px solid red;
+ opacity: 0.7;
+}
+
+.timeIndicator-timeBar {
+ background-color: red;
+ position: absolute;
+ border-radius: 2px;
+}
+
+.timeIndicator-timeBar[orient="vertical"] {
+ margin-top: -1px;
+ height: 4px;
+ width: 8px;
+ right: 0px;
+}
+
+.timeIndicator-timeBar[orient="horizontal"] {
+ margin-left: -1px;
+ height: 8px;
+ width: 4px;
+ bottom: 0px;
+}
+
+.calendar-event-box-container {
+ padding: 0;
+ overflow: hidden;
+ margin: 1px;
+}
+
+.calendar-event-box-container[categories] {
+ margin-inline-end: 0px;
+}
+
+.calendar-event-details {
+ padding-inline-start: 2px;
+ overflow: hidden;
+}
+
+.calendar-event-details-core {
+ width: 0px;
+ margin: 0px;
+ overflow: hidden;
+}
+
+.calendar-event-name-textbox {
+ background: transparent !important;
+ color: inherit;
+}
+
+calendar-event-box {
+ border: 1px solid transparent;
+}
+
+calendar-month-day-box-item[selected="true"] .calendar-color-box,
+calendar-event-box[selected="true"] .calendar-color-box,
+calendar-editable-item[selected="true"] .calendar-color-box {
+ color: var(--viewColor) !important;
+ background-color: #FDF5A0 !important;
+ box-shadow: 1px 2px 5px rgba(30, 20, 0, 0.6);
+}
+
+
+/* RTL styles for the main box and children */
+.multiday-view-main-box {
+ direction: ltr;
+}
+
+.multiday-view-label-day-box:-moz-locale-dir(rtl) {
+ direction: rtl;
+}
+
+/* headers horizontal, times vertical */
+.multiday-view-label-box[orient="horizontal"] {
+ height: 10px;
+}
+
+.multiday-view-header-box[orient="horizontal"] {
+ min-height: 30px;
+}
+
+.multiday-view-label-box[orient="horizontal"] > .multiday-view-label-time-spacer,
+.multiday-view-header-box[orient="horizontal"] > .multiday-view-header-time-spacer,
+calendar-time-bar[orient="vertical"] {
+ width: 10ex; /* space for "11:00 AM" */
+}
+
+/* headers vertical, times horizonal */
+.view-label-box[orient="vertical"] {
+ width: 30px;
+}
+
+.view-header-box[orient="vertical"] {
+ width: 40px;
+}
+
+.multiday-view-label-box[orient="vertical"] > .multiday-view-label-time-spacer,
+.multiday-view-header-box[orient="vertical"] > .multiday-view-header-time-spacer {
+ height: 40px;
+}
+
+calendar-time-bar[orient="horizontal"] {
+ height: 40px;
+}
+
+/** Start time bar **/
+
+.calendar-time-bar-label {
+ font-size: 1em;
+}
+
+.calendar-time-bar-box-odd,
+.calendar-time-bar-box-even {
+ color: var(--viewTimeBoxColor);
+ background-color: var(--viewBackground);
+ text-align: right;
+ overflow: hidden;
+}
+
+.calendar-time-bar-box-odd[off-time="true"] ,
+.calendar-time-bar-box-even[off-time="true"] {
+ background-color: var(--viewOffTimeBackground);
+ border-right: 2px solid;
+ -moz-border-right-colors: var(--viewDoubleBorderColor);
+}
+
+.calendar-time-bar-box-odd[orient="horizontal"],
+.calendar-time-bar-box-even[orient="horizontal"] {
+ border-right: 1px solid var(--viewBorderColor);
+ border-top: 1px solid var(--viewBorderColor);
+ -moz-border-right-colors: none;
+ height: 40px; /* the same as the calendar-time-bar element */
+}
+
+.calendar-time-bar-box-odd[orient="vertical"],
+.calendar-time-bar-box-even[orient="vertical"] {
+ border-bottom: 1px transparent !important;
+ border-right: 2px solid;
+ -moz-border-right-colors: var(--viewDoubleBorderColor);
+ width: 10ex; /* the same as the calendar-time-bar element */
+}
+
+/** End time bar **/
+
+calendar-multiday-view {
+ background-color: var(--viewBackground);
+ padding: 0px;
+}
+
+calendar-multiday-view[hidden="true"] {
+ display: none;
+}
+
+calendar-day-label {
+ color: var(--viewColor);
+ background-color: var(--viewBackground);
+ background-image: linear-gradient(transparent, transparent 48%,
+ rgba(0, 0, 0, 0.02) 52%, rgba(0, 0, 0, 0.1));
+ border-left: 1px solid var(--viewBorderColor);
+ border-bottom: 1px solid var(--viewBorderColor);
+}
+
+calendar-day-label[selected="true"] {
+ color: var(--viewDayLabelSelectedColor);
+ background-color: var(--viewDayLabelSelectedBackground) !important;
+}
+
+calendar-day-label[orient="vertical"] {
+ background-image: none !important;
+ min-width: 100px;
+}
+
+calendar-day-label[relation="today"],
+calendar-day-label[relation="today1day"] {
+ background-color: var(--viewTodayDayLabelBackground);
+ border: 1px solid var(--viewHighlightBorderColor);
+ margin-inline-end: -1px;
+ margin-top: -1px;
+ position: relative;
+}
+
+.calendar-day-label-name {
+ text-align: center;
+}
+
+.calendar-day-label-name[relation="today"],
+.calendar-day-label-name[relation="today1day"] {
+ font-weight: bold;
+}
+
+/* Month View */
+calendar-month-view,
+calendar-multiweek-view {
+ padding: 0px 2px 2px;
+}
+
+.calendar-month-view-grid-column {
+ min-width: 1px;
+ width: 1px;
+}
+
+.calendar-month-view-grid-row {
+ min-height: 1px;
+ height: 1px;
+ border-right: 1px solid var(--viewBorderColor);
+}
+
+calendar-month-day-box {
+ border:none !important;
+ border-left: 1px solid var(--viewBorderColor) !important;
+ border-bottom: 1px solid var(--viewBorderColor) !important;
+}
+
+.calendar-month-day-box-items-box {
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.calendar-month-day-box-current-month {
+ background-color: var(--viewBackground);
+}
+
+.calendar-month-day-box-current-month .calendar-month-day-box-date-label {
+ background-color: var(--viewMonthCurrentBackground);
+}
+
+.calendar-month-day-box-day-off {
+ background-color: var(--viewWeekendBackground);
+}
+
+.calendar-month-day-box-day-off .calendar-month-day-box-date-label {
+ background-color: var(--viewMonthDayOffLabelBackground);
+}
+
+.calendar-month-day-box-other-month {
+ background-color: var(--viewMonthOtherBackground);
+}
+
+.calendar-month-day-box-other-month .calendar-month-day-box-date-label {
+ background-color: var(--viewMonthDayOtherBackground);
+}
+
+.calendar-month-day-box-other-month.calendar-month-day-box-day-off {
+ background-color: var(--viewMonthDayOtherBackground);
+}
+
+.calendar-month-day-box-other-month.calendar-month-day-box-day-off .calendar-month-day-box-date-label {
+ background-color: var(--viewMonthDayOtherLabelBackground);
+}
+
+.calendar-month-day-box-current-month[relation="today"],
+.calendar-month-day-box-day-off[relation="today"],
+.calendar-month-day-box-other-month[relation="today"] {
+ background-color: var(--viewTodayBackground);
+ border: 1px solid var(--viewTodayBorderColor) !important;
+ margin-inline-end: -1px !important;
+ margin-top: -1px !important;
+ position: relative;
+}
+
+.calendar-month-day-box-date-label[relation="today"] {
+ color: var(--viewTodayLabelColor);
+ background-color: var(--viewTodayLabelBackground);
+ font-weight: bold;
+}
+
+.calendar-month-day-box-current-month[selected="true"] {
+ background-color: var(--viewDayBoxSelectedBackground);
+}
+
+.calendar-month-day-box-day-off[selected="true"] {
+ background-color: var(--viewDayBoxSelectedBackground);
+}
+
+.calendar-month-day-box-other-month[selected="true"] {
+ background-color: var(--viewDayBoxOtherSelectedBackground);
+}
+
+.calendar-month-day-box-date-label[selected="true"] {
+ color: var(--viewMonthDayBoxSelectedColor);
+ background-color: var(--viewMonthDayBoxSelectedBackground);
+}
+
+.calendar-month-day-box-date-label[relation="today"][selected="true"] {
+ color: var(--viewTodayLabelSelectedColor);
+ background-color: var(--viewTodayLabelSelectedBackground);
+}
+
+.calendar-month-day-box-other-month.calendar-month-day-box-day-off .calendar-month-day-box-date-label[selected="true"] {
+ background-color: var(--viewMonthDayBoxSelectedBackground);
+}
+
+.calendar-month-day-box-date-label {
+ color: var(--viewMonthDayBoxLabelColor);
+ font-size: 0.9em;
+ text-align: right;
+ margin: 0px;
+ padding-top: 1px;
+ padding-inline-end: 2px;
+ padding-bottom: 1px;
+ background-image: var(--viewMonthDayBoxLabelTexture);
+}
+
+.calendar-month-day-box-week-label {
+ text-align: left;
+ padding-inline-start: 2px;
+ font-weight: normal !important;
+ color: var(--viewMonthDayBoxWeekLabel) !important;
+}
+
+calendar-month-day-box-item {
+ margin: 1px;
+ padding: 1px 1px;
+}
+
+.calendar-color-box {
+ /* This rule should be adopted if the alarm image size is changed */
+ min-height: 13px;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.01) 50%, rgba(0, 0, 0, 0.05));
+ border: 1px solid transparent;
+ border-radius: 2px;
+}
+
+calendar-month-day-box calendar-month-day-box-item[allday="true"] .calendar-color-box {
+ border-color: rgba(0, 0, 0, 0.5);
+ box-shadow: inset -1px -1px 0 rgba(255, 255, 255, 0.7), inset 1px 1px 0 rgba(255, 255, 255, 0.7);
+}
+
+.calendar-month-day-box-item-label {
+ padding: 0px;
+ margin: 0px;
+}
+
+.calendar-month-day-box-item-label[time="true"] {
+ margin-inline-end: 4px;
+}
+
+.labeldaybox-container {
+ border-right: 1px solid var(--viewBorderColor);
+ border-top: 1px solid var(--viewBorderColor);
+}
+
+.dropshadow {
+ height: 1.2em;
+ margin: 1px;
+ padding: 0px 1px;
+ background-color: var(--viewDropshadowBackground);
+ box-shadow: 1px 1px 3px rgba(68, 35, 0, 0.7) inset;
+}
+
+calendar-event-gripbar {
+ -moz-box-align: center;
+ -moz-box-pack: center;
+ min-height: 4px;
+ min-width: 4px;
+ overflow: hidden;
+}
+
+calendar-event-gripbar[parentorient="vertical"][whichside="start"] {
+ cursor: n-resize;
+}
+
+calendar-event-gripbar[parentorient="vertical"][whichside="end"] {
+ cursor: s-resize;
+}
+
+calendar-event-gripbar[parentorient="horizontal"][whichside="start"] {
+ cursor: w-resize;
+}
+
+calendar-event-gripbar[parentorient="horizontal"][whichside="end"] {
+ cursor: e-resize;
+}
+
+calendar-event-box[orient="vertical"] .calendar-event-box-grippy-top {
+ list-style-image: url("chrome://calendar-common/skin/event-grippy-top.png");
+ visibility: hidden;
+}
+
+calendar-event-box[orient="vertical"] .calendar-event-box-grippy-bottom {
+ list-style-image: url("chrome://calendar-common/skin/event-grippy-bottom.png");
+ visibility: hidden;
+}
+
+calendar-event-box[orient="horizontal"] .calendar-event-box-grippy-top {
+ list-style-image: url("chrome://calendar-common/skin/event-grippy-left.png");
+ visibility: hidden;
+}
+
+calendar-event-box[orient="horizontal"] .calendar-event-box-grippy-bottom {
+ list-style-image: url("chrome://calendar-common/skin/event-grippy-right.png");
+ visibility: hidden;
+}
+
+calendar-event-box[gripBars="start"]:not([readonly="true"]):hover .calendar-event-box-grippy-top,
+calendar-event-box[gripBars="end"]:not([readonly="true"]):hover .calendar-event-box-grippy-bottom,
+calendar-event-box[gripBars="both"]:not([readonly="true"]):hover .calendar-event-box-grippy-top,
+calendar-event-box[gripBars="both"]:not([readonly="true"]):hover .calendar-event-box-grippy-bottom {
+ visibility: visible;
+}
+
+/* tooltips */
+.tooltipBox {
+ max-width: 40em;
+}
+
+.tooltipValueColumn {
+ max-width: 35em; /* tooltipBox max-width minus space for label */
+}
+
+.tooltipHeaderLabel {
+ font-weight: bold;
+ text-align: right;
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-inline-start: 0;
+ margin-inline-end: 1em; /* 1em space before value */
+}
+
+.tooltipHeaderDescription {
+ font-weight: normal;
+ text-align: left;
+ margin: 0pt;
+}
+
+.tooltipBodySeparator {
+ height: 1ex; /* 1ex space above body text, below last header. */
+}
+
+.tooltipBody {
+ font-weight: normal;
+ white-space: pre-wrap;
+ margin: 0pt;
+}
+
+#calendar-view-context-menu[type="event"] .todo-only,
+#calendar-view-context-menu[type="todo"] .event-only,
+#calendar-view-context-menu[type="mixed"] .event-only,
+#calendar-view-context-menu[type="mixed"] .todo-only,
+#calendar-item-context-menu[type="event"] .todo-only,
+#calendar-item-context-menu[type="todo"] .event-only,
+#calendar-item-context-menu[type="mixed"] .event-only,
+#calendar-item-context-menu[type="mixed"] .todo-only {
+ display: none;
+}
+
+.attendance-menu[itemType="single"] > menupopup > *[scope="all-occurrences"] {
+ display: none;
+}
+
+.calendar-context-heading-label {
+ font-weight: bold;
+ color: menutext;
+}
+
+calendar-event-box,
+calendar-editable-item,
+calendar-month-day-box-item {
+ opacity: 0.99;
+ /* Do not change next line, since it would break item selection */
+ -moz-user-focus: normal;
+}
+
+calendar-event-box[invitation-status="NEEDS-ACTION"],
+calendar-editable-item[invitation-status="NEEDS-ACTION"],
+calendar-month-day-box-item[invitation-status="NEEDS-ACTION"] {
+ border: 2px dotted black;
+ opacity: 0.6;
+}
+
+calendar-event-box[invitation-status="TENTATIVE"],
+calendar-editable-item[invitation-status="TENTATIVE"],
+calendar-month-day-box-item[invitation-status="TENTATIVE"],
+calendar-event-box[status="TENTATIVE"],
+calendar-editable-item[status="TENTATIVE"],
+calendar-month-day-box-item[status="TENTATIVE"],
+agenda-richlist-item[status="TENTATIVE"] {
+ opacity: 0.6;
+}
+
+calendar-event-box[invitation-status="DECLINED"],
+calendar-editable-item[invitation-status="DECLINED"],
+calendar-month-day-box-item[invitation-status="DECLINED"],
+calendar-event-box[status="CANCELLED"],
+calendar-editable-item[status="CANCELLED"],
+calendar-month-day-box-item[status="CANCELLED"],
+agenda-richlist-item[status="CANCELLED"] {
+ opacity: 0.5;
+}
+
+calendar-month-day-box-item[status="CANCELLED"] .calendar-color-box,
+calendar-event-box[status="CANCELLED"] .calendar-color-box,
+calendar-editable-item[status="CANCELLED"] .calendar-color-box,
+agenda-richlist-item[status="CANCELLED"] .agenda-container-box {
+ text-decoration: line-through;
+}
+
+/* Navigation controls for the views */
+#view-deck {
+ border: solid ThreeDShadow;
+}
+
+#view-tabs .tabs-left,
+#view-tabs .tabs-right {
+ border-bottom: 1px solid ThreeDShadow;
+ -moz-border-bottom-colors: none;
+}
+
+tab[calview] {
+ -moz-appearance: none;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ margin-top: 0px;
+ -moz-user-focus: normal;
+}
+
+tab[calview][selected="true"],
+tab[calview][selected="true"]:hover {
+ margin-bottom: 0px;
+ border-bottom: none;
+}
+
+tab[calview] > .tab-middle {
+ text-align: center;
+}
+
+.view-header {
+ font-weight: normal;
+ font-size: 14px;
+ color: ButtonText;
+}
+
+.view-header[type="end"] {
+ text-align: right;
+ margin-inline-end: 6px;
+}
+
+.navigation-inner-box {
+ padding-inline-start: 6px;
+ padding-inline-end: 6px;
+ padding-bottom: 1px;
+}
+
+.navigation-bottombox {
+ min-height: 4px;
+ max-height: 4px;
+}
+
+.view-navigation-button {
+ -moz-user-focus: normal;
+ -moz-appearance: toolbarbutton;
+ min-width: 22px;
+}
+
+.today-navigation-button {
+ -moz-user-focus: normal;
+ -moz-appearance: toolbarbutton;
+ margin-inline-start: 2px;
+ margin-inline-end: 2px;
+ color: ButtonText;
+ font-size: 14px;
+}
+
+.view-navigation-button > .toolbarbutton-text {
+ display: none;
+}
+
+.today-navigation-button > .toolbarbutton-icon {
+ display: none;
+}
+
+.item-classification-box {
+ list-style-image: url("chrome://calendar-common/skin/classification.png");
+ width: 11px;
+ height: 11px;
+ display: none;
+}
+
+.item-classification-box[classification="PUBLIC"] {
+ display: none;
+}
+
+.item-classification-box[classification="PRIVATE"] {
+ -moz-image-region: rect(0 22px 11px 11px);
+ display: -moz-box;
+}
+
+.item-classification-box[classification="CONFIDENTIAL"] {
+ -moz-image-region: rect(0 11px 11px 0);
+ display: -moz-box;
+}
+
+.multiday-headerscrollbarspacer:-moz-system-metric(overlay-scrollbars),
+.multiday-labelscrollbarspacer:-moz-system-metric(overlay-scrollbars) {
+ display: none;
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-alarm-dialog.css b/calendar/base/themes/common/dialogs/calendar-alarm-dialog.css
new file mode 100644
index 000000000..244ccb852
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-alarm-dialog.css
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Bindings */
+calendar-alarm-widget {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-alarm-widget);
+}
+
+/* Alarm dialog styles */
+#alarm-richlist {
+ margin: 10px;
+}
+
+#alarm-actionbar {
+ min-width: 1px;
+ margin: 0 5px;
+}
+
+/* Alarm widget specific styles */
+calendar-alarm-widget {
+ border-bottom: 1px dotted ThreeDShadow;
+ padding: 6px 7px;
+}
+
+calendar-alarm-widget[selected="true"] {
+ background-color: Highlight;
+ color: HighlightText;
+ padding: 0 5px;
+}
+
+calendar-alarm-widget[selected="true"] .alarm-title-label {
+ font-weight: bold;
+}
+
+calendar-alarm-widget[selected="true"] .alarm-action-buttons {
+ display: -moz-box;
+}
+
+calendar-alarm-widget[selected="true"] > hbox {
+ margin: 5px;
+}
+
+calendar-alarm-widget[selected="true"] .alarm-relative-date-label,
+.additional-information-box,
+.alarm-action-buttons {
+ display: none;
+}
+
+calendar-alarm-widget[selected="true"] .additional-information-box,
+calendar-alarm-widget[selected="true"] .action-buttons-box {
+ display: -moz-box;
+}
+
+.alarm-details-label {
+ color: HighlightText;
+ text-decoration: underline;
+}
+
+calendar-alarm-widget[selected="true"] .alarm-calendar-image {
+ list-style-image: url(chrome://calendar/skin/cal-icon32.png);
+}
+
+.resizer-box {
+ min-height: 15px;
+}
+
+.snooze-popup-ok-button {
+ -moz-image-region: rect(0px, 14px, 14px, 0px);
+}
+
+.snooze-popup-ok-button:hover {
+ -moz-image-region: rect(14px, 14px, 28px, 0px);
+}
+
+.snooze-popup-ok-button:active {
+ -moz-image-region: rect(28px, 14px, 42px, 0px);
+}
+
+.snooze-popup-ok-button[disabled="true"] {
+ -moz-image-region: rect(42px, 14px, 56px, 0px);
+}
+
+.snooze-popup-cancel-button {
+ -moz-image-region: rect(0px, 28px, 14px, 14px);
+}
+
+.snooze-popup-cancel-button:hover {
+ -moz-image-region: rect(14px, 28px, 28px, 14px);
+}
+
+.snooze-popup-cancel-button:active {
+ -moz-image-region: rect(28px, 28px, 42px, 14px);
+}
+
+.snooze-popup-button {
+ list-style-image: url(chrome://calendar-common/skin/ok-cancel.png);
+ min-width: 0;
+ -moz-appearance: toolbarbutton;
+}
+
+.snooze-popup-button > .button-box > .button-icon {
+ margin: 0;
+}
+
+.snooze-popup-button > .button-box {
+ border: 0;
+ padding: 0;
+ -moz-box-pack: center;
+ -moz-box-align: center;
+}
+
+.snooze-popup-button:focus > .button-box {
+ border: 1px dotted ThreeDDarkShadow;
+ padding: 0;
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-event-dialog.css b/calendar/base/themes/common/dialogs/calendar-event-dialog.css
new file mode 100644
index 000000000..b0e5cc213
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-event-dialog.css
@@ -0,0 +1,565 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+dialog {
+ --eventBorderColor: #8d8e90;
+ --eventWidgetBorderColor: #cacaff;
+ --eventGridStartBorderColor: #bdbec0;
+}
+
+dialog[systemcolors] {
+ --eventBorderColor: ThreeDShadow;
+ --eventWidgetBorderColor: ThreeDShadow;
+ --eventGridStartBorderColor: ThreeDShadow;
+}
+
+#calendar-event-dialog,
+#calendar-task-dialog {
+ min-width: 444px;
+ min-height: 476px;
+}
+
+#calendar-event-dialog,
+#calendar-task-dialog,
+#calendar-event-dialog-inner,
+#calendar-task-dialog-inner {
+ padding: 0px;
+}
+
+#calendar-event-dialog .todo-only,
+#calendar-task-dialog .event-only,
+#calendar-event-dialog-inner .todo-only,
+#calendar-task-dialog-inner .event-only {
+ display: none;
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog counter box section
+ *-------------------------------------------------------------------*/
+
+#counter-proposal-box {
+ background-color: rgb(186, 238, 255);
+ border-bottom: 1px solid #444444;
+}
+
+#counter-proposal-property-values > description {
+ margin-bottom: 2px;
+}
+
+#counter-proposal-summary {
+ font-weight: bold;
+}
+
+.counter-buttons {
+ max-height: 25px;
+}
+
+#yearly-period-of-label,
+label.label {
+ text-align: right;
+}
+
+#item-calendar,
+#item-categories,
+#item-repeat,
+#item-alarm,
+.datepicker-text-class {
+ min-width: 12em;
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ .cal-event-toolbarbutton .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.icon-holder[type="calendarEvent"],
+.tabmail-tab[type="calendarEvent"] {
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-tab.png);
+ -moz-image-region: auto;
+}
+
+.icon-holder[type="calendarTask"],
+.tabmail-tab[type="calendarTask"] {
+ list-style-image: url(chrome://calendar-common/skin/calendar-task-tab.png);
+ -moz-image-region: auto;
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog tabbox section
+ *-------------------------------------------------------------------*/
+
+#event-grid-tabbox {
+ margin: 5px 0px;
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog keep duration button
+ *-------------------------------------------------------------------*/
+
+#keepduration-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog.png);
+ -moz-image-region: rect(0px 147px 24px 140px);
+ padding-top: 3px;
+ padding-bottom: 3px;
+ margin-inline-start: -3px;
+ margin-bottom: -15px;
+ position: relative;
+ -moz-user-focus: normal;
+}
+
+#keepduration-button[keep="true"] {
+ -moz-image-region: rect(0px 139px 24px 132px);
+}
+
+#keepduration-button[disabled="true"] {
+ -moz-image-region: rect(0px 163px 24px 156px);
+}
+
+#keepduration-button[keep="true"][disabled="true"] {
+ -moz-image-region: rect(0px 155px 24px 148px);
+}
+
+#keepduration-button > label {
+ display: none;
+}
+
+.keepduration-link-image {
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog.png);
+ margin-inline-start: -1px;
+}
+
+#link-image-top {
+ -moz-image-region: rect(7px 174px 14px 164px);
+ margin-top: 0.6em;
+}
+
+#link-image-top[keep="true"]{
+ -moz-image-region: rect(0px 174px 7px 164px);
+}
+
+#link-image-bottom {
+ -moz-image-region: rect(0px 184px 7px 174px);
+ margin-bottom: 0.6em;
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog statusbar images
+ *-------------------------------------------------------------------*/
+
+.cal-statusbar-1 {
+ -moz-box-orient: vertical;
+ min-width: 0px;
+ list-style-image: url("chrome://calendar-common/skin/calendar-event-dialog.png");
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog statusbarpanels
+ *-------------------------------------------------------------------*/
+
+#status-privacy,
+#status-priority,
+#status-status,
+#status-freebusy {
+ overflow: hidden;
+}
+
+/*--------------------------------------------------------------------
+ * priority "low" image
+ *-------------------------------------------------------------------*/
+
+#image-priority-low {
+ -moz-image-region: rect(0px 100px 16px 84px);
+}
+
+/*--------------------------------------------------------------------
+ * priority "normal" image
+ *-------------------------------------------------------------------*/
+
+#image-priority-normal {
+ -moz-image-region: rect(0px 116px 16px 100px);
+}
+
+/*--------------------------------------------------------------------
+ * priority "high" image
+ *-------------------------------------------------------------------*/
+
+#image-priority-high {
+ -moz-image-region: rect(0px 132px 16px 116px);
+}
+
+/*--------------------------------------------------------------------
+ * Recurrence dialog preview border
+ *-------------------------------------------------------------------*/
+#preview-border {
+ border: none;
+ padding: 0px;
+}
+
+/*--------------------------------------------------------------------
+ * freebusy
+ *-------------------------------------------------------------------*/
+#freebusy-container {
+ overflow: hidden;
+ min-width: 100px;
+}
+
+#freebusy-grid {
+ min-width: 1px;
+}
+
+#calendar-summary-dialog {
+ min-width: 35em;
+}
+
+listbox[disabled="true"] {
+ color: -moz-FieldText;
+}
+
+daypicker-weekday {
+ margin-top: 2px;
+}
+
+daypicker-monthday {
+ margin-top: 2px;
+}
+
+.headline {
+ font-weight: bold;
+}
+
+.headline[align=end],
+.headline[align=right]{
+ text-align: right;
+}
+
+.default-spacer {
+ width: 1em;
+ height: 1em;
+}
+
+.default-indent {
+ margin-inline-start: 1.5em;
+}
+
+/*--------------------------------------------------------------------
+ * Attendees Dialog
+ *-------------------------------------------------------------------*/
+
+.listbox-noborder {
+ -moz-appearance: none;
+ margin: 0px 0px;
+ border: 1px solid var(--eventBorderColor);
+}
+
+#timebar > .listbox-noborder {
+ border-bottom-style: none;
+}
+
+#freebusy-grid > .listbox-noborder {
+ border-top-color: transparent;
+}
+
+/* remove on Windows the double border with the splitter */
+@media (-moz-windows-theme) {
+ #attendees-list > .listbox-noborder {
+ border-inline-end-style: none;
+ }
+ #timebar > .listbox-noborder,
+ #freebusy-grid > .listbox-noborder {
+ border-inline-start-style: none;
+ }
+}
+
+.selection-bar {
+ background-color: rgba(0, 128, 128, .2);
+ border: 1px solid #008080;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+.freebusy-container {
+ overflow: hidden;
+ clip: rect(0px 0px 0px 0px);
+}
+
+.freebusy-listitem {
+ border-width: 0px;
+}
+
+freebusy-day {
+ margin-inline-end: 2px;
+ border-inline-start: 1px solid var(--eventBorderColor);
+ border-inline-end: 1px solid var(--eventBorderColor);
+ border-bottom: 1px solid var(--eventBorderColor);
+}
+
+freebusy-day:first-child {
+ border-inline-start: 1px solid transparent;
+}
+
+freebusy-day:last-child {
+ border-inline-end: 1px solid transparent;
+}
+
+/* negative margins to clear freebusy-day's border and margin */
+freebusy-day > box {
+ margin-inline-start: -1px;
+ margin-inline-end: -3px;
+}
+
+.freebusy-content {
+ overflow: hidden;
+ clip: rect(0px 0px 0px 0px);
+}
+
+/* margin-inline-end 2px is needed to take border-right from the grid elements into account */
+.freebusy-timebar-title {
+ margin-inline-end: 2px;
+ padding-inline-start: 2px;
+}
+
+.freebusy-timebar-hour {
+ padding-inline-start: 2px;
+ padding-inline-end: 3px;
+ margin-top: 2px;
+ margin-bottom: 3px;
+}
+
+.freebusy-timebar-hour[scheduled="true"] {
+ /* the 'sechuled'-attribute is used in the timebar to indicate
+ which hours are affected of the currently schedued event.
+ since we added the selection-bar this is no longer necessary
+ but we keep the possibity to decorate those hours if it should
+ become beneficial.
+ text-decoration: underline;
+ */
+}
+
+.freebusy-grid {
+ border-inline-start: 1px solid var(--eventGridStartBorderColor);
+ background-color: #E09EBD;
+ color: #E09EBD;
+ min-height: 16px;
+ padding-inline-start: 1px;
+}
+
+.freebusy-grid[state="busy"] {
+ background-color: #153E7E;
+ color: #153E7E;
+}
+
+.freebusy-grid[state="busy_tentative"] {
+ background-color: #1589FF;
+ color: #1589FF;
+}
+
+.freebusy-grid[state="busy_unavailable"] {
+ background-color: #4E387E;
+ color: #4E387E;
+}
+
+.freebusy-grid[state="free"] {
+ background-color: #EBEBE4;
+ color: #EBEBE4;
+}
+
+.freebusy-grid:first-child {
+ border-inline-start: 1px solid transparent;
+}
+
+.freebusy-grid.last-in-day {
+ border-inline-end: 1px solid var(--eventBorderColor);
+ margin-inline-end: 2px;
+}
+
+#dialog-box {
+ padding-top: 8px;
+ padding-bottom: 10px;
+ padding-inline-start: 8px;
+ padding-inline-end: 10px;
+}
+
+#addressingWidget {
+ -moz-user-focus: none;
+}
+
+#typecol-addressingWidget {
+ min-width: 9em;
+ border-right: 1px solid var(--eventWidgetBorderColor);
+}
+
+/* This applies to rows of the attendee-list and the freebusy-grid */
+.addressingWidgetItem,
+.dummy-row {
+ border: none !important;
+ background-color: inherit !important;
+ color: inherit !important;
+
+ /* we set the minimal height to the height of the
+ largest icon [the usertype-icon in this case] to
+ ensure that the rows of the freebusy-grid and
+ the attendee-list always have the same height,
+ regardless of the font size. */
+ min-height: 16px;
+}
+
+.addressingWidgetCell {
+ border-bottom: 1px solid var(--eventWidgetBorderColor);
+ padding: 0px;
+}
+
+.addressingWidgetCell:first-child {
+ border-top: none;
+}
+
+.dummy-row-cell:first-child {
+ border-top: none;
+}
+
+.zoom-in-icon {
+ margin: 3px 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 97px 14px 84px);
+}
+.zoom-in-icon[disabled="true"] {
+ -moz-image-region: rect(14px 97px 28px 84px);
+}
+
+.zoom-out-icon {
+ margin: 3px 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 110px 14px 97px);
+}
+.zoom-out-icon[disabled="true"] {
+ -moz-image-region: rect(14px 110px 28px 97px);
+}
+
+.left-icon {
+ margin: 3px 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 124px 14px 110px);
+}
+.left-icon[disabled="true"] {
+ -moz-image-region: rect(14px 124px 28px 110px);
+}
+
+.right-icon {
+ margin: 3px 3px;
+ list-style-image: url(chrome://calendar-common/skin/calendar-event-dialog-attendees.png);
+ -moz-image-region: rect(0px 138px 14px 124px);
+}
+.right-icon[disabled="true"] {
+ -moz-image-region: rect(14px 138px 28px 124px);
+}
+
+.left-icon .button-icon {
+ margin-inline-end: 3px;
+}
+
+.right-icon .button-icon {
+ margin-inline-start: 3px;
+}
+
+.legend {
+ width: 3em;
+ height: 1em;
+ border-top: 1px solid #A1A1A1;
+ border-right: 1px solid #C3C3C3;
+ border-bottom: 1px solid #DDDDDD;
+ border-left: 1px solid #C3C3C3;
+}
+
+.legend[status="FREE"] {
+ background-color: #EBEBE4;
+ color: #EBEBE4;
+}
+
+.legend[status="BUSY"] {
+ background-color: #153E7E;
+ color: #153E7E;
+}
+
+.legend[status="BUSY_TENTATIVE"] {
+ background-color: #1589FF;
+ color: #1589FF;
+}
+
+.legend[status="BUSY_UNAVAILABLE"] {
+ background-color: #4E387E;
+ color: #4E387E;
+}
+
+.legend[status="UNKNOWN"] {
+ background-color: #E09EBD;
+ color: #E09EBD;
+}
+
+#content-frame {
+ border-left: 1px solid ThreeDDarkShadow;
+ border-right: 1px solid ThreeDLightShadow;
+ min-width: 10px;
+ min-height: 10px;
+ height: 400px;
+}
+
+.attendees-list-listbox > listboxbody {
+ overflow-y: hidden !important;
+}
+
+.selection-bar-left {
+ width: 3px;
+ cursor: w-resize;
+}
+
+.selection-bar-right {
+ width: 3px;
+ cursor: e-resize;
+}
+
+.selection-bar-spacer {
+ cursor: grab;
+}
+
+.checkbox-no-label > .checkbox-label-box {
+ display: none;
+}
+
+/*--------------------------------------------------------------------
+ * Event summary dialog
+ *-------------------------------------------------------------------*/
+
+#calendar-summary-dialog,
+#calendar-event-summary-dialog,
+#calendar-task-summary-dialog {
+ min-width: 35em;
+}
+
+#calendar-summary-dialog #item-attachment-cell,
+#calendar-event-summary-dialog #item-attachment-cell,
+#calendar-task-summary-dialog #item-attachment-cell {
+ margin-left: 6px;
+}
+
+#calendar-summary-dialog .item-attachment-cell-label,
+#calendar-event-summary-dialog .item-attachment-cell-label,
+#calendar-task-summary-dialog .item-attachment-cell-label {
+ margin-left: 3px;
+}
+
+#calendar-summary-dialog #item-description,
+#calendar-event-summary-dialog #item-description,
+#calendar-task-summary-dialog #item-description {
+ min-height: 54px;
+}
+
+#calendar-summary-dialog #item-start-row .headline,
+#calendar-event-summary-dialog #item-start-row .headline,
+#calendar-task-summary-dialog #item-start-row .headline,
+#calendar-summary-dialog #item-end-row .headline,
+#calendar-event-summary-dialog #item-end-row .headline,
+#calendar-task-summary-dialog #item-end-row .headline {
+ font-weight: normal;
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-invitations-dialog.css b/calendar/base/themes/common/dialogs/calendar-invitations-dialog.css
new file mode 100644
index 000000000..66dcd0f9e
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-invitations-dialog.css
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-invitations-dialog {
+ min-width: 500px;
+ min-height: 273px;
+}
+
+.calendar-invitations-updating-icon {
+ list-style-image: url("chrome://global/skin/icons/loading.png");
+ opacity: 0.5;
+}
+
+calendar-invitations-richlistbox {
+ background-color: -moz-Field;
+ color: -moz-FieldText;
+ border: 1px solid #7F9DB9;
+}
+
+calendar-invitations-richlistitem {
+ padding-top: 6px;
+ padding-bottom: 6px;
+ padding-inline-start: 7px;
+ padding-inline-end: 7px;
+ min-height: 25px;
+ border-bottom: 1px dotted #C0C0C0;
+}
+
+calendar-invitations-richlistitem[selected="true"] {
+ background-color: Highlight;
+ color: HighlightText;
+ border-bottom: 1px dotted #7F9DB9;
+}
+
+.calendar-invitations-richlistitem-title {
+ font-weight: bold;
+}
+
+.calendar-invitations-richlistitem-icon[status="NEEDS-ACTION"] {
+ list-style-image: url("chrome://calendar-common/skin/calendar-invitations-dialog-list-images.png");
+ -moz-image-region: rect(0px 32px 32px 0px);
+}
+
+.calendar-invitations-richlistitem-icon[status="ACCEPTED"] {
+ list-style-image: url("chrome://calendar-common/skin/calendar-invitations-dialog-list-images.png");
+ -moz-image-region: rect(0px 64px 32px 32px);
+}
+
+.calendar-invitations-richlistitem-icon[status="DECLINED"] {
+ list-style-image: url("chrome://calendar-common/skin/calendar-invitations-dialog-list-images.png");
+ -moz-image-region: rect(0px 96px 32px 64px);
+}
+
+.calendar-invitations-richlistitem-button {
+ margin-bottom: 10px;
+ visibility: hidden;
+}
+
+calendar-invitations-richlistitem[selected="true"] .calendar-invitations-richlistitem-button {
+ visibility: visible;
+}
+
+.calendar-invitations-richlistitem-button .button-icon {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-inline-start: 0px;
+ margin-inline-end: 5px;
+}
+
+.calendar-invitations-richlistitem-accept-button {
+ list-style-image: url("chrome://calendar-common/skin/calendar-invitations-dialog-button-images.png");
+ -moz-image-region: rect(0px 16px 16px 0px);
+}
+
+.calendar-invitations-richlistitem-decline-button {
+ list-style-image: url("chrome://calendar-common/skin/calendar-invitations-dialog-button-images.png");
+ -moz-image-region: rect(0px 32px 16px 16px);
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-properties-dialog.css b/calendar/base/themes/common/dialogs/calendar-properties-dialog.css
new file mode 100644
index 000000000..68f7fd024
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-properties-dialog.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-properties-grid {
+ margin-inline-start: 20px;
+}
+
+#calendar-properties-rows > row {
+ min-height: 26px;
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-subscriptions-dialog.css b/calendar/base/themes/common/dialogs/calendar-subscriptions-dialog.css
new file mode 100644
index 000000000..6351eb984
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-subscriptions-dialog.css
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-subscriptions-dialog {
+ min-width: 480px;
+ min-height: 320px;
+}
+
+calendar-subscriptions-richlistbox {
+ background-color: white;
+ border: 1px solid black;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ margin-inline-start: 4px;
+ margin-inline-end: 4px;
+ padding-top: 2px;
+ padding-bottom: 3px;
+ padding-inline-start: 4px;
+ padding-inline-end: 2px;
+}
+
+calendar-subscriptions-richlistitem[selected="true"] {
+ background-color: Highlight;
+ color: HighlightText;
+}
+
+.calendar-subscriptions-richlistitem-checkbox {
+ margin-inline-end: 0px;
+}
+
+.calendar-subscriptions-richlistitem-checkbox > .checkbox-label-box {
+ display: none;
+}
+
+.calendar-subscriptions-select-label {
+ margin-top: 6px;
+}
+
+.calendar-subscriptions-status-box {
+ margin-top: 2px;
+ margin-bottom: 2px;
+ margin-inline-start: 4px;
+}
+
+.calendar-subscriptions-status-icon {
+ list-style-image: url("chrome://messenger/skin/icons/notloading.png");
+}
+
+.calendar-subscriptions-status-icon[busy="true"] {
+ list-style-image: url("chrome://global/skin/icons/loading.png");
+}
diff --git a/calendar/base/themes/common/dialogs/calendar-timezone-highlighter.css b/calendar/base/themes/common/dialogs/calendar-timezone-highlighter.css
new file mode 100644
index 000000000..725c7af8d
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/calendar-timezone-highlighter.css
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.timezone-highlight {
+ list-style-image: url(chrome://calendar-common/skin/timezones.png);
+}
+.timezone-highlight[tzid="none"] {
+ display: none;
+}
+.timezone-highlight[tzid="+0000"] {
+ -moz-image-region: rect(0 7360px 287px 6900px);
+}
+.timezone-highlight[tzid="+0100"] {
+ -moz-image-region: rect(0 7820px 287px 7360px);
+}
+.timezone-highlight[tzid="+0200"] {
+ -moz-image-region: rect(0 8280px 287px 7820px);
+}
+.timezone-highlight[tzid="+0300"] {
+ -moz-image-region: rect(0 8740px 287px 8280px);
+}
+.timezone-highlight[tzid="+0330"] {
+ -moz-image-region: rect(0 9200px 287px 8740px);
+}
+.timezone-highlight[tzid="+0400"] {
+ -moz-image-region: rect(0 9660px 287px 9200px);
+}
+.timezone-highlight[tzid="+0430"] {
+ -moz-image-region: rect(0 10120px 287px 9660px);
+}
+.timezone-highlight[tzid="+0500"] {
+ -moz-image-region: rect(0 10580px 287px 10120px);
+}
+.timezone-highlight[tzid="+0530"] {
+ -moz-image-region: rect(0 11040px 287px 10580px);
+}
+.timezone-highlight[tzid="+0545"] {
+ -moz-image-region: rect(0 11500px 287px 11040px);
+}
+.timezone-highlight[tzid="+0600"] {
+ -moz-image-region: rect(0 11960px 287px 11500px);
+}
+.timezone-highlight[tzid="+0630"] {
+ -moz-image-region: rect(0 12420px 287px 11960px);
+}
+.timezone-highlight[tzid="+0700"] {
+ -moz-image-region: rect(0 12880px 287px 12420px);
+}
+.timezone-highlight[tzid="+0800"] {
+ -moz-image-region: rect(0 13340px 287px 12880px);
+}
+.timezone-highlight[tzid="+0845"] {
+ display: none;
+}
+.timezone-highlight[tzid="+0900"] {
+ -moz-image-region: rect(0 13800px 287px 13340px);
+}
+.timezone-highlight[tzid="+0930"] {
+ -moz-image-region: rect(0 14260px 287px 13800px);
+}
+.timezone-highlight[tzid="+1000"] {
+ -moz-image-region: rect(0 14720px 287px 14260px);
+}
+.timezone-highlight[tzid="+1030"] {
+ -moz-image-region: rect(0 15180px 287px 14720px);
+}
+.timezone-highlight[tzid="+1100"] {
+ -moz-image-region: rect(0 15640px 287px 15180px);
+}
+.timezone-highlight[tzid="+1130"] {
+ -moz-image-region: rect(0 15640px 287px 15180px);
+}
+.timezone-highlight[tzid="+1200"] {
+ -moz-image-region: rect(0 16560px 287px 16100px);
+}
+.timezone-highlight[tzid="+1245"] {
+ -moz-image-region: rect(0 17020px 287px 16560px);
+}
+.timezone-highlight[tzid="+1300"] {
+ -moz-image-region: rect(0 17480px 287px 17020px);
+}
+.timezone-highlight[tzid="+1400"] {
+ -moz-image-region: rect(0 17940px 287px 17480px);
+}
+.timezone-highlight[tzid="-0100"] {
+ -moz-image-region: rect(0 6900px 287px 6440px);
+}
+.timezone-highlight[tzid="-0200"] {
+ -moz-image-region: rect(0 6440px 287px 5980px);
+}
+.timezone-highlight[tzid="-0300"] {
+ -moz-image-region: rect(0 5980px 287px 5520px);
+}
+.timezone-highlight[tzid="-0330"] {
+ -moz-image-region: rect(0 5520px 287px 5060px);
+}
+.timezone-highlight[tzid="-0400"] {
+ -moz-image-region: rect(0 5060px 287px 4600px);
+}
+.timezone-highlight[tzid="-0430"] {
+ display: none;
+}
+.timezone-highlight[tzid="-0500"] {
+ -moz-image-region: rect(0 4600px 287px 4140px);
+}
+.timezone-highlight[tzid="-0600"] {
+ -moz-image-region: rect(0 4140px 287px 3680px);
+}
+.timezone-highlight[tzid="-0700"] {
+ -moz-image-region: rect(0 3680px 287px 3220px);
+}
+.timezone-highlight[tzid="-0800"] {
+ -moz-image-region: rect(0 18400px 287px 17940px);
+}
+.timezone-highlight[tzid="-0830"] {
+ -moz-image-region: rect(0 3220px 287px 2760px);
+}
+.timezone-highlight[tzid="-0900"] {
+ -moz-image-region: rect(0 2760px 287px 2300px);
+}
+.timezone-highlight[tzid="-0930"] {
+ -moz-image-region: rect(0 2300px 287px 1840px);
+}
+.timezone-highlight[tzid="-1000"] {
+ -moz-image-region: rect(0 1840px 287px 1380px);
+}
+.timezone-highlight[tzid="-1100"] {
+ -moz-image-region: rect(0 1380px 287px 920px);
+}
+.timezone-highlight[tzid="-1245"] {
+ -moz-image-region: rect(0 920px 287px 460px);
+}
+.timezone-highlight[tzid="-1200"] {
+ -moz-image-region: rect(0 460px 287px 0px);
+}
diff --git a/calendar/base/themes/common/dialogs/images/calendar-event-dialog-attendees.png b/calendar/base/themes/common/dialogs/images/calendar-event-dialog-attendees.png
new file mode 100644
index 000000000..ea52cbc19
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-event-dialog-attendees.png
Binary files differ
diff --git a/calendar/base/themes/common/dialogs/images/calendar-event-dialog.png b/calendar/base/themes/common/dialogs/images/calendar-event-dialog.png
new file mode 100644
index 000000000..8c5294a5b
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-event-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/dialogs/images/calendar-event-tab.png b/calendar/base/themes/common/dialogs/images/calendar-event-tab.png
new file mode 100644
index 000000000..664cd8262
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-event-tab.png
Binary files differ
diff --git a/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-button-images.png b/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-button-images.png
new file mode 100644
index 000000000..b641ed276
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-button-images.png
Binary files differ
diff --git a/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-list-images.png b/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-list-images.png
new file mode 100644
index 000000000..db33ad817
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-invitations-dialog-list-images.png
Binary files differ
diff --git a/calendar/base/themes/common/dialogs/images/calendar-task-tab.png b/calendar/base/themes/common/dialogs/images/calendar-task-tab.png
new file mode 100644
index 000000000..58e9daf04
--- /dev/null
+++ b/calendar/base/themes/common/dialogs/images/calendar-task-tab.png
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-alarm-dialog.ico b/calendar/base/themes/common/icons/calendar-alarm-dialog.ico
new file mode 100644
index 000000000..c55dcc7f2
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-alarm-dialog.ico
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-alarm-dialog.png b/calendar/base/themes/common/icons/calendar-alarm-dialog.png
new file mode 100644
index 000000000..daec5e331
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-alarm-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-event-dialog.ico b/calendar/base/themes/common/icons/calendar-event-dialog.ico
new file mode 100644
index 000000000..eb89f54e3
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-event-dialog.ico
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-event-dialog.png b/calendar/base/themes/common/icons/calendar-event-dialog.png
new file mode 100644
index 000000000..b4a583116
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-event-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-event-summary-dialog.ico b/calendar/base/themes/common/icons/calendar-event-summary-dialog.ico
new file mode 100644
index 000000000..6fdee6522
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-event-summary-dialog.ico
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-event-summary-dialog.png b/calendar/base/themes/common/icons/calendar-event-summary-dialog.png
new file mode 100644
index 000000000..7619817b1
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-event-summary-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-task-dialog.ico b/calendar/base/themes/common/icons/calendar-task-dialog.ico
new file mode 100644
index 000000000..809d02a38
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-task-dialog.ico
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-task-dialog.png b/calendar/base/themes/common/icons/calendar-task-dialog.png
new file mode 100644
index 000000000..6ed67385c
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-task-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-task-summary-dialog.ico b/calendar/base/themes/common/icons/calendar-task-summary-dialog.ico
new file mode 100644
index 000000000..cf75dc928
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-task-summary-dialog.ico
Binary files differ
diff --git a/calendar/base/themes/common/icons/calendar-task-summary-dialog.png b/calendar/base/themes/common/icons/calendar-task-summary-dialog.png
new file mode 100644
index 000000000..8593ad27f
--- /dev/null
+++ b/calendar/base/themes/common/icons/calendar-task-summary-dialog.png
Binary files differ
diff --git a/calendar/base/themes/common/images/alarm-flashing.png b/calendar/base/themes/common/images/alarm-flashing.png
new file mode 100644
index 000000000..b094b5fa7
--- /dev/null
+++ b/calendar/base/themes/common/images/alarm-flashing.png
Binary files differ
diff --git a/calendar/base/themes/common/images/alarm-icons.png b/calendar/base/themes/common/images/alarm-icons.png
new file mode 100644
index 000000000..554f823a0
--- /dev/null
+++ b/calendar/base/themes/common/images/alarm-icons.png
Binary files differ
diff --git a/calendar/base/themes/common/images/attendee-icons.png b/calendar/base/themes/common/images/attendee-icons.png
new file mode 100644
index 000000000..c425552e7
--- /dev/null
+++ b/calendar/base/themes/common/images/attendee-icons.png
Binary files differ
diff --git a/calendar/base/themes/common/images/calendar-overlay.png b/calendar/base/themes/common/images/calendar-overlay.png
new file mode 100644
index 000000000..af341e77d
--- /dev/null
+++ b/calendar/base/themes/common/images/calendar-overlay.png
Binary files differ
diff --git a/calendar/base/themes/common/images/calendar-status.png b/calendar/base/themes/common/images/calendar-status.png
new file mode 100644
index 000000000..baae3074d
--- /dev/null
+++ b/calendar/base/themes/common/images/calendar-status.png
Binary files differ
diff --git a/calendar/base/themes/common/images/checkbox-images.png b/calendar/base/themes/common/images/checkbox-images.png
new file mode 100644
index 000000000..dcbbae97b
--- /dev/null
+++ b/calendar/base/themes/common/images/checkbox-images.png
Binary files differ
diff --git a/calendar/base/themes/common/images/classification.png b/calendar/base/themes/common/images/classification.png
new file mode 100644
index 000000000..3b1eb5c0c
--- /dev/null
+++ b/calendar/base/themes/common/images/classification.png
Binary files differ
diff --git a/calendar/base/themes/common/images/day-box-item-image.png b/calendar/base/themes/common/images/day-box-item-image.png
new file mode 100644
index 000000000..b674ce33e
--- /dev/null
+++ b/calendar/base/themes/common/images/day-box-item-image.png
Binary files differ
diff --git a/calendar/base/themes/common/images/event-grippy-bottom.png b/calendar/base/themes/common/images/event-grippy-bottom.png
new file mode 100644
index 000000000..a6864a9c7
--- /dev/null
+++ b/calendar/base/themes/common/images/event-grippy-bottom.png
Binary files differ
diff --git a/calendar/base/themes/common/images/event-grippy-left.png b/calendar/base/themes/common/images/event-grippy-left.png
new file mode 100644
index 000000000..69d0f8072
--- /dev/null
+++ b/calendar/base/themes/common/images/event-grippy-left.png
Binary files differ
diff --git a/calendar/base/themes/common/images/event-grippy-right.png b/calendar/base/themes/common/images/event-grippy-right.png
new file mode 100644
index 000000000..62afc8c0f
--- /dev/null
+++ b/calendar/base/themes/common/images/event-grippy-right.png
Binary files differ
diff --git a/calendar/base/themes/common/images/event-grippy-top.png b/calendar/base/themes/common/images/event-grippy-top.png
new file mode 100644
index 000000000..87dadbc89
--- /dev/null
+++ b/calendar/base/themes/common/images/event-grippy-top.png
Binary files differ
diff --git a/calendar/base/themes/common/images/ok-cancel.png b/calendar/base/themes/common/images/ok-cancel.png
new file mode 100644
index 000000000..18380954d
--- /dev/null
+++ b/calendar/base/themes/common/images/ok-cancel.png
Binary files differ
diff --git a/calendar/base/themes/common/images/task-images.png b/calendar/base/themes/common/images/task-images.png
new file mode 100644
index 000000000..ddf4188c1
--- /dev/null
+++ b/calendar/base/themes/common/images/task-images.png
Binary files differ
diff --git a/calendar/base/themes/common/images/timezone_map.png b/calendar/base/themes/common/images/timezone_map.png
new file mode 100644
index 000000000..1d3f84445
--- /dev/null
+++ b/calendar/base/themes/common/images/timezone_map.png
Binary files differ
diff --git a/calendar/base/themes/common/images/timezones.png b/calendar/base/themes/common/images/timezones.png
new file mode 100644
index 000000000..9170d9d79
--- /dev/null
+++ b/calendar/base/themes/common/images/timezones.png
Binary files differ
diff --git a/calendar/base/themes/common/today-pane-cycler.svg b/calendar/base/themes/common/today-pane-cycler.svg
new file mode 100644
index 000000000..b4cbf1572
--- /dev/null
+++ b/calendar/base/themes/common/today-pane-cycler.svg
@@ -0,0 +1,15 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+ <style>
+ path {
+ display: none;
+ }
+ path:target {
+ display: block;
+ }
+ </style>
+ <path id="normal" d="m 3,2 9,6 -9,6 z" style="fill: -moz-dialogtext;"/>
+ <path id="inverted" d="m 3,2 9,6 -9,6 z" style="fill: white;"/>
+</svg>
diff --git a/calendar/base/themes/common/today-pane.css b/calendar/base/themes/common/today-pane.css
new file mode 100644
index 000000000..b30cc527d
--- /dev/null
+++ b/calendar/base/themes/common/today-pane.css
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.today-subpane {
+ border-style: solid;
+ border-width: 1px 0;
+ margin-bottom: 3px;
+ padding: 0px;
+}
+
+#buttonspacer {
+ width: 5px;
+}
+
+#today-pane-panel {
+ background-color: -moz-Dialog;
+}
+
+#today-pane-panel:-moz-lwtheme {
+ background-color: transparent;
+}
+
+#today-pane-panel > * {
+ color: -moz-DialogText;
+}
+
+#today-pane-panel:-moz-lwtheme > sidebarheader {
+ color: inherit;
+}
+
+#agenda-panel:-moz-lwtheme > vbox,
+#today-pane-splitter:-moz-lwtheme,
+#todo-tab-panel:-moz-lwtheme {
+ background-color: -moz-Dialog;
+}
+
+#today-pane-panel:-moz-lwtheme > vbox {
+ text-shadow: none;
+ background-color: -moz-Dialog;
+}
+
+#today-minimonth-box {
+ background-color: -moz-field;
+}
+
+#weekdayNameContainer {
+ font-family: Trebuchet MS, Lucida Grande, Arial, Helvetica;
+ padding-top: 4px;
+ font-weight: bold;
+ font-size: 18px;
+}
+
+.monthlabel {
+ margin-inline-end: 0;
+}
+
+.dateValue {
+ font-family: Arial, Helvetica, Trebuchet MS, Lucida Grande, sans-serif;
+ margin-top: initial;
+ margin-bottom: initial;
+ font-size: 36px;
+ font-weight: bold;
+ width: 1em;
+ text-align: center;
+}
+
+#dragCenter-image {
+ list-style-image: url("chrome://calendar-common/skin/widgets/drag-center.svg");
+}
+
+.miniday-nav-buttons {
+ margin-top: 2px;
+ min-width: 19px;
+ -moz-user-focus: normal;
+}
+
+#today-button {
+ list-style-image: url("chrome://calendar-common/skin/widgets/nav-today.svg");
+}
+
+.miniday-nav-buttons[disabled] {
+ opacity: .3;
+}
+
+.miniday-nav-buttons > .toolbarbutton-icon {
+ margin: 1px;
+}
+
+#miniday-dropdown-button {
+ margin: 2px;
+ -moz-user-focus: normal;
+}
+
+#miniday-dropdown-button > .toolbarbutton-icon,
+#miniday-dropdown-button > .toolbarbutton-text,
+ .miniday-nav-buttons > .toolbarbutton-text {
+ display: none;
+}
+
+#miniday-dropdown-button > .toolbarbutton-menu-dropmarker {
+ padding-inline-start: 0;
+}
+
+#agenda-toolbar {
+ border: none;
+ padding: 1px;
+}
+
+#todaypane-new-event-button {
+ -moz-user-focus: normal;
+}
+
+#todaypane-new-event-button > .toolbarbutton-text {
+ padding-inline-start: 5px;
+}
+
+#agenda-listbox {
+ -moz-appearance: none;
+ -moz-user-focus: normal;
+ margin: 3px 0 0;
+ border-top: 1px solid ThreeDShadow;
+ background-color: -moz-field;
+}
+
+agenda-checkbox-richlist-item {
+ -moz-binding: url("chrome://calendar/content/agenda-listbox.xml#agenda-checkbox-richlist-item");
+ -moz-user-focus: normal;
+}
+
+agenda-richlist-item {
+ -moz-binding: url("chrome://calendar/content/agenda-listbox.xml#agenda-richlist-item");
+ -moz-user-focus: normal;
+}
+
+agenda-allday-richlist-item {
+ -moz-binding: url("chrome://calendar/content/agenda-listbox.xml#agenda-allday-richlist-item");
+ -moz-user-focus: normal;
+}
+.wrap {
+ overflow: visible;
+}
+
+.agenda-container-box {
+ border-bottom: 1px dotted #C0C0C0;
+ margin-inline-start: 4px;
+ margin-inline-end: 4px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.agenda-allday-container-box {
+ border-bottom: 1px dotted #C0C0C0;
+ margin-inline-start: 4px;
+ margin-inline-end: 4px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.agenda-container-box[selected="true"],
+.agenda-allday-container-box[selected="true"],
+.agenda-checkbox[selected="true"],
+.agenda-container-box[selected="true"][current="true"],
+.agenda-allday-container-box[selected="true"][current="true"],
+.agenda-checkbox[selected="true"][current="true"] {
+ background-color: #FDF5A0;
+ color: #000000;
+}
+
+.agenda-container-box[current="true"],
+.agenda-alldaycontainer-box[current="true"],
+.agenda-checkbox[current="true"],
+.agenda-container-box[selected="true"][current="true"][disabled="true"],
+.agenda-allday-container-box[selected="true"][current="true"][disabled="true"],
+.agenda-checkbox[selected="true"][current="true"][disabled="true"] {
+ background-color: #DFEAF4;
+}
+
+.agenda-container-box[selected="true"][disabled="true"],
+.agenda-allday-container-box[selected="true"][disabled="true"],
+.agenda-checkbox[selected="true"][disabled="true"] {
+ color: -moz-dialogText;
+ background-color: -moz-dialog;
+}
+
+.agenda-allday-container-box .item-classification-box {
+ display: none;
+}
+
+.agenda-event-title {
+ margin-top: 0px;
+}
+
+.agenda-event-start {
+ margin-bottom: 0px;
+}
+
+.agenda-new-date {
+ width: 15px;
+ height: 15px;
+ border: 1px solid grey;
+ cursor: pointer;
+}
+
+.agenda-calendar-image {
+ list-style-image: url("chrome://calendar-common/skin/calendar-overlay.png");
+ -moz-image-region: rect(0px 10px 10px 0px);
+ margin-top: 0.35em;
+ margin-inline-start: 4px;
+ width: 10px;
+ height: 10px;
+}
+
+.agenda-multiDayEvent-image {
+ list-style-image: url("chrome://calendar-common/skin/calendar-overlay.png");
+ margin-top: 0.35em;
+ margin-inline-start: 3px;
+ width: 10px;
+ height: 10px;
+ display: none;
+}
+
+.agenda-allday-container-box .agenda-multiDayEvent-image {
+ margin-top: 0.6em;
+}
+
+.agenda-multiDayEvent-image[type="start"] {
+ -moz-image-region: rect(0px 20px 10px 10px);
+ display: -moz-box;
+}
+
+.agenda-multiDayEvent-image[type="continue"] {
+ -moz-image-region: rect(0px 30px 10px 20px);
+ display: -moz-box;
+}
+
+.agenda-multiDayEvent-image[type="end"] {
+ -moz-image-region: rect(0px 40px 10px 30px);
+ display: -moz-box;
+}
diff --git a/calendar/base/themes/common/widgets/calendar-widgets.css b/calendar/base/themes/common/widgets/calendar-widgets.css
new file mode 100644
index 000000000..655d6e255
--- /dev/null
+++ b/calendar/base/themes/common/widgets/calendar-widgets.css
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+treenode-checkbox {
+ -moz-box-align: center;
+ padding-top: 4px;
+ padding-inline-start: 4px;
+ padding-inline-end: 4px;
+ font-weight: bold;
+}
+
+.checkbox-label-box {
+ margin-inline-start: 4px;
+}
+
+.checkbox-icon {
+ margin-inline-end: 2px;
+}
+
+.checkbox-label {
+ margin: 0 !important;
+}
+
+treenode-checkbox > .checkbox-label-center-box > .checkbox-label-box > .checkbox-label {
+ font-weight: bold;
+ border-bottom: 1px solid -moz-Dialog;
+}
+
+.view-navigation-button {
+ list-style-image: url(chrome://calendar-common/skin/widgets/view-navigation.svg);
+ -moz-user-focus: normal;
+}
+
+.view-navigation-button[disabled="true"] {
+ opacity: .3;
+}
+
+.view-navigation-button:-moz-locale-dir(ltr)[type="prev"] > .toolbarbutton-icon,
+.view-navigation-button:-moz-locale-dir(rtl)[type="next"] > .toolbarbutton-icon {
+ transform: scaleX(-1);
+}
+
+.view-navigation-button > .toolbarbutton-icon {
+ margin: 0px !important;
+}
+
+.view-navigation-button > .toolbarbutton-text {
+ display: none;
+}
+
+.selected-text {
+ font-weight: bold;
+}
+
+.selected-text:not([selected="true"]),
+.unselected-text[selected="true"] {
+ visibility: hidden;
+}
+
+.categories-listbox {
+ -moz-appearance: none;
+ background-color: Menu;
+ color: MenuText;
+ margin: 0 0 4px 0;
+ border: 0;
+}
+
+.categories-textbox .textbox-search-icon {
+ list-style-image: none;
+ cursor:default;
+}
+
+.categories-textbox {
+ -moz-appearance: textfield;
+}
diff --git a/calendar/base/themes/common/widgets/images/drag-center.svg b/calendar/base/themes/common/widgets/images/drag-center.svg
new file mode 100644
index 000000000..2e0491801
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/drag-center.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="36" height="15">
+ <line id="plusV_b" x1="31.5" y1="3" x2="31.5" y2="12" stroke="#ffffff" stroke-width="3" stroke-opacity="0.8" fill="none"/>
+ <line id="plusH_b" x1="27" y1="7.5" x2="36" y2="7.5" stroke="#ffffff" stroke-width="3" stroke-opacity="0.8" fill="none"/>
+ <line id="minus_b" x1="0" y1="7.5" x2="8" y2="7.5" stroke="#ffffff" stroke-width="3" stroke-opacity="0.8" fill="none"/>
+ <line id="minus" x1="1" y1="7.5" x2="7" y2="7.5" stroke="#aaaaaa" stroke-width="1" fill="none"/>
+ <line id="plusH" x1="28" y1="7.5" x2="35" y2="7.5" stroke="#aaaaaa" stroke-width="1" fill="none"/>
+ <line id="plusV" x1="31.5" y1="4" x2="31.5" y2="11" stroke="#aaaaaa" stroke-width="1" fill="none"/>
+ <circle id="outer" r="6.5" cy="7.5" cx="18" stroke="#ffffff" stroke-width="2" stroke-opacity="0.8" fill="none"/>
+ <circle id="inner" r="5.5" cy="7.5" cx="18" stroke="#4c4c4c" stroke-width="1.5" stroke-opacity="0.5" fill="none"/>
+</svg>
diff --git a/calendar/base/themes/common/widgets/images/nav-arrow.svg b/calendar/base/themes/common/widgets/images/nav-arrow.svg
new file mode 100644
index 000000000..1e6dcbf74
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/nav-arrow.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="5" height="13">
+ <path d="M 0,2 5,6.5 0,11 z" style="fill:-moz-dialogtext" />
+</svg>
diff --git a/calendar/base/themes/common/widgets/images/nav-today-hov.svg b/calendar/base/themes/common/widgets/images/nav-today-hov.svg
new file mode 100644
index 000000000..4e19cf37e
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/nav-today-hov.svg
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="11" height="13">
+ <path d="m 9.67,6.5 a 4.17,4.1 0 1 1 -8.34,0 4.17,4.1 0 1 1 8.34,0 z"
+ style="fill:none;stroke:-moz-buttonhovertext;stroke-width:2;" />
+</svg>
diff --git a/calendar/base/themes/common/widgets/images/nav-today.svg b/calendar/base/themes/common/widgets/images/nav-today.svg
new file mode 100644
index 000000000..f2b6e3a66
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/nav-today.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="11" height="13">
+ <path
+ d="m 9.67,6.5 a 4.17,4.1 0 1 1 -8.34,0 4.17,4.1 0 1 1 8.34,0 z"
+ style="fill:none;stroke:-moz-dialogtext;stroke-width:2;" />
+</svg>
diff --git a/calendar/base/themes/common/widgets/images/view-navigation-hov.svg b/calendar/base/themes/common/widgets/images/view-navigation-hov.svg
new file mode 100644
index 000000000..692203009
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/view-navigation-hov.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="11" height="11">
+ <path d="M 1,1 10,5.5 1,10 Z" style="fill:-moz-buttonhovertext;" />
+</svg>
diff --git a/calendar/base/themes/common/widgets/images/view-navigation.svg b/calendar/base/themes/common/widgets/images/view-navigation.svg
new file mode 100644
index 000000000..7febb01a5
--- /dev/null
+++ b/calendar/base/themes/common/widgets/images/view-navigation.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="11" height="11">
+ <path d="M 1,1 10,5.5 1,10 Z" style="fill:ButtonText;" />
+</svg>
diff --git a/calendar/base/themes/common/widgets/minimonth.css b/calendar/base/themes/common/widgets/minimonth.css
new file mode 100644
index 000000000..1dc2adf45
--- /dev/null
+++ b/calendar/base/themes/common/widgets/minimonth.css
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+minimonth {
+ --mmMainBackground: #fff;
+ --mmHighlightColor: HighlightText;
+ --mmHighlightBackground: Highlight;
+ --mmHighlightBorderColor: Highlight;
+ --mmBoxBackground: #f5f5f6;
+ --mmBoxBorderColor: #c0c0c0;
+ --mmDayColor: #2e4e73;
+ --mmDayBorderColor: #fff;
+ --mmDayOtherColor: #b2b2b2;
+ --mmDayOtherBackground: #f0f0f0;
+ --mmDayOtherBorderColor: #f0f0f0;
+ --mmDayTodayBackground: #dfeaf4;
+ --mmDayTodayBorderColor: #67acd8;
+ --mmDaySelectedColor: #2e4e73;
+ --mmDaySelectedBackground: #fffabc;
+ --mmDaySelectedBorderColor: #d9c585;
+ --mmDaySelectedTodayBackground: #f2edb2;
+ --mmDaySelectedTodayBorderColor: #67acd8;
+}
+
+:root[systemcolors] minimonth {
+ --mmMainBackground: -moz-field;
+ --mmBoxBackground: -moz-Dialog;
+ --mmBoxBorderColor: ThreeDShadow;
+ --mmDayColor: WindowText;
+ --mmDayBorderColor: Window;
+ --mmDayOtherColor: GrayText;
+ --mmDayOtherBackground: ButtonFace;
+ --mmDayOtherBorderColor: Transparent;
+ --mmDayTodayBackground: -moz-field;
+ --mmDayTodayBorderColor: Highlight;
+ --mmDaySelectedColor: HighlightText;
+ --mmDaySelectedBackground: Highlight;
+ --mmDaySelectedBorderColor: ButtonFace;
+ --mmDaySelectedTodayBackground: Highlight;
+ --mmDaySelectedTodayBorderColor: ButtonFace;
+}
+
+minimonth {
+ background-color: var(--mmMainBackground);
+ border: 0px;
+ padding: 4px;
+}
+
+.minimonth-month-box {
+ background-color: var(--mmBoxBackground);
+ border: 1px dotted var(--mmBoxBorderColor);
+}
+
+.minimonth-month-name {
+ font-weight: bold;
+ padding: 0px;
+ -moz-user-focus: normal;
+}
+
+.minimonth-month-name-readonly {
+ text-align: right;
+ font-weight: bold;
+}
+
+.minimonth-year-name {
+ min-width: 4em;
+ font-weight: bold;
+ padding: 0px;
+ -moz-user-focus: normal;
+}
+
+.minimonth-year-name-readonly {
+ min-width: 4em;
+ font-weight: bold;
+ padding-inline-start: 4px;
+}
+
+.minimonth-month-name > .toolbarbutton-text {
+ text-align: right;
+}
+
+.minimonth-month-name > .toolbarbutton-icon,
+.minimonth-year-name > .toolbarbutton-icon {
+ display: none;
+}
+
+.minimonth-nav-btns > .toolbarbutton-icon {
+ margin: 1px;
+}
+
+.minimonth-nav-btns {
+ padding: 0px;
+ min-width: 19px;
+ list-style-image: url("chrome://calendar-common/skin/widgets/nav-arrow.svg");
+ -moz-user-focus: normal;
+}
+
+.minimonth-nav-btns > .toolbarbutton-text {
+ display: none;
+}
+
+.minimonth-nav-btns[disabled] {
+ opacity: .3;
+}
+
+.minimonth-nav-btns:-moz-locale-dir(ltr)[dir="-1"],
+.minimonth-nav-btns:-moz-locale-dir(rtl)[dir="1"] {
+ transform: scaleX(-1);
+}
+
+.minimonth-nav-btns[dir="0"] {
+ list-style-image: url("chrome://calendar-common/skin/widgets/nav-today.svg");
+}
+
+.minimonth-row-header {
+ text-align: center;
+}
+
+.minimonth-day {
+ color: var(--mmDayColor);
+ text-align: center;
+ border: 1px solid var(--mmDayBorderColor);
+ background-color: var(--mmMainBackground);
+ min-height: 16px;
+}
+
+.minimonth-row-header-week {
+ color: var(--mmDayOtherColor);
+ text-align: center;
+ border-right: 1px dotted var(--mmDayOtherColor);
+}
+
+.minimonth-week {
+ color: var(--mmDayOtherColor);
+ text-align: center;
+ border: 1px solid var(--mmDayBorderColor);
+ border-right: 1px dotted var(--mmDayOtherColor);
+ background-color: var(--mmMainBackground);
+ min-height: 16px;
+}
+
+.minimonth-day[othermonth="true"] {
+ color: var(--mmDayOtherColor);
+ background-color: var(--mmDayOtherBackground);
+ border: 1px solid var(--mmDayOtherBorderColor);
+}
+
+.minimonth-day[today="true"] {
+ background-color: var(--mmDayTodayBackground);
+ border: 1px solid var(--mmDayTodayBorderColor);
+}
+
+.minimonth-day[selected="true"] {
+ background-color: var(--mmDaySelectedBackground);
+ color: var(--mmDaySelectedColor);
+ border: 1px solid var(--mmDaySelectedBorderColor);
+}
+
+#repeat-until-datepicker .minimonth-day[extra="true"],
+#repeat-until-date .minimonth-day[extra="true"] {
+ border: 1px solid var(--mmDayOtherColor);
+}
+
+#repeat-until-datepicker .minimonth-day:hover[extra="true"],
+#repeat-until-date .minimonth-day:hover[extra="true"] {
+ border: 1px solid var(--mmHighlightBorderColor);
+}
+
+.minimonth-day[selected="true"][today="true"] {
+ background-color: var(--mmDaySelectedTodayBackground);
+ border: 1px solid var(--mmDaySelectedTodayBorderColor);
+}
+
+.minimonth-day[busy] {
+ font-weight: bold;
+}
+
+.minimonth-day:hover[interactive] {
+ cursor: pointer;
+ border: 1px solid var(--mmHighlightBorderColor);
+}
+
+.minimonth-day:active[interactive] {
+ background-color: var(--mmHighlightBackground);
+ color: var(--mmHighlightColor);
+}
+
+.minimonth-list {
+ padding-inline-start: 1em;
+ padding-inline-end: 1em;
+}
+
+.minimonth-list[current="true"] {
+ font-weight: bold;
+}
+
+.minimonth-list:hover {
+ background-color: var(--mmHighlightBackground);
+ color: var(--mmHighlightColor);
+ cursor: pointer;
+}
diff --git a/calendar/base/themes/linux/calendar-daypicker.css b/calendar/base/themes/linux/calendar-daypicker.css
new file mode 100644
index 000000000..f8a3c0680
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-daypicker.css
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-daypicker.css);
+
+daypicker {
+ border-top: 1px solid ThreeDShadow;
+ border-left: 1px solid ThreeDShadow;
+}
+
+daypicker[bottom="true"] {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+daypicker[right="true"] {
+ border-right: 1px solid ThreeDShadow;
+}
diff --git a/calendar/base/themes/linux/calendar-management.css b/calendar/base/themes/linux/calendar-management.css
new file mode 100644
index 000000000..8017f4c53
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-management.css
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-management.css);
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol) {
+ list-style-image: url(chrome://calendar-common/skin/checkbox-images.png);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol) {
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked) {
+ -moz-image-region: rect(0 26px 13px 13px);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, disabled) {
+ -moz-image-region: rect(0 39px 13px 26px);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-cell-text(disabled) {
+ color: GrayText;
+}
diff --git a/calendar/base/themes/linux/calendar-task-tree.css b/calendar/base/themes/linux/calendar-task-tree.css
new file mode 100644
index 000000000..e946a8282
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-task-tree.css
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-task-tree.css);
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed),
+.calendar-task-tree-col-completed {
+ list-style-image: url(chrome://calendar-common/skin/checkbox-images.png);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, completed),
+.calendar-task-tree-col-completed {
+ -moz-image-region: rect(0 26px 13px 13px);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, duetoday),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, overdue),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, future),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, inprogress) {
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, repeating) {
+ -moz-image-region: rect(0 39px 13px 26px);
+}
diff --git a/calendar/base/themes/linux/calendar-task-view.css b/calendar/base/themes/linux/calendar-task-view.css
new file mode 100644
index 000000000..88dcf3f19
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-task-view.css
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-task-view.css);
+
+#calendar-task-view-splitter {
+ -moz-appearance: none;
+ border-bottom: 1px solid ThreeDShadow;
+ /* splitter grip area */
+ height: 5px;
+ /* make only the splitter border visible */
+ margin-top: -5px;
+ /* because of the negative margin needed to make the splitter visible */
+ position: relative;
+ z-index: 10;
+}
+
+#task-addition-box {
+ height: 37px;
+}
+
+#calendar-task-details-container {
+ padding-top: 1px;
+}
+
+#other-actions-box {
+ margin-inline-end: -1px;
+}
+
+.task-details-name {
+ color: windowtext;
+ opacity: 0.5; /* lower contrast */
+}
+
+#calendar-task-details-grid > rows > .item-date-row > .headline {
+ color: windowtext;
+ opacity: 0.5; /* lower contrast */
+}
+
+.task-details-value {
+ color: WindowText;
+}
+
+#task-text-filter-field {
+ margin: 5px;
+}
+
+/* ::::: task actions toolbar ::::: */
+
+#task-actions-toolbox {
+ -moz-appearance: none;
+}
+
+#task-actions-toolbar {
+ min-height: 0;
+ padding: 0;
+}
+
+#task-actions-category {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#category);
+}
+
+#task-actions-markcompleted {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete);
+}
+
+#task-actions-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+}
+
+#calendar-delete-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+}
+
+#task-actions-toolbar[brighttext] > #task-actions-category {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#category-inverted);
+}
+
+#task-actions-toolbar[brighttext] > #task-actions-markcompleted {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete-inverted);
+}
+
+#task-actions-toolbar[brighttext] > #task-actions-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-inverted);
+}
+
+
+#task-actions-toolbar[brighttext] > #calendar-delete-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted);
+}
+
+#calendar-add-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask);
+ -moz-image-region: rect(0 18px 18px 0);
+}
+
+#calendar-add-task-button[disabled="true"] > .toolbarbutton-icon {
+ opacity: 0.4;
+}
+
+#calendar-add-task-button > .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ margin: -1px;
+}
diff --git a/calendar/base/themes/linux/calendar-unifinder.css b/calendar/base/themes/linux/calendar-unifinder.css
new file mode 100644
index 000000000..a51892749
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-unifinder.css
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-unifinder.css);
+
+/* restyle splitter-border to match Thunderbird's layout */
+#calendar-view-splitter {
+ -moz-appearance: none;
+ border-bottom: 1px solid ThreeDShadow;
+ /* splitter grip area */
+ height: 5px;
+ /* make only the splitter border visible */
+ margin-top: -5px;
+ /* because of the negative margin needed to make the splitter visible */
+ position: relative;
+ z-index: 10;
+}
+
+#bottom-events-box {
+ border-left: 1px solid ThreeDShadow;
+}
+
+/* added for new id ..... search box ..... */
+#unifinder-searchBox {
+ background-color: transparent;
+ border-bottom: 1px solid ThreeDShadow;
+ height: 37px;
+}
+
+.unifinder-closebutton {
+ -moz-appearance: none;
+ border: none;
+ padding: 2px;
+ background: transparent;
+}
diff --git a/calendar/base/themes/linux/calendar-views.css b/calendar/base/themes/linux/calendar-views.css
new file mode 100644
index 000000000..c60d3d2d7
--- /dev/null
+++ b/calendar/base/themes/linux/calendar-views.css
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-views.css);
+
+/* Navigation controls for the views */
+#calendar-nav-control {
+ background-color: AppWorkspace;
+ border-inline-start: 1px solid ThreeDShadow;
+ padding-top: 1px;
+}
+
+#view-deck {
+ background-color: -moz-Field;
+ border-width: 0 0 1px 1px;
+}
+
+tab[calview] {
+ background-color: rgba(0, 0, 0, .1);
+ color: ButtonText;
+ border: 1px solid ThreeDShadow;
+ font-size: 14px;
+}
+
+tab[calview][selected="true"],
+tab[calview][selected="true"]:hover {
+ background-color: -moz-Field;
+}
+
+tab[calview]:hover {
+ background-color: ButtonHighlight;
+}
+
+#calendarWeek {
+ margin-top: 4px;
+ margin-bottom: 0px;
+}
+
+.navigation-inner-box {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.navigation-bottombox {
+ background-color: -moz-Field;
+}
+
+.navigation-spacer-box {
+ min-width: 4px;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.view-navigation-button {
+ margin-inline-start: 2px;
+ margin-inline-end: 2px;
+}
+
+.today-navigation-button {
+ padding-top: 0px !important; /* a workaround to center the label vertically on Windows */
+}
diff --git a/calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css b/calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css
new file mode 100644
index 000000000..ae4bbc79a
--- /dev/null
+++ b/calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/dialogs/calendar-alarm-dialog.css);
+
+menupopup[type="snooze-menupopup"] > arrowscrollbox {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-snooze-popup);
+}
+
+.snooze-popup-ok-button:hover {
+ background-color: -moz-menuhover;
+}
+
+.snooze-popup-cancel-button:hover {
+ background-color: -moz-menuhover;
+}
diff --git a/calendar/base/themes/linux/dialogs/calendar-event-dialog.css b/calendar/base/themes/linux/dialogs/calendar-event-dialog.css
new file mode 100644
index 000000000..01bd2ebad
--- /dev/null
+++ b/calendar/base/themes/linux/dialogs/calendar-event-dialog.css
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*--------------------------------------------------------------------
+ * Event dialog keep duration button
+ *-------------------------------------------------------------------*/
+
+#keepduration-button {
+ min-width: 21px;
+}
+
+#timezone-endtime {
+ margin-inline-start: 16px;
+}
+
+/*--------------------------------------------------------------------
+ * Event dialog toolbar buttons
+ *-------------------------------------------------------------------*/
+
+#button-save {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save);
+}
+
+#button-saveandclose {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-close);
+}
+
+#button-attendees {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#address);
+}
+
+#button-privacy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#security);
+}
+
+#button-url {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#attach);
+}
+
+#button-delete.cal-event-toolbarbutton {
+ /* !important to override the SM #button-delete states */
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete) !important;
+ -moz-image-region: auto !important;
+}
+
+#button-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+}
+
+#button-status {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#status);
+}
+
+#button-freebusy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#freebusy);
+}
+
+#button-timezones {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#timezones);
+}
+
+toolbar[brighttext] #button-save {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-inverted);
+}
+
+toolbar[brighttext] #button-saveandclose {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-close-inverted);
+}
+
+toolbar[brighttext] #button-attendees {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#address-inverted);
+}
+
+toolbar[brighttext] #button-privacy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#security-inverted);
+}
+
+toolbar[brighttext] #button-url {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#attach-inverted);
+}
+
+toolbar[brighttext] #button-delete.cal-event-toolbarbutton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted) !important;
+}
+
+toolbar[brighttext] #button-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-inverted);
+}
+
+toolbar[brighttext] #button-status {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#status-inverted);
+}
+
+toolbar[brighttext] #button-freebusy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#freebusy-inverted);
+}
+
+toolbar[brighttext] #button-timezones {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#timezones-inverted);
+}
diff --git a/calendar/base/themes/linux/dialogs/calendar-invitations-dialog.css b/calendar/base/themes/linux/dialogs/calendar-invitations-dialog.css
new file mode 100644
index 000000000..31cd81a37
--- /dev/null
+++ b/calendar/base/themes/linux/dialogs/calendar-invitations-dialog.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/dialogs/calendar-invitations-dialog.css);
diff --git a/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar-small.png b/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar-small.png
new file mode 100644
index 000000000..d4b19241e
--- /dev/null
+++ b/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar-small.png
Binary files differ
diff --git a/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar.png b/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar.png
new file mode 100644
index 000000000..044b7ee5e
--- /dev/null
+++ b/calendar/base/themes/linux/dialogs/images/calendar-event-dialog-toolbar.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/cal-icon24.png b/calendar/base/themes/linux/images/cal-icon24.png
new file mode 100644
index 000000000..01cd8a61c
--- /dev/null
+++ b/calendar/base/themes/linux/images/cal-icon24.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/cal-icon32.png b/calendar/base/themes/linux/images/cal-icon32.png
new file mode 100644
index 000000000..1ecfba92b
--- /dev/null
+++ b/calendar/base/themes/linux/images/cal-icon32.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/calendar-occurrence-prompt.png b/calendar/base/themes/linux/images/calendar-occurrence-prompt.png
new file mode 100644
index 000000000..aa6042b5b
--- /dev/null
+++ b/calendar/base/themes/linux/images/calendar-occurrence-prompt.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/tasks-actions.png b/calendar/base/themes/linux/images/tasks-actions.png
new file mode 100644
index 000000000..c822fbd0d
--- /dev/null
+++ b/calendar/base/themes/linux/images/tasks-actions.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/toolbar-large.png b/calendar/base/themes/linux/images/toolbar-large.png
new file mode 100644
index 000000000..0aa38aa49
--- /dev/null
+++ b/calendar/base/themes/linux/images/toolbar-large.png
Binary files differ
diff --git a/calendar/base/themes/linux/images/toolbar-small.png b/calendar/base/themes/linux/images/toolbar-small.png
new file mode 100644
index 000000000..60da4f8de
--- /dev/null
+++ b/calendar/base/themes/linux/images/toolbar-small.png
Binary files differ
diff --git a/calendar/base/themes/linux/preferences/Options.png b/calendar/base/themes/linux/preferences/Options.png
new file mode 100644
index 000000000..a62e74429
--- /dev/null
+++ b/calendar/base/themes/linux/preferences/Options.png
Binary files differ
diff --git a/calendar/base/themes/linux/preferences/preferences.css b/calendar/base/themes/linux/preferences/preferences.css
new file mode 100644
index 000000000..c42048849
--- /dev/null
+++ b/calendar/base/themes/linux/preferences/preferences.css
@@ -0,0 +1,94 @@
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*/
+
+/* Global Styles */
+#CalendarPreferences radio[pane] {
+ list-style-image: url("chrome://calendar/skin/preferences/Options.png");
+}
+
+radio[pane=paneGeneral] {
+ -moz-image-region: rect(0px, 32px, 32px, 0px)
+}
+radio[pane=paneGeneral]:hover, radio[pane=paneGeneral][selected="true"] {
+ -moz-image-region: rect(32px, 32px, 64px, 0px)
+}
+
+radio[pane=paneAlarms] {
+ -moz-image-region: rect(0px, 64px, 32px, 32px)
+}
+radio[pane=paneAlarms]:hover, radio[pane=paneAlarms][selected="true"] {
+ -moz-image-region: rect(32px, 64px, 64px, 32px)
+}
+
+radio[pane=paneCategories] {
+ -moz-image-region: rect(0px, 128px, 32px, 96px)
+}
+radio[pane=paneCategories]:hover, radio[pane=paneCategories][selected="true"] {
+ -moz-image-region: rect(32px, 128px, 64px, 96px)
+}
+
+radio[pane=paneViews] {
+ -moz-image-region: rect(0px, 192px, 32px, 160px)
+}
+radio[pane=paneViews]:hover, radio[pane=paneViews][selected="true"] {
+ -moz-image-region: rect(32px, 192px, 64px, 160px)
+}
+
+radio[pane=paneTimezones] {
+ -moz-image-region: rect(0px, 224px, 32px, 192px)
+}
+radio[pane=paneTimezones]:hover, radio[pane=paneTimezones][selected="true"] {
+ -moz-image-region: rect(32px, 224px, 64px, 192px)
+}
+
+radio[pane=paneAdvanced] {
+ -moz-image-region: rect(0px, 160px, 32px, 128px)
+}
+radio[pane=paneAdvanced]:hover, radio[pane=paneAdvanced][selected="true"] {
+ -moz-image-region: rect(32px, 160px, 64px, 128px)
+}
+
+/* File Field Widget */
+filefield {
+ margin: 2px 4px;
+ -moz-appearance: textfield;
+}
+
+.fileFieldContentBox {
+ background-color: -moz-Dialog;
+ color: -moz-DialogText;
+ margin: 1px;
+}
+
+filefield[disabled="true"] .fileFieldContentBox {
+ opacity: 0.5;
+}
+
+filefield[disabled="true"] .fileFieldIcon {
+ opacity: 0.2;
+}
+
+.fileFieldIcon {
+ width: 16px;
+ height: 16px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ margin-inline-start: 2px;
+ margin-inline-end: 4px;
+}
+
+.fileFieldLabel {
+ -moz-appearance: none;
+ background-color: transparent;
+ border: none;
+ padding: 1px 0px 0px;
+ margin: 0px;
+}
+
+tabpanels caption {
+ background-color: -moz-Dialog;
+}
diff --git a/calendar/base/themes/linux/today-pane.css b/calendar/base/themes/linux/today-pane.css
new file mode 100644
index 000000000..9ca34821d
--- /dev/null
+++ b/calendar/base/themes/linux/today-pane.css
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/today-pane.css);
+
+#today-pane-panel {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+#today-pane-panel:-moz-lwtheme {
+ box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset;
+}
+
+#today-pane-panel:-moz-lwtheme > sidebarheader {
+ background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 19px);
+}
+
+#today-pane-splitter {
+ -moz-appearance: none;
+ border-bottom: 3px double ThreeDShadow;
+ /* splitter grip area */
+ height: 5px;
+ /* make only the splitter border visible */
+ margin-top: -3px;
+ /* because of the negative margin needed to make the splitter visible */
+ position: relative;
+ z-index: 10;
+}
+
+.today-pane-cycler {
+ padding-inline-end: 0 !important;
+}
+
+.today-pane-cycler {
+ list-style-image: url("chrome://calendar-common/skin/today-pane-cycler.svg#normal");
+}
+
+.today-pane-cycler:-moz-lwtheme-brighttext {
+ list-style-image: url("chrome://calendar-common/skin/today-pane-cycler.svg#inverted");
+}
+
+.today-pane-cycler[dir="prev"]:-moz-locale-dir(ltr) > .toolbarbutton-icon,
+.today-pane-cycler[dir="next"]:-moz-locale-dir(rtl) > .toolbarbutton-icon {
+ transform: scaleX(-1);
+}
+
+.today-subpane {
+ border-color: ThreeDShadow;
+}
+
+#mini-day-image {
+ background-image: linear-gradient(transparent, rgba(0, 0, 0, .1));
+}
+
+.miniday-nav-buttons {
+ max-width: 19px;
+}
+
+#next-day-button > .toolbarbutton-icon {
+ -moz-appearance: button-arrow-next;
+}
+
+#previous-day-button > .toolbarbutton-icon {
+ -moz-appearance: button-arrow-previous;
+}
+
+#today-button {
+ max-width: none;
+}
+
+#today-button:hover {
+ list-style-image: url("chrome://calendar-common/skin/widgets/nav-today-hov.svg");
+}
+
+#miniday-dropdown-button {
+ max-width: 18px;
+}
+
+#todaypane-new-event-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+}
+
+#todaypane-new-event-button[disabled="true"] > .toolbarbutton-icon {
+ opacity: 0.4;
+}
+
+#todaypane-new-event-button > .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ margin: -1px;
+}
diff --git a/calendar/base/themes/linux/widgets/calendar-widgets.css b/calendar/base/themes/linux/widgets/calendar-widgets.css
new file mode 100644
index 000000000..461b78aac
--- /dev/null
+++ b/calendar/base/themes/linux/widgets/calendar-widgets.css
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/widgets/calendar-widgets.css);
+
+treenode-checkbox[checked="true"] > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty-open.png);
+}
+
+treenode-checkbox > .checkbox-check {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ border: none;
+ width: 9px; /* The image's width is 9 pixels */
+ height: 9px;
+ background-image: url(chrome://global/skin/tree/twisty-clsd.png);
+}
+
+#task-tree-filtergroup {
+ padding-inline-start: 12px;
+}
+
+calendar-list-tree .tree-scrollable-columns {
+ padding-inline-start: 18px;
+}
+
+.view-navigation-button:hover {
+ list-style-image: url(chrome://calendar-common/skin/widgets/view-navigation-hov.svg);
+}
+
+.toolbarbutton-icon-begin {
+ margin-inline-end: 5px;
+}
+
+.toolbarbutton-icon-end {
+ margin-inline-start: 5px;
+}
diff --git a/calendar/base/themes/windows/calendar-daypicker.css b/calendar/base/themes/windows/calendar-daypicker.css
new file mode 100644
index 000000000..f8a3c0680
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-daypicker.css
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-daypicker.css);
+
+daypicker {
+ border-top: 1px solid ThreeDShadow;
+ border-left: 1px solid ThreeDShadow;
+}
+
+daypicker[bottom="true"] {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+daypicker[right="true"] {
+ border-right: 1px solid ThreeDShadow;
+}
diff --git a/calendar/base/themes/windows/calendar-management.css b/calendar/base/themes/windows/calendar-management.css
new file mode 100644
index 000000000..8017f4c53
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-management.css
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-management.css);
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol) {
+ list-style-image: url(chrome://calendar-common/skin/checkbox-images.png);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol) {
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked) {
+ -moz-image-region: rect(0 26px 13px 13px);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, disabled) {
+ -moz-image-region: rect(0 39px 13px 26px);
+}
+
+calendar-list-tree > tree > treechildren::-moz-tree-cell-text(disabled) {
+ color: GrayText;
+}
diff --git a/calendar/base/themes/windows/calendar-task-tree.css b/calendar/base/themes/windows/calendar-task-tree.css
new file mode 100644
index 000000000..8bbf39b27
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-task-tree.css
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-task-tree.css);
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed),
+.calendar-task-tree-col-completed {
+ list-style-image: url(chrome://calendar-common/skin/checkbox-images.png);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, completed),
+.calendar-task-tree-col-completed {
+ -moz-image-region: rect(0 26px 13px 13px);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, duetoday),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, overdue),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, future),
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, inprogress) {
+ -moz-image-region: rect(0 13px 13px 0);
+}
+
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed, repeating) {
+ -moz-image-region: rect(0 39px 13px 26px);
+}
+
+/* Use on Vista and up default theme a dark text color when selected focus */
+@media not all and (-moz-os-version: windows-xp) {
+ @media (-moz-windows-default-theme) {
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(selected, focus),
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(duetoday, selected, focus),
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(future, selected, focus),
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(completed, selected, focus) {
+ color: -moz-FieldText;
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-row(inprogress, selected, focus) {
+ -moz-border-top-colors: green rgba(255, 255, 255, .4);
+ -moz-border-right-colors: green rgba(255, 255, 255, .4);
+ -moz-border-left-colors: green rgba(255, 255, 255, .4);
+ -moz-border-bottom-colors: green rgba(255, 255, 255, .6);
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(inprogress, selected, focus) {
+ color: white;
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-row(overdue, selected, focus) {
+ -moz-border-top-colors: red rgba(255, 255, 255, .4);
+ -moz-border-right-colors: red rgba(255, 255, 255, .4);
+ -moz-border-left-colors: red rgba(255, 255, 255, .4);
+ -moz-border-bottom-colors: red rgba(255, 255, 255, .6);
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-cell-text(overdue, selected, focus) {
+ color: white;
+ }
+
+ @media (-moz-os-version: windows-vista),
+ (-moz-os-version: windows-win7) {
+ .calendar-task-tree > treechildren::-moz-tree-row(inprogress, selected, focus) {
+ background: linear-gradient(rgba(0, 128, 0, .28), rgba(0, 128, 0, .5));
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-row(overdue, selected, focus) {
+ background: linear-gradient(rgba(255, 0, 0, .28), rgba(255, 0, 0, .5));
+ }
+ }
+
+ @media (-moz-os-version: windows-win8),
+ (-moz-os-version: windows-win10) {
+ .calendar-task-tree > treechildren::-moz-tree-row(inprogress, selected, focus) {
+ background: linear-gradient(rgba(0, 128, 0, .5), rgba(0, 128, 0, .5));
+ }
+
+ .calendar-task-tree > treechildren::-moz-tree-row(overdue, selected, focus) {
+ background: linear-gradient(rgba(255, 0, 0, .5), rgba(255, 0, 0, .5));
+ }
+ }
+ }
+}
diff --git a/calendar/base/themes/windows/calendar-task-view.css b/calendar/base/themes/windows/calendar-task-view.css
new file mode 100644
index 000000000..89b003ccd
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-task-view.css
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-task-view.css);
+
+#calendar-task-view-splitter {
+ border: none;
+ min-height: 5px;
+}
+
+#calendar-task-details-container {
+ border-top: 1px solid ThreeDShadow;
+ border-left: 1px solid ThreeDShadow;
+ border-right: 0px;
+ border-bottom: 0px;
+ overflow: hidden;
+}
+
+#other-actions-box {
+ margin-inline-end: -2px;
+}
+
+.task-details-name {
+ color: windowtext;
+ opacity: 0.5; /* lower contrast */
+}
+
+#calendar-task-details-grid > rows > .item-date-row > .headline {
+ color: windowtext;
+ opacity: 0.5; /* lower contrast */
+}
+
+.task-details-value {
+ color: WindowText;
+}
+
+#task-text-filter-field {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+#task-text-filter-field .textbox-search-icons {
+ margin-bottom: -1px;
+}
+
+/* ::::: task actions toolbar ::::: */
+
+#task-actions-toolbox {
+ -moz-appearance: none;
+}
+
+#task-actions-toolbar {
+ min-height: 0;
+ padding: 0;
+}
+
+@media (-moz-os-version: windows-xp) {
+ #task-addition-box {
+ border-inline-start: 1px solid ThreeDShadow;
+ }
+
+ #calendar-task-tree {
+ border-inline-start: 1px solid ThreeDShadow;
+ border-bottom: 1px solid ThreeDHighlight;
+ }
+
+ #view-task-edit-field,
+ #task-text-filter-field {
+ margin-top: 4px;
+ margin-bottom: 3px;
+ }
+
+ #task-actions-category {
+ list-style-image: url(chrome://calendar/skin/tasks-actions.png);
+ -moz-image-region: rect(0 16px 16px 0);
+ }
+
+ #task-actions-markcompleted {
+ list-style-image: url(chrome://calendar/skin/tasks-actions.png);
+ -moz-image-region: rect(0 32px 16px 16px);
+ }
+
+ #task-actions-priority {
+ list-style-image: url(chrome://calendar/skin/tasks-actions.png);
+ -moz-image-region: rect(0 48px 16px 32px);
+ }
+
+ #calendar-delete-task-button {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 48px 16px 32px);
+ }
+
+ #calendar-add-task-button {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 256px 16px 240px);
+ }
+
+ #calendar-add-task-button[disabled="true"] {
+ -moz-image-region: rect(32px 256px 48px 240px);
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #task-actions-category {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#category);
+ }
+
+ #task-actions-markcompleted {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete);
+ }
+
+ #task-actions-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+ }
+
+ #calendar-delete-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+ }
+
+ #task-actions-toolbar[brighttext] > #task-actions-category {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#category-inverted);
+ }
+
+ #task-actions-toolbar[brighttext] > #task-actions-markcompleted {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete-inverted);
+ }
+
+ #task-actions-toolbar[brighttext] > #task-actions-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-inverted);
+ }
+
+
+ #task-actions-toolbar[brighttext] > #calendar-delete-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted);
+ }
+
+ #calendar-add-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask);
+ -moz-image-region: rect(0 18px 18px 0);
+ }
+
+ #calendar-add-task-button[disabled="true"] > .toolbarbutton-icon {
+ opacity: 0.4;
+ }
+
+ #view-task-edit-field,
+ #task-text-filter-field {
+ width: 15em;
+ }
+
+ #calendar-task-box #calendar-task-view-splitter {
+ border: none;
+ border-bottom: 1px solid #A9B7C9;
+ min-height: 0;
+ height: 5px;
+ background-color: transparent;
+ margin-top: -5px;
+ position: relative;
+ z-index: 10;
+ }
+
+ #calendar-nav-control {
+ border-top-width: 0;
+ }
+
+ #calendar-task-details-container {
+ border-top-width: 0;
+ padding-top: 0;
+ }
+
+ @media (-moz-windows-default-theme) {
+ #task-addition-box {
+ background-color: #f8f8f8;
+ height: 32px;
+ }
+ }
+}
+
+@media (-moz-os-version: windows-vista),
+ (-moz-os-version: windows-win7) {
+ #view-task-edit-field,
+ #task-text-filter-field {
+ margin-top: 4px;
+ margin-bottom: 4px;
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #task-actions-category {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#category-flat);
+ }
+
+ #task-actions-markcompleted {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete-flat);
+ }
+
+ #task-actions-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-flat);
+ }
+
+ #calendar-add-task-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-flat);
+ }
+}
diff --git a/calendar/base/themes/windows/calendar-unifinder.css b/calendar/base/themes/windows/calendar-unifinder.css
new file mode 100644
index 000000000..fcc0375af
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-unifinder.css
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-unifinder.css);
+
+#bottom-events-box {
+ border-inline-start: 1px solid ThreeDShadow;
+}
+
+#unifinder-searchBox {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.unifinder-closebutton {
+ -moz-appearance: none;
+ border: none;
+ padding: 2px;
+ background: transparent;
+}
+
+@media (-moz-os-version: windows-xp) {
+ #unifinder-searchBox {
+ background-color: transparent;
+ height: 30px;
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ @media (-moz-windows-default-theme) {
+ #unifinder-searchBox {
+ background-color: #f8f8f8;
+ height: 33px;
+ }
+ }
+}
+
+@media (-moz-os-version: windows-win8),
+ (-moz-os-version: windows-win10) {
+ #unifinder-searchBox {
+ height: 35px;
+ }
+
+ .unifinder-closebutton {
+ -moz-image-region: rect(0 20px 20px 0);
+ }
+
+ .unifinder-closebutton:hover {
+ -moz-image-region: rect(0 40px 20px 20px);
+ }
+
+ .unifinder-closebutton:hover:active {
+ -moz-image-region: rect(0 60px 20px 40px);
+ }
+}
diff --git a/calendar/base/themes/windows/calendar-views.css b/calendar/base/themes/windows/calendar-views.css
new file mode 100644
index 000000000..137222a18
--- /dev/null
+++ b/calendar/base/themes/windows/calendar-views.css
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/calendar-views.css);
+
+/* Navigation controls for the views */
+#calendar-nav-control {
+ background-color: ButtonFace;
+ border-top: 1px solid ThreeDShadow;
+ border-inline-start: 1px solid ThreeDShadow;
+ padding-top: 1px;
+}
+
+#view-deck {
+ background-color: -moz-Field;
+ border-width: 0;
+ border-inline-start: 1px;
+}
+
+tab[calview] {
+ background-color: rgba(0, 0, 0, .1);
+ color: ButtonText;
+ border: 1px solid ThreeDShadow;
+ font-size: 14px;
+}
+
+tab[calview][selected="true"],
+tab[calview][selected="true"]:hover {
+ background-color: -moz-Field;
+}
+
+tab[calview]:hover {
+ background-color: ButtonHighlight;
+}
+
+#calendarWeek {
+ margin-top: 4px;
+ margin-bottom: 0px;
+}
+
+.navigation-inner-box {
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.navigation-bottombox {
+ background-color: -moz-Field;
+}
+
+.navigation-spacer-box {
+ min-width: 4px;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.view-navigation-button {
+ margin-inline-start: 2px;
+ margin-inline-end: 2px;
+}
+
+.today-navigation-button {
+ padding-top: 0px !important; /* a workaround to center the label vertically on Windows */
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #calendar-view-box #calendar-view-splitter {
+ border: none;
+ border-bottom: 1px solid #A9B7C9;
+ min-height: 0;
+ height: 5px;
+ background-color: transparent;
+ margin-top: -5px;
+ position: relative;
+ z-index: 10;
+ }
+}
diff --git a/calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css b/calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css
new file mode 100644
index 000000000..ae4bbc79a
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/dialogs/calendar-alarm-dialog.css);
+
+menupopup[type="snooze-menupopup"] > arrowscrollbox {
+ -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-snooze-popup);
+}
+
+.snooze-popup-ok-button:hover {
+ background-color: -moz-menuhover;
+}
+
+.snooze-popup-cancel-button:hover {
+ background-color: -moz-menuhover;
+}
diff --git a/calendar/base/themes/windows/dialogs/calendar-event-dialog.css b/calendar/base/themes/windows/dialogs/calendar-event-dialog.css
new file mode 100644
index 000000000..fc3520d86
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/calendar-event-dialog.css
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*--------------------------------------------------------------------
+ * Event dialog keep duration button
+ *-------------------------------------------------------------------*/
+
+#keepduration-button {
+ min-width: 21px;
+}
+
+#timezone-endtime {
+ margin-inline-start: 16px;
+}
+
+@media (-moz-os-version: windows-xp) {
+ .cal-event-toolbarbutton {
+ list-style-image: url("chrome://calendar/skin/calendar-event-dialog-toolbar.png");
+ }
+
+ toolbar[iconsize="small"] .cal-event-toolbarbutton {
+ list-style-image: url("chrome://calendar/skin/calendar-event-dialog-toolbar-small.png");
+ }
+
+ #button-attendees {
+ -moz-image-region: rect(0px 48px 24px 24px);
+ }
+ #button-attendees[disabled="true"] {
+ -moz-image-region: rect(48px 48px 72px 24px);
+ }
+
+ toolbar[iconsize="small"] #button-attendees {
+ -moz-image-region: rect(0px 32px 16px 16px);
+ }
+ toolbar[iconsize="small"] #button-attendees[disabled="true"] {
+ -moz-image-region: rect(32px 32px 48px 16px);
+ }
+
+ #button-url {
+ -moz-image-region: rect(0px 96px 24px 72px);
+ }
+ #button-url[disabled="true"] {
+ -moz-image-region: rect(48px 96px 72px 72px);
+ }
+
+ toolbar[iconsize="small"] #button-url {
+ -moz-image-region: rect(0px 64px 16px 48px);
+ }
+ toolbar[iconsize="small"] #button-url[disabled="true"] {
+ -moz-image-region: rect(32px 64px 48px 48px);
+ }
+
+ #button-privacy {
+ -moz-image-region: rect(0px 120px 24px 96px);
+ }
+ #button-privacy[disabled="true"] {
+ -moz-image-region: rect(48px 120px 72px 96px);
+ }
+
+ toolbar[iconsize="small"] #button-privacy {
+ -moz-image-region: rect(0px 80px 16px 64px);
+ }
+ toolbar[iconsize="small"] #button-privacy[disabled="true"] {
+ -moz-image-region: rect(32px 80px 48px 64px);
+ }
+
+ #button-save {
+ -moz-image-region: rect(0px 144px 24px 120px);
+ }
+ #button-save[disabled="true"] {
+ -moz-image-region: rect(48px 144px 72px 120px);
+ }
+
+ toolbar[iconsize="small"] #button-save {
+ -moz-image-region: rect(0px 96px 16px 80px);
+ }
+ toolbar[iconsize="small"] #button-save[disabled="true"] {
+ -moz-image-region: rect(32px 96px 48px 80px);
+ }
+
+ #button-saveandclose {
+ -moz-image-region: rect(0px 696px 24px 672px);
+ }
+ #button-saveandclose[disabled="true"] {
+ -moz-image-region: rect(48px 696px 72px 672px);
+ }
+
+ toolbar[iconsize="small"] #button-saveandclose {
+ -moz-image-region: rect(0px 464px 16px 448px);
+ }
+ toolbar[iconsize="small"] #button-saveandclose[disabled="true"] {
+ -moz-image-region: rect(32px 464px 48px 448px);
+ }
+
+ #button-delete.cal-event-toolbarbutton {
+ list-style-image: url("chrome://calendar/skin/calendar-event-dialog-toolbar.png");
+ -moz-image-region: rect(0px 408px 24px 384px);
+ }
+ #button-delete.cal-event-toolbarbutton[disabled="true"],
+ #button-delete.cal-event-toolbarbutton[disabled="true"]:hover {
+ -moz-image-region: rect(48px 408px 72px 384px);
+ }
+ #button-delete.cal-event-toolbarbutton:hover,
+ #button-delete.cal-event-toolbarbutton:hover:active {
+ -moz-image-region: rect(24px 408px 48px 384px);
+ }
+
+ toolbar[iconsize="small"] #button-delete.cal-event-toolbarbutton {
+ list-style-image: url("chrome://calendar/skin/calendar-event-dialog-toolbar-small.png");
+ -moz-image-region: rect(0px 272px 16px 256px);
+ }
+ toolbar[iconsize="small"] #button-delete.cal-event-toolbarbutton[disabled="true"],
+ toolbar[iconsize="small"] #button-delete.cal-event-toolbarbutton[disabled="true"]:hover {
+ -moz-image-region: rect(32px 272px 48px 256px);
+ }
+ toolbar[iconsize="small"] #button-delete.cal-event-toolbarbutton:hover,
+ toolbar[iconsize="small"] #button-delete.cal-event-toolbarbutton:hover:active {
+ -moz-image-region: rect(16px 272px 32px 256px);
+ }
+
+ #button-priority {
+ -moz-image-region: rect(0px 600px 24px 576px);
+ }
+ #button-priority[disabled="true"] {
+ -moz-image-region: rect(48px 600px 72px 576px);
+ }
+
+ toolbar[iconsize="small"] #button-priority {
+ -moz-image-region: rect(0px 400px 16px 384px);
+ }
+ toolbar[iconsize="small"] #button-priority[disabled="true"] {
+ -moz-image-region: rect(32px 400px 48px 384px);
+ }
+
+ #button-status {
+ -moz-image-region: rect(0px 624px 24px 600px);
+ }
+ #button-status[disabled="true"] {
+ -moz-image-region: rect(48px 624px 72px 600px);
+ }
+
+ toolbar[iconsize="small"] #button-status {
+ -moz-image-region: rect(0px 416px 16px 400px);
+ }
+ toolbar[iconsize="small"] #button-status[disabled="true"] {
+ -moz-image-region: rect(32px 416px 48px 400px);
+ }
+
+ #button-freebusy {
+ -moz-image-region: rect(0px 648px 24px 624px);
+ }
+ #button-freebusy[disabled="true"] {
+ -moz-image-region: rect(48px 648px 72px 624px);
+ }
+
+ toolbar[iconsize="small"] #button-freebusy {
+ -moz-image-region: rect(0px 432px 16px 416px);
+ }
+ toolbar[iconsize="small"] #button-freebusy[disabled="true"] {
+ -moz-image-region: rect(32px 432px 48px 416px);
+ }
+
+ #button-timezones {
+ -moz-image-region: rect(0px 672px 24px 648px);
+ }
+ #button-timezones[disabled="true"] {
+ -moz-image-region: rect(48px 672px 72px 648px);
+ }
+
+ toolbar[iconsize="small"] #button-timezones {
+ -moz-image-region: rect(0px 448px 16px 432px);
+ }
+ toolbar[iconsize="small"] #button-timezones[disabled="true"] {
+ -moz-image-region: rect(32px 448px 48px 432px);
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #button-save {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save);
+ }
+
+ #button-saveandclose {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-close);
+ }
+
+ #button-attendees {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#address);
+ }
+
+ #button-privacy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#security);
+ }
+
+ #button-url {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#attach);
+ }
+
+ #button-delete.cal-event-toolbarbutton {
+ /* !important to override the SM #button-delete states */
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete) !important;
+ -moz-image-region: auto !important;
+ }
+
+ #button-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+ }
+
+ #button-status {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#status);
+ }
+
+ #button-freebusy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#freebusy);
+ }
+
+ #button-timezones {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#timezones);
+ }
+
+ toolbar[brighttext] #button-save {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-inverted);
+ }
+
+ toolbar[brighttext] #button-saveandclose {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-close-inverted);
+ }
+
+ toolbar[brighttext] #button-attendees {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#address-inverted);
+ }
+
+ toolbar[brighttext] #button-privacy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#security-inverted);
+ }
+
+ toolbar[brighttext] #button-url {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#attach-inverted);
+ }
+
+ toolbar[brighttext] #button-delete.cal-event-toolbarbutton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted) !important;
+ }
+
+ toolbar[brighttext] #button-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-inverted);
+ }
+
+ toolbar[brighttext] #button-status {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#status-inverted);
+ }
+
+ toolbar[brighttext] #button-freebusy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#freebusy-inverted);
+ }
+
+ toolbar[brighttext] #button-timezones {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#timezones-inverted);
+ }
+
+ #calendar-event-dialog > #event-toolbox > #event-toolbar {
+ padding-bottom: 2px;
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #button-save {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-flat);
+ }
+
+ #button-saveandclose {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#save-close-flat);
+ }
+
+ #button-attendees {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#address-flat);
+ }
+
+ #button-privacy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#security-flat);
+ }
+
+ #button-priority {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-flat);
+ }
+
+ #button-status {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#status-flat);
+ }
+
+ #button-freebusy {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#freebusy-flat);
+ }
+
+ #button-timezones {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#timezones-flat);
+ }
+}
diff --git a/calendar/base/themes/windows/dialogs/calendar-invitations-dialog.css b/calendar/base/themes/windows/dialogs/calendar-invitations-dialog.css
new file mode 100644
index 000000000..31cd81a37
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/calendar-invitations-dialog.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/dialogs/calendar-invitations-dialog.css);
diff --git a/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-aero.png b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-aero.png
new file mode 100644
index 000000000..4790c21be
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-inverted.png b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-inverted.png
new file mode 100644
index 000000000..3cda77303
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-inverted.png
Binary files differ
diff --git a/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-small.png b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-small.png
new file mode 100644
index 000000000..bc89f805b
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar-small.png
Binary files differ
diff --git a/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar.png b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar.png
new file mode 100644
index 000000000..884e3eabd
--- /dev/null
+++ b/calendar/base/themes/windows/dialogs/images/calendar-event-dialog-toolbar.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/cal-icon24.png b/calendar/base/themes/windows/images/cal-icon24.png
new file mode 100644
index 000000000..c17924e85
--- /dev/null
+++ b/calendar/base/themes/windows/images/cal-icon24.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/cal-icon32.png b/calendar/base/themes/windows/images/cal-icon32.png
new file mode 100644
index 000000000..6601ed9fa
--- /dev/null
+++ b/calendar/base/themes/windows/images/cal-icon32.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/calendar-occurrence-prompt-aero.png b/calendar/base/themes/windows/images/calendar-occurrence-prompt-aero.png
new file mode 100644
index 000000000..1daf447d4
--- /dev/null
+++ b/calendar/base/themes/windows/images/calendar-occurrence-prompt-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/calendar-occurrence-prompt.png b/calendar/base/themes/windows/images/calendar-occurrence-prompt.png
new file mode 100644
index 000000000..09ece5a13
--- /dev/null
+++ b/calendar/base/themes/windows/images/calendar-occurrence-prompt.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/tasks-actions-aero.png b/calendar/base/themes/windows/images/tasks-actions-aero.png
new file mode 100644
index 000000000..db61b7f54
--- /dev/null
+++ b/calendar/base/themes/windows/images/tasks-actions-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/tasks-actions-inverted.png b/calendar/base/themes/windows/images/tasks-actions-inverted.png
new file mode 100644
index 000000000..d13e1b1be
--- /dev/null
+++ b/calendar/base/themes/windows/images/tasks-actions-inverted.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/tasks-actions.png b/calendar/base/themes/windows/images/tasks-actions.png
new file mode 100644
index 000000000..42f2c97ba
--- /dev/null
+++ b/calendar/base/themes/windows/images/tasks-actions.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-aero-inverted.png b/calendar/base/themes/windows/images/toolbar-aero-inverted.png
new file mode 100644
index 000000000..ff1ce4be4
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-aero-inverted.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-aero.png b/calendar/base/themes/windows/images/toolbar-aero.png
new file mode 100644
index 000000000..a2a73e513
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-large-aero.png b/calendar/base/themes/windows/images/toolbar-large-aero.png
new file mode 100644
index 000000000..2b342b6ac
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-large-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-large.png b/calendar/base/themes/windows/images/toolbar-large.png
new file mode 100644
index 000000000..16eae13ee
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-large.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-small-aero.png b/calendar/base/themes/windows/images/toolbar-small-aero.png
new file mode 100644
index 000000000..8f1ad149d
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-small-aero.png
Binary files differ
diff --git a/calendar/base/themes/windows/images/toolbar-small.png b/calendar/base/themes/windows/images/toolbar-small.png
new file mode 100644
index 000000000..a0aea24a7
--- /dev/null
+++ b/calendar/base/themes/windows/images/toolbar-small.png
Binary files differ
diff --git a/calendar/base/themes/windows/preferences/Options.png b/calendar/base/themes/windows/preferences/Options.png
new file mode 100644
index 000000000..a62e74429
--- /dev/null
+++ b/calendar/base/themes/windows/preferences/Options.png
Binary files differ
diff --git a/calendar/base/themes/windows/preferences/preferences.css b/calendar/base/themes/windows/preferences/preferences.css
new file mode 100644
index 000000000..c42048849
--- /dev/null
+++ b/calendar/base/themes/windows/preferences/preferences.css
@@ -0,0 +1,94 @@
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*/
+
+/* Global Styles */
+#CalendarPreferences radio[pane] {
+ list-style-image: url("chrome://calendar/skin/preferences/Options.png");
+}
+
+radio[pane=paneGeneral] {
+ -moz-image-region: rect(0px, 32px, 32px, 0px)
+}
+radio[pane=paneGeneral]:hover, radio[pane=paneGeneral][selected="true"] {
+ -moz-image-region: rect(32px, 32px, 64px, 0px)
+}
+
+radio[pane=paneAlarms] {
+ -moz-image-region: rect(0px, 64px, 32px, 32px)
+}
+radio[pane=paneAlarms]:hover, radio[pane=paneAlarms][selected="true"] {
+ -moz-image-region: rect(32px, 64px, 64px, 32px)
+}
+
+radio[pane=paneCategories] {
+ -moz-image-region: rect(0px, 128px, 32px, 96px)
+}
+radio[pane=paneCategories]:hover, radio[pane=paneCategories][selected="true"] {
+ -moz-image-region: rect(32px, 128px, 64px, 96px)
+}
+
+radio[pane=paneViews] {
+ -moz-image-region: rect(0px, 192px, 32px, 160px)
+}
+radio[pane=paneViews]:hover, radio[pane=paneViews][selected="true"] {
+ -moz-image-region: rect(32px, 192px, 64px, 160px)
+}
+
+radio[pane=paneTimezones] {
+ -moz-image-region: rect(0px, 224px, 32px, 192px)
+}
+radio[pane=paneTimezones]:hover, radio[pane=paneTimezones][selected="true"] {
+ -moz-image-region: rect(32px, 224px, 64px, 192px)
+}
+
+radio[pane=paneAdvanced] {
+ -moz-image-region: rect(0px, 160px, 32px, 128px)
+}
+radio[pane=paneAdvanced]:hover, radio[pane=paneAdvanced][selected="true"] {
+ -moz-image-region: rect(32px, 160px, 64px, 128px)
+}
+
+/* File Field Widget */
+filefield {
+ margin: 2px 4px;
+ -moz-appearance: textfield;
+}
+
+.fileFieldContentBox {
+ background-color: -moz-Dialog;
+ color: -moz-DialogText;
+ margin: 1px;
+}
+
+filefield[disabled="true"] .fileFieldContentBox {
+ opacity: 0.5;
+}
+
+filefield[disabled="true"] .fileFieldIcon {
+ opacity: 0.2;
+}
+
+.fileFieldIcon {
+ width: 16px;
+ height: 16px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ margin-inline-start: 2px;
+ margin-inline-end: 4px;
+}
+
+.fileFieldLabel {
+ -moz-appearance: none;
+ background-color: transparent;
+ border: none;
+ padding: 1px 0px 0px;
+ margin: 0px;
+}
+
+tabpanels caption {
+ background-color: -moz-Dialog;
+}
diff --git a/calendar/base/themes/windows/today-pane.css b/calendar/base/themes/windows/today-pane.css
new file mode 100644
index 000000000..b124d913c
--- /dev/null
+++ b/calendar/base/themes/windows/today-pane.css
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/today-pane.css);
+
+@media (-moz-os-version: windows-xp),
+ (-moz-os-version: windows-vista),
+ (-moz-os-version: windows-win7) {
+ #today-pane-panel:-moz-lwtheme {
+ box-shadow: 0 1px 0 rgba(253, 253, 253, 0.45) inset;
+ }
+}
+
+#today-pane-panel:-moz-lwtheme > sidebarheader {
+ background-image: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 19px);
+}
+
+.today-pane-cycler {
+ list-style-image: url("chrome://calendar-common/skin/today-pane-cycler.svg#normal");
+}
+
+.today-pane-cycler:-moz-lwtheme-brighttext {
+ list-style-image: url("chrome://calendar-common/skin/today-pane-cycler.svg#inverted");
+}
+
+.today-pane-cycler[dir="prev"]:-moz-locale-dir(ltr) > .toolbarbutton-icon,
+.today-pane-cycler[dir="next"]:-moz-locale-dir(rtl) > .toolbarbutton-icon {
+ transform: scaleX(-1);
+}
+
+.today-subpane {
+ border-color: ThreeDShadow;
+}
+
+#mini-day-image {
+ background-image: linear-gradient(transparent, rgba(0, 0, 0, .1));
+}
+
+@media (-moz-os-version: windows-win8),
+ (-moz-os-version: windows-win10) {
+ #mini-day-image {
+ background-image: none;
+ }
+}
+
+@media (-moz-os-version: windows-win8) {
+ #mini-day-box {
+ padding-top: 1px;
+ padding-bottom: 1px;
+ }
+}
+
+.miniday-nav-buttons {
+ list-style-image: url("chrome://calendar-common/skin/widgets/nav-arrow.svg");
+}
+
+#previous-day-button:-moz-locale-dir(ltr),
+#next-day-button:-moz-locale-dir(rtl) {
+ transform: scaleX(-1);
+}
+
+#miniday-dropdown-button {
+ max-width: 18px;
+}
+
+@media (-moz-os-version: windows-xp) {
+ #today-pane-panel {
+ border-left: 1px solid ThreeDShadow;
+ }
+
+ .today-pane-cycler {
+ padding-inline-end: 0;
+ }
+
+ #todaypane-new-event-button {
+ list-style-image: url("chrome://calendar/skin/toolbar-small.png");
+ -moz-image-region: rect(0px 16px 16px 0px);
+ }
+ #todaypane-new-event-button[disabled="true"] {
+ -moz-image-region: rect(32px 16px 48px 0px);
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #today-none-box {
+ border-top: 1px solid ThreeDShadow;
+ }
+
+ .today-pane-cycler {
+ padding-inline-start: 5px;
+ padding-inline-end: 5px;
+ }
+
+ #todaypane-new-event-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+ }
+
+ #todaypane-new-event-button[disabled="true"] > .toolbarbutton-icon {
+ opacity: 0.4;
+ }
+
+ #todaypane-new-event-button > .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ margin: -1px;
+ }
+
+ @media (-moz-windows-default-theme) {
+ #today-pane-panel > sidebarheader {
+ -moz-appearance: none;
+ background-color: #f8f8f8;
+ border-bottom: none;
+ }
+
+ #today-pane-panel:-moz-lwtheme > sidebarheader {
+ background-color: rgba(255, 255, 255, 0.3);
+ background-image: linear-gradient(rgba(255, 255, 255, 0.5),
+ rgba(255, 255, 255, 0) 28px);
+ border-top: 1px solid rgba(253, 253, 253, 0.45);
+ }
+
+ sidebarheader > spacer {
+ min-height: 25px;
+ }
+ }
+}
+
+@media all and (-moz-windows-compositor) {
+ @media not all and (-moz-os-version: windows-win10) {
+ #messengerWindow[sizemode=normal] #today-pane-panel {
+ border-inline-end: 1px solid rgba(10%, 10%, 10%, .4);
+ border-bottom: 1px solid rgba(10%, 10%, 10%, .4);
+ background-clip: padding-box;
+ }
+ }
+
+ .today-pane-cycler {
+ margin-top: -1px;
+ }
+}
+
+@media (-moz-os-version: windows-win8),
+ (-moz-os-version: windows-win10) {
+ .today-closebutton {
+ padding-top: 0;
+ padding-bottom: 0;
+ -moz-image-region: rect(0 20px 20px 0);
+ }
+
+ .today-closebutton:hover {
+ -moz-image-region: rect(0 40px 20px 20px);
+ }
+
+ .today-closebutton:hover:active {
+ -moz-image-region: rect(0 60px 20px 40px);
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #todaypane-new-event-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-flat);
+ }
+}
diff --git a/calendar/base/themes/windows/widgets/calendar-widgets.css b/calendar/base/themes/windows/widgets/calendar-widgets.css
new file mode 100644
index 000000000..80564bf1f
--- /dev/null
+++ b/calendar/base/themes/windows/widgets/calendar-widgets.css
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://calendar-common/skin/widgets/calendar-widgets.css);
+
+treenode-checkbox > .checkbox-check {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ border: none;
+ width: 9px; /* The image's width is 9 pixels */
+ height: 9px;
+ background-image: url(chrome://global/skin/tree/twisty.svg#clsd);
+}
+
+treenode-checkbox[checked="true"] > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#open);
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ treenode-checkbox:hover > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#clsd-hover);
+ }
+
+ treenode-checkbox[checked="true"]:hover > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#open-hover);
+ }
+
+ treenode-checkbox:-moz-locale-dir(rtl) > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#clsd-rtl);
+ }
+
+ treenode-checkbox[checked="true"]:-moz-locale-dir(rtl) > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#open-rtl);
+ }
+
+ treenode-checkbox:-moz-locale-dir(rtl):hover > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#clsd-hover-rtl);
+ }
+
+ treenode-checkbox[checked="true"]:-moz-locale-dir(rtl):hover > .checkbox-check {
+ background-image: url(chrome://global/skin/tree/twisty.svg#open-hover-rtl);
+ }
+}
+
+#task-tree-filtergroup {
+ padding-inline-start: 12px;
+}
+
+calendar-list-tree .tree-scrollable-columns {
+ padding-inline-start: 18px;
+}
+
+.toolbarbutton-icon-begin {
+ margin-inline-end: 5px;
+}
+
+.toolbarbutton-icon-end {
+ margin-inline-start: 5px;
+}