summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-task-tree-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/calendar-task-tree-view.js')
-rw-r--r--comm/calendar/base/content/calendar-task-tree-view.js495
1 files changed, 495 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-task-tree-view.js b/comm/calendar/base/content/calendar-task-tree-view.js
new file mode 100644
index 0000000000..ebe258419f
--- /dev/null
+++ b/comm/calendar/base/content/calendar-task-tree-view.js
@@ -0,0 +1,495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 CalendarTaskTreeView */
+
+/* import-globals-from item-editing/calendar-item-editing.js */
+/* import-globals-from widgets/mouseoverPreviews.js */
+
+/* globals cal */
+
+/**
+ * The tree view for a CalendarTaskTree.
+ */
+class CalendarTaskTreeView {
+ /**
+ * Creates a new task tree view and connects it to a given task tree.
+ *
+ * @param {CalendarTaskTree} taskTree - The task tree to connect the view to.
+ */
+ constructor(taskTree) {
+ this.tree = taskTree;
+ this.mSelectedColumn = null;
+ this.sortDirection = null;
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsITreeView"]);
+
+ /**
+ * Get the selected column.
+ *
+ * @returns {Element} A treecol element.
+ */
+ get selectedColumn() {
+ return this.mSelectedColumn;
+ }
+
+ /**
+ * Set the selected column and sort by that column.
+ *
+ * @param {Element} column - A treecol element.
+ */
+ set selectedColumn(column) {
+ const columnProperty = column.getAttribute("itemproperty");
+
+ this.tree.querySelectorAll("treecol").forEach(col => {
+ if (col.getAttribute("sortActive")) {
+ col.removeAttribute("sortActive");
+ col.removeAttribute("sortDirection");
+ }
+ if (columnProperty == col.getAttribute("itemproperty")) {
+ col.setAttribute("sortActive", "true");
+ col.setAttribute("sortDirection", this.sortDirection);
+ }
+ });
+ this.mSelectedColumn = column;
+ }
+
+ // High-level task tree manipulation
+
+ /**
+ * Adds an array of items (tasks) to the list if they match the currently applied filter.
+ *
+ * @param {object[]} items - An array of task objects to add.
+ * @param {boolean} [doNotSort] - Whether to re-sort after adding the tasks.
+ */
+ addItems(items, doNotSort) {
+ this.modifyItems(items, [], doNotSort, true);
+ }
+ /**
+ * Removes an array of items (tasks) from the list.
+ *
+ * @param {object[]} items - An array of task objects to remove.
+ */
+ removeItems(items) {
+ this.modifyItems([], items, 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.
+ *
+ * @param {object[]} newItems - An array of new items to add.
+ * @param {object[]} oldItems - An array of old items to remove.
+ * @param {boolean} [doNotSort] - Whether to re-sort the list after modifying it.
+ * @param {boolean} [selectNew] - Whether to select the new tasks.
+ */
+ modifyItems(newItems = [], oldItems = [], doNotSort, selectNew) {
+ let selItem = this.tree.currentTask;
+ let selIndex = this.tree.currentIndex;
+ let firstHash = null;
+ let remIndexes = [];
+
+ this.tree.beginUpdateBatch();
+
+ let idiff = new cal.item.ItemDiff();
+ idiff.load(oldItems);
+ idiff.difference(newItems);
+ 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.tree.mHash2Index) {
+ // The old item needs to be removed.
+ remIndexes.push(this.tree.mHash2Index[item.hashId]);
+ delete this.tree.mHash2Index[item.hashId];
+ }
+ }
+
+ // Modified items need to be updated.
+ for (let item of modItems.mArray) {
+ if (item.hashId in this.tree.mHash2Index) {
+ // Make sure we're using the new version of a modified item.
+ this.tree.mTaskArray[this.tree.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.tree.mTaskArray.splice(index, 1);
+ this.tree.rowCountChanged(index, -1);
+ });
+
+ // Add the new items.
+ for (let item of addItems.mArray) {
+ if (!(item.hashId in this.tree.mHash2Index)) {
+ let index = this.tree.mTaskArray.length;
+ this.tree.mTaskArray.push(item);
+ this.tree.mHash2Index[item.hashId] = index;
+ this.tree.rowCountChanged(index, 1);
+ firstHash = firstHash || item.hashId;
+ }
+ }
+
+ if (doNotSort) {
+ this.tree.recreateHashTable();
+ } else {
+ this.tree.sortItems();
+ }
+
+ if (selectNew && firstHash && firstHash in this.tree.mHash2Index) {
+ // Select the first item added into the list.
+ selIndex = this.tree.mHash2Index[firstHash];
+ } else if (selItem && selItem.hashId in this.tree.mHash2Index) {
+ // Select the previously selected item.
+ selIndex = this.tree.mHash2Index[selItem.hashId];
+ } else if (selIndex >= this.tree.mTaskArray.length) {
+ // Make sure the previously selected index is valid.
+ selIndex = this.tree.mTaskArray.length - 1;
+ }
+
+ if (selIndex > -1) {
+ this.tree.view.selection.select(selIndex);
+ this.tree.ensureRowIsVisible(selIndex);
+ }
+
+ this.tree.endUpdateBatch();
+ }
+
+ /**
+ * Remove all tasks from the list/tree.
+ */
+ clear() {
+ let count = this.tree.mTaskArray.length;
+ if (count > 0) {
+ this.tree.mTaskArray = [];
+ this.tree.mHash2Index = {};
+ this.tree.rowCountChanged(0, -count);
+ this.tree.view.selection.clearSelection();
+ }
+ }
+
+ /**
+ * Refresh the display for a given task.
+ *
+ * @param {object} item - The task object to refresh.
+ */
+ updateItem(item) {
+ let index = this.tree.mHash2Index[item.hashId];
+ if (index) {
+ this.tree.invalidateRow(index);
+ }
+ }
+
+ /**
+ * Return the item (task) object that's related to a given event. If passed a column and/or row
+ * object, set their 'value' property to the column and/or row related to the event.
+ *
+ * @param {Event} event - An event.
+ * @param {object} [col] - A column object.
+ * @param {object} [row] - A row object.
+ * @returns {object | false} The task object related to the event or false if none found.
+ */
+ getItemFromEvent(event, col, row) {
+ let { col: eventColumn, row: eventRow } = this.tree.getCellAt(event.clientX, event.clientY);
+ if (col) {
+ col.value = eventColumn;
+ }
+ if (row) {
+ row.value = eventRow;
+ }
+ return eventRow > -1 && this.tree.mTaskArray[eventRow];
+ }
+
+ // nsITreeView Methods and Properties
+
+ get rowCount() {
+ return this.tree.mTaskArray.length;
+ }
+
+ getCellProperties(row, col) {
+ let rowProps = this.getRowProperties(row);
+ let colProps = this.getColumnProperties(col);
+ return rowProps + (rowProps && colProps ? " " : "") + colProps;
+ }
+
+ getColumnProperties(col) {
+ return col.element.getAttribute("id") || "";
+ }
+
+ getRowProperties(row) {
+ let properties = [];
+ let item = this.tree.mTaskArray[row];
+ if (item.priority > 0 && item.priority < 5) {
+ properties.push("highpriority");
+ } else if (item.priority > 5 && item.priority < 10) {
+ properties.push("lowpriority");
+ }
+ properties.push(cal.item.getProgressAtom(item));
+
+ // Add calendar name and id atom.
+ properties.push("calendar-" + cal.view.formatStringForCSSRule(item.calendar.name));
+ properties.push("calendarid-" + cal.view.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(cal.view.formatStringForCSSRule));
+
+ return properties.join(" ");
+ }
+
+ cycleCell(row, col) {
+ let task = this.tree.mTaskArray[row];
+
+ // 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 (col != null) {
+ let content = col.element.getAttribute("itemproperty");
+ if (content == "completed") {
+ let newTask = task.clone().QueryInterface(Ci.calITodo);
+ newTask.isCompleted = !task.completedDate;
+ doTransaction("modify", newTask, newTask.calendar, task, null);
+ }
+ }
+ }
+
+ cycleHeader(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;
+ let selectedItems = this.tree.selectedTasks;
+ this.tree.sortItems();
+ if (selectedItems != undefined) {
+ this.tree.view.selection.clearSelection();
+ for (let item of selectedItems) {
+ let index = this.tree.mHash2Index[item.hashId];
+ this.tree.view.selection.toggleSelect(index);
+ }
+ }
+ }
+
+ getCellText(row, col) {
+ let task = this.tree.mTaskArray[row];
+ if (!task) {
+ return "";
+ }
+
+ const property = col.element.getAttribute("itemproperty");
+ switch (property) {
+ case "title":
+ // Return title, or "Untitled" if empty/null.
+ return task.title ? task.title.replace(/\n/g, " ") : cal.l10n.getCalString("eventUntitled");
+ case "entryDate":
+ case "dueDate":
+ case "completedDate":
+ return task.recurrenceInfo
+ ? cal.l10n.getDateFmtString("Repeating")
+ : this._formatDateTime(task[property]);
+ case "percentComplete":
+ return task.percentComplete > 0 ? task.percentComplete + "%" : "";
+ case "categories":
+ // TODO This is l10n-unfriendly.
+ return task.getCategories().join(", ");
+ case "location":
+ return task.getProperty("LOCATION");
+ case "status":
+ return getToDoStatusString(task);
+ case "calendar":
+ return task.calendar.name;
+ case "duration":
+ return this.tree.duration(task);
+ case "completed":
+ case "priority":
+ default:
+ return "";
+ }
+ }
+
+ getCellValue(row, col) {
+ let task = this.tree.mTaskArray[row];
+ if (!task) {
+ return null;
+ }
+ switch (col.element.getAttribute("itemproperty")) {
+ case "percentComplete":
+ return task.percentComplete;
+ }
+ return null;
+ }
+
+ setCellValue(row, col, value) {
+ return null;
+ }
+
+ getImageSrc(row, col) {
+ return "";
+ }
+
+ isEditable(row, col) {
+ return true;
+ }
+
+ /**
+ * Called to link the task tree to the tree view. A null argument un-sets/un-links the tree.
+ *
+ * @param {object | null} tree
+ */
+ setTree(tree) {
+ const hasOldTree = this.tree != null;
+ if (hasOldTree && !tree) {
+ // Balances the addObserver calls from the refresh method in the tree.
+
+ // Remove the composite calendar observer.
+ const composite = cal.view.getCompositeCalendar(window);
+ composite.removeObserver(this.tree.mTaskTreeObserver);
+
+ // Remove the preference observer.
+ const branch = Services.prefs.getBranch("");
+ branch.removeObserver("calendar.", this.tree.mPrefObserver);
+ }
+ this.tree = tree;
+ }
+
+ isContainer(row) {
+ return false;
+ }
+ isContainerOpen(row) {
+ return false;
+ }
+ isContainerEmpty(row) {
+ return false;
+ }
+
+ isSeparator(row) {
+ return false;
+ }
+
+ isSorted(row) {
+ return false;
+ }
+
+ canDrop() {
+ return false;
+ }
+
+ drop(row, orientation) {}
+
+ getParentIndex(row) {
+ return -1;
+ }
+
+ getLevel(row) {
+ return 0;
+ }
+
+ // End nsITreeView Methods and Properties
+ // Task Tree Event Handlers
+
+ onSelect(event) {}
+
+ /**
+ * Handle double click events.
+ *
+ * @param {Event} event - The double click event.
+ */
+ onDoubleClick(event) {
+ // Only handle left mouse button clicks.
+ if (event.button != 0) {
+ return;
+ }
+ const initialDate = cal.dtz.getDefaultStartDate(this.tree.getInitialDate());
+ const col = {};
+ const item = this.getItemFromEvent(event, col);
+ if (item) {
+ const itemProperty = col.value.element.getAttribute("itemproperty");
+
+ // If itemProperty == "completed" then the user has clicked a "completed" checkbox
+ // and `item` holds the checkbox state toggled by the first click. So, to make sure the
+ // user notices that the state changed, don't call modifyEventWithDialog.
+ if (itemProperty != "completed") {
+ modifyEventWithDialog(item, true, initialDate);
+ }
+ } else {
+ createTodoWithDialog(null, null, null, null, initialDate);
+ }
+ }
+
+ /**
+ * Handle key press events.
+ *
+ * @param {Event} event - The key press event.
+ */
+ onKeyPress(event) {
+ switch (event.key) {
+ case "Delete": {
+ event.target.triggerNode = this.tree;
+ document.getElementById("calendar_delete_todo_command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ break;
+ }
+ case " ": {
+ if (this.tree.currentIndex > -1) {
+ let col = this.tree.querySelector("[itemproperty='completed']");
+ this.cycleCell(this.tree.currentIndex, { element: col });
+ }
+ break;
+ }
+ case "Enter": {
+ let index = this.tree.currentIndex;
+ if (index > -1) {
+ modifyEventWithDialog(this.tree.mTaskArray[index]);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Set the context menu on mousedown to change it before it is opened.
+ *
+ * @param {Event} event - The mousedown event.
+ */
+ onMouseDown(event) {
+ if (!this.getItemFromEvent(event)) {
+ this.tree.view.selection.invalidateSelection();
+ }
+ }
+
+ // Private Methods and Attributes
+
+ /**
+ * Format a datetime object for display.
+ *
+ * @param {object} dateTime - From a todo object, not a JavaScript date.
+ * @returns {string} Formatted string version of the datetime ("" if invalid).
+ */
+ _formatDateTime(dateTime) {
+ return dateTime && dateTime.isValid
+ ? cal.dtz.formatter.formatDateTime(dateTime.getInTimezone(cal.dtz.defaultTimezone))
+ : "";
+ }
+}