summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-base-view.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/calendar-base-view.js647
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;
+}