diff options
Diffstat (limited to 'comm/calendar/base/content/calendar-base-view.js')
-rw-r--r-- | comm/calendar/base/content/calendar-base-view.js | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-base-view.js b/comm/calendar/base/content/calendar-base-view.js new file mode 100644 index 0000000000..317770b984 --- /dev/null +++ b/comm/calendar/base/content/calendar-base-view.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/. */ + +/* global cal, calendarNavigationBar, CalendarFilteredViewMixin, calFilterProperties, currentView, + gCurrentMode, MozElements, MozXULElement, Services, toggleOrientation */ + +"use strict"; + +// Wrap in a block to prevent leaking to window scope. +{ + const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); + + /** + * Calendar observer for calendar view elements. Used in CalendarBaseView class. + * + * @implements {calIObserver} + * @implements {calICompositeObserver} + * @implements {calIAlarmServiceObserver} + */ + class CalendarViewObserver { + /** + * Constructor for CalendarViewObserver. + * + * @param {CalendarBaseView} calendarView - A calendar view. + */ + constructor(calendarView) { + this.calView = calendarView.calICalendarView; + } + + QueryInterface = ChromeUtils.generateQI(["calIAlarmServiceObserver"]); + + // calIAlarmServiceObserver + + onAlarm(alarmItem) { + this.calView.flashAlarm(alarmItem, false); + } + + onNotification(item) {} + + onRemoveAlarmsByItem(item) { + // Stop the flashing for the item. + this.calView.flashAlarm(item, true); + } + + onRemoveAlarmsByCalendar(calendar) { + // Stop the flashing for all items of this calendar. + for (const key in this.calView.mFlashingEvents) { + const item = this.calView.mFlashingEvents[key]; + if (item.calendar.id == calendar.id) { + this.calView.flashAlarm(item, true); + } + } + } + + onAlarmsLoaded(calendar) {} + + // End calIAlarmServiceObserver + } + + /** + * Abstract base class for calendar view elements (day, week, multiweek, month). + * + * @implements {calICalendarView} + * @abstract + */ + class CalendarBaseView extends CalendarFilteredViewMixin(MozXULElement) { + /** + * Whether the view has been initialized. + * + * @type {boolean} + */ + #isInitialized = false; + + connectedCallback() { + if (this.delayConnectedCallback() || this.hasConnected) { + return; + } + this.hasConnected = true; + + // For some unknown reason, `console.createInstance` isn't available when + // `ensureInitialized` runs. + this.mLog = console.createInstance({ + prefix: `calendar.baseview (${this.constructor.name})`, + maxLogLevel: "Warn", + maxLogLevelPref: "calendar.baseview.loglevel", + }); + + this.mSelectedItems = []; + } + + ensureInitialized() { + if (this.#isInitialized) { + return; + } + this.#isInitialized = true; + + this.calICalendarView = this.getCustomInterfaceCallback(Ci.calICalendarView); + + this.addEventListener("move", event => { + this.moveView(event.detail); + }); + + this.addEventListener("keypress", event => { + switch (event.key) { + case "PageUp": + this.moveView(-1); + break; + case "PageDown": + this.moveView(1); + break; + } + }); + + this.addEventListener("wheel", event => { + const pixelThreshold = 150; + + if (event.shiftKey && Services.prefs.getBoolPref("calendar.view.mousescroll", true)) { + 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); + } + event.preventDefault(); + } + }); + + this.addEventListener("MozRotateGesture", event => { + // 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; + + const absval = Math.abs(event.delta); + if (this.supportsRotation && absval >= MIN_ROTATE_ANGLE && absval < MAX_ROTATE_ANGLE) { + toggleOrientation(); + event.preventDefault(); + } + }); + + this.addEventListener("MozMagnifyGestureStart", event => { + this.mMagnifyAmount = 0; + }); + + this.addEventListener("MozMagnifyGestureUpdate", event => { + // 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(); + } + }); + + this.addEventListener("MozSwipeGesture", event => { + 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); + } + }); + + this.mRangeStartDate = null; + this.mRangeEndDate = null; + + this.mWorkdaysOnly = false; + + this.mController = null; + + this.mStartDate = null; + this.mEndDate = null; + + this.mTasksInView = false; + this.mShowCompleted = false; + + this.mDisplayDaysOff = true; + this.mDaysOffArray = [0, 6]; + + this.mTimezone = null; + this.mFlashingEvents = {}; + + this.mDropShadowsLength = null; + + this.mShadowOffset = null; + this.mDropShadows = null; + + this.mMagnifyAmount = 0; + this.mPixelScrollDelta = 0; + + this.mViewStart = null; + this.mViewEnd = null; + + this.mToggleStatus = 0; + + this.mToggleStatusFlag = { + WorkdaysOnly: 1, + TasksInView: 2, + ShowCompleted: 4, + }; + + this.mTimezoneObserver = { + observe: () => { + this.timezone = cal.dtz.defaultTimezone; + this.refreshView(); + + this.updateTimeIndicatorPosition(); + }, + }; + + this.mPrefObserver = { + calView: this.calICalendarView, + + observe(subj, topic, pref) { + this.calView.handlePreference(subj, topic, pref); + }, + }; + + this.mObserver = new CalendarViewObserver(this); + + const isChecked = id => document.getElementById(id).getAttribute("checked") == "true"; + + this.workdaysOnly = isChecked("calendar_toggle_workdays_only_command"); + this.tasksInView = isChecked("calendar_toggle_tasks_in_view_command"); + this.rotated = isChecked("calendar_toggle_orientation_command"); + this.showCompleted = isChecked("calendar_toggle_show_completed_in_view_command"); + + this.mTimezone = cal.dtz.defaultTimezone; + const alarmService = Cc["@mozilla.org/calendar/alarm-service;1"].getService( + Ci.calIAlarmService + ); + + alarmService.addObserver(this.mObserver); + + this.setAttribute("type", this.type); + + window.addEventListener("viewresize", event => { + if (gCurrentMode == "calendar" && this.isVisible()) { + this.onResize(); + } + }); + + // Add a preference observer to monitor changes. + Services.prefs.addObserver("calendar.", this.mPrefObserver); + Services.obs.addObserver(this.mTimezoneObserver, "defaultTimezoneChanged"); + + this.updateDaysOffPrefs(); + this.updateTimeIndicatorPosition(); + + // Remove observers on window unload. + window.addEventListener( + "unload", + () => { + alarmService.removeObserver(this.mObserver); + + Services.prefs.removeObserver("calendar.", this.mPrefObserver); + Services.obs.removeObserver(this.mTimezoneObserver, "defaultTimezoneChanged"); + }, + { once: true } + ); + } + + /** + * Handle resizing by adjusting the view to the new size. + * + * @param {calICalendarView} [calViewElem] - A calendar view element. + */ + onResize() { + // Child classes should provide the implementation. + throw new Error(this.constructor.name + ".onResize not implemented"); + } + + /** + * Whether the view has been initialized. + * + * @returns {boolean} - True if the view has been initialized, otherwise + * false. + */ + get isInitialized() { + return this.#isInitialized; + } + + get type() { + const typelist = this.id.split("-"); + return typelist[0]; + } + + set rotated(rotated) { + this.setAttribute("orient", rotated ? "horizontal" : "vertical"); + this.toggleAttribute("rotated", rotated); + } + + get rotated() { + return this.getAttribute("orient") == "horizontal"; + } + + get supportsRotation() { + return false; + } + + set displayDaysOff(displayDaysOff) { + this.mDisplayDaysOff = displayDaysOff; + } + + get displayDaysOff() { + return this.mDisplayDaysOff; + } + + set controller(controller) { + this.mController = controller; + } + + get controller() { + return this.mController; + } + + set daysOffArray(daysOffArray) { + this.mDaysOffArray = daysOffArray; + } + + get daysOffArray() { + return this.mDaysOffArray; + } + + set tasksInView(tasksInView) { + this.mTasksInView = tasksInView; + this.updateItemType(); + } + + get tasksInView() { + return this.mTasksInView; + } + + set showCompleted(showCompleted) { + this.mShowCompleted = showCompleted; + this.updateItemType(); + } + + get showCompleted() { + return this.mShowCompleted; + } + + set timezone(timezone) { + this.mTimezone = timezone; + } + + get timezone() { + return this.mTimezone; + } + + set workdaysOnly(workdaysOnly) { + this.mWorkdaysOnly = workdaysOnly; + } + + get workdaysOnly() { + return this.mWorkdaysOnly; + } + + get supportsWorkdaysOnly() { + return true; + } + + get supportsZoom() { + return false; + } + + get selectionObserver() { + return this.mSelectionObserver; + } + + get startDay() { + return this.startDate; + } + + get endDay() { + return this.endDate; + } + + get supportDisjointDates() { + return false; + } + + get hasDisjointDates() { + return false; + } + + set rangeStartDate(startDate) { + this.mRangeStartDate = startDate; + } + + get rangeStartDate() { + return this.mRangeStartDate; + } + + set rangeEndDate(endDate) { + this.mRangeEndDate = endDate; + } + + get rangeEndDate() { + return this.mRangeEndDate; + } + + get observerID() { + return "base-view-observer"; + } + + // The end date that should be used for getItems and similar queries. + get queryEndDate() { + if (!this.endDate) { + return null; + } + const end = this.endDate.clone(); + end.day += 1; + end.isDate = true; + return end; + } + + /** + * Return a date object representing the current day. + * + * @returns {calIDateTime} A date object. + */ + today() { + const date = cal.dtz.jsDateToDateTime(new Date()).getInTimezone(this.mTimezone); + date.isDate = true; + return date; + } + + /** + * Return whether this view is currently active and visible in the UI. + * + * @returns {boolean} + */ + isVisible() { + return this == currentView(); + } + + /** + * Set the view's item type based on the `tasksInView` and `showCompleted` properties. + */ + updateItemType() { + if (!this.mTasksInView) { + this.itemType = Ci.calICalendar.ITEM_FILTER_TYPE_EVENT; + return; + } + + let type = Ci.calICalendar.ITEM_FILTER_TYPE_ALL; + type |= this.mShowCompleted + ? Ci.calICalendar.ITEM_FILTER_COMPLETED_ALL + : Ci.calICalendar.ITEM_FILTER_COMPLETED_NO; + this.itemType = type; + } + + // CalendarFilteredViewMixin implementation (clearItems and removeItemsFromCalendar + // are implemented in subclasses). + + addItems(items) { + for (let item of items) { + this.doAddItem(item); + } + } + + removeItems(items) { + for (let item of items) { + this.doRemoveItem(item); + } + } + + // End of CalendarFilteredViewMixin implementation. + + /** + * Create and fire an event. + * + * @param {string} eventName - Name of the event. + * @param {object} eventDetail - The details to add to the event. + */ + fireEvent(eventName, eventDetail) { + this.dispatchEvent( + new CustomEvent(eventName, { bubbles: true, cancelable: false, detail: eventDetail }) + ); + } + + /** + * A preference handler typically called by a preferences observer when a preference + * changes. Handles common preferences while other preferences are handled in subclasses. + * + * @param {object} subject - A subject, a prefs object. + * @param {string} topic - A topic. + * @param {string} preference - A preference that has changed. + */ + handleCommonPreference(subject, topic, preference) { + switch (preference) { + 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.alarms.indicator.show": + case "calendar.date.format": + case "calendar.view.showLocation": + // Break here to ensure the view is refreshed. + break; + default: + return; + } + this.refreshView(); + } + + /** + * Check preferences and update which days are days off. + */ + updateDaysOffPrefs() { + const prefix = "calendar.week."; + const daysOffPrefs = [ + [0, "d0sundaysoff", "true"], + [1, "d1mondaysoff", "false"], + [2, "d2tuesdaysoff", "false"], + [3, "d3wednesdaysoff", "false"], + [4, "d4thursdaysoff", "false"], + [5, "d5fridaysoff", "false"], + [6, "d6saturdaysoff", "true"], + ]; + const filterDaysOff = ([number, name, defaultValue]) => + Services.prefs.getBoolPref(prefix + name, defaultValue); + + this.daysOffArray = daysOffPrefs.filter(filterDaysOff).map(pref => pref[0]); + } + + /** + * Adjust the position of this view's indicator of the current time, if any. + */ + updateTimeIndicatorPosition() {} + + /** + * Refresh the view. + */ + refreshView() { + if (!this.startDay || !this.endDay) { + // Don't refresh if we're not initialized. + return; + } + this.goToDay(this.selectedDay); + } + + handlePreference(subject, topic, pref) { + // Do nothing by default. + } + + flashAlarm(alarmItem, stop) { + // Do nothing by default. + } + + // calICalendarView Methods + + /** + * @note This is overridden in each of the built-in calendar views. + * It's only left here in case some extension is relying on it. + */ + goToDay(date) { + this.showDate(date); + } + + getRangeDescription() { + return cal.dtz.formatter.formatInterval(this.rangeStartDate, this.rangeEndDate); + } + + removeDropShadows() { + this.querySelectorAll("[dropbox='true']").forEach(dbox => { + dbox.setAttribute("dropbox", "false"); + }); + } + + setDateRange(startDate, endDate) { + calendarNavigationBar.setDateRange(startDate, endDate); + } + + getSelectedItems() { + return this.mSelectedItems; + } + + setSelectedItems(items) { + this.mSelectedItems = items.concat([]); + return this.mSelectedItems; + } + + getDateList() { + const start = this.startDate.clone(); + const dateList = []; + while (start.compare(this.endDate) <= 0) { + dateList.push(start); + start.day++; + } + return dateList; + } + + zoomIn(level) {} + + zoomOut(level) {} + + zoomReset() {} + + // End calICalendarView Methods + } + + XPCOMUtils.defineLazyPreferenceGetter( + CalendarBaseView.prototype, + "weekStartOffset", + "calendar.week.start", + 0 + ); + + MozXULElement.implementCustomInterface(CalendarBaseView, [Ci.calICalendarView]); + + MozElements.CalendarBaseView = CalendarBaseView; +} |