summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-views-utils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/calendar-views-utils.js617
1 files changed, 617 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-views-utils.js b/comm/calendar/base/content/calendar-views-utils.js
new file mode 100644
index 0000000000..b88f0e5954
--- /dev/null
+++ b/comm/calendar/base/content/calendar-views-utils.js
@@ -0,0 +1,617 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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, minimonthPick,
+ * observeViewDaySelect, toggleOrientation,
+ * toggleWorkdaysOnly, toggleTasksInView, toggleShowCompletedInView,
+ * goToDate, gLastShownCalendarView, deleteSelectedEvents,
+ * editSelectedEvents, selectAllEvents, calendarNavigationBar
+ */
+
+/* import-globals-from item-editing/calendar-item-editing.js */
+/* import-globals-from calendar-modes.js */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { countOccurrences } = ChromeUtils.import(
+ "resource:///modules/calendar/calRecurrenceUtils.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalEvent: "resource:///modules/CalEvent.jsm",
+});
+
+/**
+ * Controller for the views
+ *
+ * @see calIcalendarViewController
+ */
+var calendarViewController = {
+ QueryInterface: ChromeUtils.generateQI(["calICalendarViewController"]),
+
+ /**
+ * Creates a new event
+ *
+ * @see calICalendarViewController
+ */
+ createNewEvent(calendar, startTime, endTime, forceAllday) {
+ // if we're given both times, skip the dialog
+ if (startTime && endTime && !startTime.isDate && !endTime.isDate) {
+ let item = new CalEvent();
+ setDefaultItemValues(item, calendar, startTime, endTime);
+ doTransaction("add", item, item.calendar, null, null);
+ } else {
+ createEventWithDialog(calendar, startTime, null, null, null, forceAllday);
+ }
+ },
+
+ /**
+ * View the given occurrence.
+ *
+ * @param {calIItemBase} occurrence
+ * @see calICalendarViewController
+ */
+ viewOccurrence(occurrence) {
+ openEventDialogForViewing(occurrence);
+ },
+
+ /**
+ * Modifies the given occurrence
+ *
+ * @see calICalendarViewController
+ */
+ modifyOccurrence(occurrence, newStartTime, newEndTime, newTitle) {
+ // if modifying this item directly (e.g. just dragged to new time),
+ // then do so; otherwise pop up the dialog
+ if (newStartTime || newEndTime || newTitle) {
+ let instance = occurrence.clone();
+
+ if (newTitle) {
+ instance.title = newTitle;
+ }
+
+ // 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 (newStartTime || newEndTime) {
+ // Yay for variable names that make this next line look silly
+ if (instance.isEvent()) {
+ if (newStartTime && instance.startDate) {
+ instance.startDate = newStartTime;
+ }
+ if (newEndTime && instance.endDate) {
+ instance.endDate = newEndTime;
+ }
+ } else {
+ if (newStartTime && instance.entryDate) {
+ instance.entryDate = newStartTime;
+ }
+ if (newEndTime && instance.dueDate) {
+ instance.dueDate = newEndTime;
+ }
+ }
+ }
+
+ doTransaction("modify", instance, instance.calendar, occurrence, null);
+ } else {
+ modifyEventWithDialog(occurrence, true);
+ }
+ },
+
+ /**
+ * Deletes the given occurrences
+ *
+ * @see calICalendarViewController
+ */
+ deleteOccurrences(occurrencesArg, useParentItems, doNotConfirm, extResponseArg = null) {
+ if (!cal.window.promptDeleteItems(occurrencesArg)) {
+ return;
+ }
+ startBatchTransaction();
+ let recurringItems = {};
+ let extResponse = extResponseArg || { responseMode: Ci.calIItipItem.USER };
+
+ let getSavedItem = function (itemToDelete) {
+ // Get the parent item, saving it in our recurringItems object for
+ // later use.
+ let hashVal = itemToDelete.parentItem.hashId;
+ if (!recurringItems[hashVal]) {
+ recurringItems[hashVal] = {
+ oldItem: itemToDelete.parentItem,
+ newItem: itemToDelete.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 doRemoveItem 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 = occurrencesArg.filter(item => cal.acl.isCalendarWritable(item.calendar));
+
+ // we check how many occurrences the parent item has
+ let parents = new Map();
+ for (let occ of occurrences) {
+ if (!parents.has(occ.id)) {
+ parents.set(occ.id, countOccurrences(occ));
+ }
+ }
+
+ let promptUser = !doNotConfirm;
+ let previousResponse = 0;
+ for (let itemToDelete of occurrences) {
+ if (parents.get(itemToDelete.id) == -1) {
+ // we have scheduled the master item for deletion in a previous
+ // loop already
+ continue;
+ }
+ if (useParentItems || parents.get(itemToDelete.id) == 1 || previousResponse == 3) {
+ // 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.
+ // if an occurrence is the only one of a series or the user
+ // decided so before, we delete the series, too.
+ itemToDelete = itemToDelete.parentItem;
+ parents.set(itemToDelete.id, -1);
+ } else if (promptUser) {
+ let [targetItem, , response] = promptOccurrenceModification(itemToDelete, false, "delete");
+ if (!response) {
+ // The user canceled the dialog, bail out
+ break;
+ }
+ itemToDelete = targetItem;
+
+ // if we have multiple items and the user decided already for one
+ // item whether to delete the occurrence or the entire series,
+ // we apply that decision also to subsequent items
+ previousResponse = response;
+ promptUser = false;
+ }
+
+ // 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, extResponse);
+ } 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,
+ extResponse
+ );
+ }
+ endBatchTransaction();
+ },
+};
+
+/**
+ * This function does the common steps to switch between views. Should be called
+ * from app-specific view switching functions
+ *
+ * @param viewType The type of view to select.
+ */
+function switchToView(viewType) {
+ let viewBox = getViewBox();
+ let selectedDay;
+ let currentSelection = [];
+
+ // Set up the view commands
+ let views = viewBox.children;
+ 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 == viewType + "-view") {
+ command.setAttribute("checked", "true");
+ } else {
+ command.removeAttribute("checked");
+ }
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("previousViewButton"),
+ `calendar-nav-button-prev-tooltip-${viewType}`
+ );
+ document.l10n.setAttributes(
+ document.getElementById("nextViewButton"),
+ `calendar-nav-button-next-tooltip-${viewType}`
+ );
+ document.l10n.setAttributes(
+ document.getElementById("calendar-view-context-menu-previous"),
+ `calendar-context-menu-previous-${viewType}`
+ );
+ document.l10n.setAttributes(
+ document.getElementById("calendar-view-context-menu-next"),
+ `calendar-context-menu-next-${viewType}`
+ );
+
+ // These are hidden until the calendar is loaded.
+ for (let node of document.querySelectorAll(".hide-before-calendar-loaded")) {
+ node.removeAttribute("hidden");
+ }
+
+ // Anyone wanting to plug in a view needs to follow this naming scheme
+ let view = document.getElementById(viewType + "-view");
+ let oldView = currentView();
+ if (oldView?.isActive) {
+ if (oldView == view) {
+ // Not actually changing view, there's nothing else to do.
+ return;
+ }
+
+ selectedDay = oldView.selectedDay;
+ currentSelection = oldView.getSelectedItems();
+ oldView.deactivate();
+ }
+
+ if (!selectedDay) {
+ selectedDay = cal.dtz.now();
+ }
+ for (let i = 0; i < viewBox.children.length; i++) {
+ if (view.id == viewBox.children[i].id) {
+ viewBox.children[i].hidden = false;
+ viewBox.setAttribute("selectedIndex", i);
+ } else {
+ viewBox.children[i].hidden = true;
+ }
+ }
+
+ view.ensureInitialized();
+ if (!view.controller) {
+ view.timezone = cal.dtz.defaultTimezone;
+ view.controller = calendarViewController;
+ }
+
+ view.goToDay(selectedDay);
+ view.setSelectedItems(currentSelection);
+
+ view.onResize(view);
+ view.activate();
+}
+
+/**
+ * Returns the calendar view box element.
+ *
+ * @returns The view-box element.
+ */
+function getViewBox() {
+ return document.getElementById("view-box");
+}
+
+/**
+ * Returns the currently selected calendar view.
+ *
+ * @returns The selected calendar view
+ */
+function currentView() {
+ for (let element of getViewBox().children) {
+ if (!element.hidden) {
+ return element;
+ }
+ }
+ return null;
+}
+
+/**
+ * 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.target.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();
+}
+
+/**
+ * Shows the given date in the current view, if in calendar mode.
+ *
+ * @param aNewDate The new date as a JSDate.
+ */
+function minimonthPick(aNewDate) {
+ if (gCurrentMode == "calendar" || gCurrentMode == "task") {
+ let cdt = cal.dtz.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();
+ }
+}
+
+/**
+ * Provides a neutral way to get the minimonth.
+ *
+ * @returns 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);
+
+ for (let view of getViewBox().children) {
+ 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);
+
+ for (let view of getViewBox().children) {
+ 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);
+
+ for (let view of getViewBox().children) {
+ 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);
+
+ for (let view of getViewBox().children) {
+ view.showCompleted = newValue == "true";
+ }
+
+ // Refresh the current view
+ currentView().goToDay();
+}
+
+/**
+ * Open the calendar layout options menu popup.
+ *
+ * @param {Event} event - The click DOMEvent.
+ */
+function showCalControlBarMenuPopup(event) {
+ let moreContext = document.getElementById("calControlBarMenuPopup");
+ moreContext.openPopup(event.target, { triggerEvent: event });
+}
+
+/**
+ * Provides a neutral way to go to the current day in the views and minimonth.
+ *
+ * @param date The date to go.
+ */
+function goToDate(date) {
+ getMinimonth().value = cal.dtz.dateTimeToJsDate(date);
+ currentView().goToDay(date);
+}
+
+var gLastShownCalendarView = {
+ _lastView: null,
+
+ /**
+ * Returns the calendar view that was selected before restart, or the current
+ * calendar view if it has already been set in this session.
+ *
+ * @returns {string} The last calendar view.
+ */
+ get() {
+ if (!this._lastView) {
+ if (Services.xulStore.hasValue(document.location.href, "view-box", "selectedIndex")) {
+ let viewBox = getViewBox();
+ let selectedIndex = Services.xulStore.getValue(
+ document.location.href,
+ "view-box",
+ "selectedIndex"
+ );
+ for (let i = 0; i < viewBox.children.length; i++) {
+ viewBox.children[i].hidden = selectedIndex != i;
+ }
+ let viewNode = viewBox.children[selectedIndex];
+ this._lastView = viewNode.id.replace(/-view/, "");
+ document
+ .querySelector(`.calview-toggle-item[aria-controls="${viewNode.id}"]`)
+ ?.setAttribute("aria-selected", true);
+ } else {
+ // No deck item was selected beforehand, default to week view.
+ this._lastView = "week";
+ document
+ .querySelector(`.calview-toggle-item[aria-controls="week-view"]`)
+ ?.setAttribute("aria-selected", true);
+ }
+ }
+ return this._lastView;
+ },
+
+ set(view) {
+ this._lastView = view;
+ },
+};
+
+/**
+ * Deletes items currently selected in the view and clears selection.
+ */
+function deleteSelectedEvents() {
+ let selectedItems = currentView().getSelectedItems();
+ calendarViewController.deleteOccurrences(selectedItems, false, false);
+ // clear selection
+ currentView().setSelectedItems([], true);
+}
+
+/**
+ * Open the items currently selected in the view.
+ */
+function viewSelectedEvents() {
+ let items = currentView().getSelectedItems();
+ if (items.length >= 1) {
+ openEventDialogForViewing(items[0]);
+ }
+}
+
+/**
+ * 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], true);
+ }
+}
+
+/**
+ * Select all events from all calendars. Use with care.
+ */
+async function selectAllEvents() {
+ let composite = cal.view.getCompositeCalendar(window);
+ 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;
+
+ let items = await composite.getItemsAsArray(filter, 0, currentView().startDay, end);
+ currentView().setSelectedItems(items, false);
+}
+
+var calendarNavigationBar = {
+ setDateRange(startDate, endDate) {
+ let docTitle = "";
+ if (startDate) {
+ let intervalLabel = document.getElementById("intervalDescription");
+ let firstWeekNo = cal.weekInfoService.getWeekTitle(startDate);
+ let secondWeekNo = firstWeekNo;
+ let weekLabel = document.getElementById("calendarWeek");
+ if (startDate.nativeTime == endDate.nativeTime) {
+ intervalLabel.textContent = cal.dtz.formatter.formatDate(startDate);
+ } else {
+ intervalLabel.textContent = currentView().getRangeDescription();
+ secondWeekNo = cal.weekInfoService.getWeekTitle(endDate);
+ }
+ if (secondWeekNo == firstWeekNo) {
+ weekLabel.textContent = cal.l10n.getCalString("singleShortCalendarWeek", [firstWeekNo]);
+ weekLabel.tooltipText = cal.l10n.getCalString("singleLongCalendarWeek", [firstWeekNo]);
+ } else {
+ weekLabel.textContent = cal.l10n.getCalString("severalShortCalendarWeeks", [
+ firstWeekNo,
+ secondWeekNo,
+ ]);
+ weekLabel.tooltipText = cal.l10n.getCalString("severalLongCalendarWeeks", [
+ firstWeekNo,
+ secondWeekNo,
+ ]);
+ }
+ docTitle = intervalLabel.textContent;
+ }
+
+ if (gCurrentMode == "calendar") {
+ document.title =
+ (docTitle ? docTitle + " - " : "") +
+ cal.l10n.getAnyString("branding", "brand", "brandFullName");
+ }
+ },
+};
+
+var timezoneObserver = {
+ observe() {
+ let minimonth = getMinimonth();
+ minimonth.update(minimonth.value);
+ },
+};
+Services.obs.addObserver(timezoneObserver, "defaultTimezoneChanged");
+window.addEventListener("unload", () => {
+ Services.obs.removeObserver(timezoneObserver, "defaultTimezoneChanged");
+});