Bug 1531096 - [de-xbl] convert calendar-task-tree bindings to custom elements. r=philipp
authorPaul Morris <paul@paulwmorris.com>
Tue, 09 Apr 2019 14:19:57 -0400
changeset 26370 b6e519322394
parent 26369 765a945dbeb3
child 26371 4a2e39cfc820
push id15805
push usermozilla@jorgk.com
push dateWed, 17 Apr 2019 07:56:42 +0000
treeherdercomm-central@b6e519322394 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp
bugs1531096
Bug 1531096 - [de-xbl] convert calendar-task-tree bindings to custom elements. r=philipp
calendar/.eslintrc.js
calendar/base/content/calendar-bindings.css
calendar/base/content/calendar-task-tree-utils.js
calendar/base/content/calendar-task-tree-view.js
calendar/base/content/calendar-task-tree.js
calendar/base/content/calendar-task-tree.xml
calendar/base/content/calendar-task-view.xul
calendar/base/content/calendar-unifinder-todo.xul
calendar/base/jar.mn
calendar/base/modules/calUtils.jsm
calendar/base/themes/common/calendar-task-tree.css
calendar/base/themes/osx/calendar-task-tree.css
calendar/lightning/content/imip-bar-overlay.xul
calendar/lightning/content/lightning-widgets.css
calendar/lightning/jar.mn
calendar/test/mozmill/testBasicFunctionality.js
calendar/test/mozmill/views/testTaskView.js
mail/installer/allowed-dupes.mn
suite/installer/allowed-dupes.mn
--- a/calendar/.eslintrc.js
+++ b/calendar/.eslintrc.js
@@ -401,18 +401,19 @@ module.exports = {
         "func-names": [2, "never"],
 
         // Enforce placing object properties on separate lines
         "object-property-newline": [2, { allowMultiplePropertiesPerLine: true }],
 
         // Enforce consistent line breaks inside braces
         "object-curly-newline": [2, { multiline: true }],
 
-        // Require Object Literal Shorthand Syntax (consistent for now)
-        "object-shorthand": [2, "consistent"],
+        // Do Not Require Object Literal Shorthand Syntax
+        // (Override the parent eslintrc setting for this.)
+        "object-shorthand": "off",
 
         // Disallow whitespace before properties
         "no-whitespace-before-property": 2,
 
         // Disallow unnecessary escape usage
         "no-useless-escape": 2,
 
         // Disallow mixes of different operators, but allow simple math operations.
--- a/calendar/base/content/calendar-bindings.css
+++ b/calendar/base/content/calendar-bindings.css
@@ -16,17 +16,13 @@ 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);
-}
-
 /* Needed until menupopup is fully converted to a custom element (see bug 1531296). */
 menupopup[is="calendar-task-progress-menupopup"],
 menupopup[is="calendar-task-priority-menupopup"] {
   -moz-binding: none;
 }
--- a/calendar/base/content/calendar-task-tree-utils.js
+++ b/calendar/base/content/calendar-task-tree-utils.js
@@ -60,31 +60,28 @@ function setAttributeOnChildrenOrTheirCo
 /**
  * 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;
+    const treeNodeId = aEvent.target.triggerNode.closest(".calendar-task-tree").id;
+    const isTodaypane = treeNodeId == "unifinder-todo-tree";
+    const isMainTaskTree = treeNodeId == "calendar-task-tree";
+
+    document.getElementById("task-context-menu-new").hidden = isTodaypane;
+    document.getElementById("task-context-menu-modify").hidden = isTodaypane;
+    document.getElementById("task-context-menu-new-todaypane").hidden = isMainTaskTree;
+    document.getElementById("task-context-menu-modify-todaypane").hidden = isMainTaskTree;
+    document.getElementById("task-context-menu-filter-todaypane").hidden = isMainTaskTree;
+    document.getElementById("task-context-menu-separator-filter").hidden = isMainTaskTree;
+
     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);
 
     setAttributeOnChildrenOrTheirCommands("disabled", !tasksSelected, aEvent.target);
 
     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");
@@ -110,17 +107,17 @@ function changeContextMenuForTask(aEvent
 }
 
 /**
  * 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;
+    let tree = aEvent.target.triggerNode.closest(".calendar-task-tree");
 
     if (tree) {
         tree.updateFocus();
     }
 }
 
 /**
  * Change the opening menu for the selected tasks.
new file mode 100644
--- /dev/null
+++ b/calendar/base/content/calendar-task-tree-view.js
@@ -0,0 +1,497 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-globals-from calendar-common-sets.js */
+
+/* exported CalendarTaskTreeView */
+
+/**
+ * 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(IID) {
+        return cal.generateClassQI(this, IID, [Ci.nsITreeView]);
+    }
+
+    /**
+     * Get the selected column.
+     *
+     * @return {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.
+     * @return {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, null, 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": {
+                document.popupNode = 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.
+     * @return {string}            Formatted string version of the datetime ("" if invalid).
+     */
+    _formatDateTime(dateTime) {
+        return dateTime && dateTime.isValid
+            ? Cc["@mozilla.org/calendar/datetime-formatter;1"]
+                .getService(Ci.calIDateTimeFormatter)
+                .formatDateTime(dateTime.getInTimezone(cal.dtz.defaultTimezone))
+            : "";
+    }
+}
rename from calendar/base/content/calendar-task-tree.xml
rename to calendar/base/content/calendar-task-tree.js
--- a/calendar/base/content/calendar-task-tree.xml
+++ b/calendar/base/content/calendar-task-tree.js
@@ -1,1149 +1,642 @@
-<?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/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-globals-from agenda-listbox-utils.js -->
-<!-- import-globals-from calendar-common-sets.js -->
-<!-- import-globals-from calendar-dnd-listener.js -->
-<!-- import-globals-from calendar-task-tree-utils.js -->
+/* import-globals-from calendar-common-sets.js */
+
+/* globals MozXULElement CalendarTaskTreeView */
 
-<!DOCTYPE dialog [
-  <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
-  <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
-]>
+// Wrap in a block to prevent leaking to window scope.
+{
+    /**
+     * An observer for the calendar event data source. This keeps the unifinder
+     * display up to date when the calendar event data is changed.
+     *
+     * @implements {calIObserver}
+     * @implements {calICompositeObserver}
+     */
+    class TaskTreeObserver {
+        /**
+         * Creates and connects the new observer to a CalendarTaskTree and sets up Query Interface.
+         *
+         * @param {CalendarTaskTree} taskTree    The tree to observe.
+         */
+        constructor(taskTree) {
+            this.tree = taskTree;
+            this.QueryInterface = cal.generateQI([Ci.calICompositeObserver, Ci.calIObserver]);
+        }
 
-<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">
+        // calIObserver Methods
+
+        onStartBatch() { }
+
+        onEndBatch() { }
+
+        onLoad() {
+            this.tree.refresh();
+        }
+
+        onAddItem(item) {
+            if (cal.item.isToDo(item)) {
+                this.tree.mTreeView.addItems(this.tree.mFilter.getOccurrences(item));
+            }
+        }
 
-  <binding id="calendar-task-tree">
-    <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 class="calendar-task-tree-col-completed-checkboximg" 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"
-                       minwidth="40"
-                       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>
+        onModifyItem(newItem, oldItem) {
+            if (cal.item.isToDo(newItem) || cal.item.isToDo(oldItem)) {
+                this.tree.mTreeView.modifyItems(
+                    this.tree.mFilter.getOccurrences(newItem),
+                    this.tree.mFilter.getOccurrences(oldItem)
+                );
+                // We also need to notify potential listeners.
+                let event = document.createEvent("Events");
+                event.initEvent("select", true, false);
+                this.tree.dispatchEvent(event);
+            }
+        }
+
+        onDeleteItem(deletedItem) {
+            if (cal.item.isToDo(deletedItem)) {
+                this.tree.mTreeView.removeItems(
+                    this.tree.mFilter.getOccurrences(deletedItem)
+                );
+            }
+        }
+
+        onError(calendar, errNo, message) { }
 
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-          const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-          const { cal } = ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
+        onPropertyChanged(calendar, name, value, oldValue) {
+            switch (name) {
+                case "disabled":
+                    if (value) {
+                        this.tree.onCalendarRemoved(calendar);
+                    } else {
+                        this.tree.onCalendarAdded(calendar);
+                    }
+                    break;
+            }
+        }
+
+        onPropertyDeleting(calendar, name) {
+            this.onPropertyChanged(calendar, name, null, null);
+        }
 
-          // set up the tree filter
-          this.mFilter = new calFilter();
+        // End calIObserver Methods
+        // calICompositeObserver Methods
+
+        onCalendarAdded(calendar) {
+            if (!calendar.getProperty("disabled")) {
+                this.tree.onCalendarAdded(calendar);
+            }
+        }
+
+        onCalendarRemoved(calendar) {
+            this.tree.onCalendarRemoved(calendar);
+        }
+
+        onDefaultCalendarChanged(newDefaultCalendar) { }
+
+        // End calICompositeObserver Methods
+    }
 
-          // set up the custom tree view
-          let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
-          customElements.upgrade(tree);
-          this.mTreeView.tree = tree;
-          tree.view = this.mTreeView;
-
-          // set up our calendar event observer
-          let composite = cal.view.getCompositeCalendar(window);
-          composite.addObserver(this.mTaskTreeObserver);
-
-          // set up the preference observer
-          let branch = Services.prefs.getBranch("");
-          branch.addObserver("calendar.", this);
-
-
-          // 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];
-                  }
-              }
-          }
-
-          this.dispatchEvent(new CustomEvent("bindingattached", { bubbles: false }));
-      ]]></constructor>
-      <destructor><![CDATA[
-          const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-          // remove composite calendar observer
-          let composite = cal.view.getCompositeCalendar(window);
-          composite.removeObserver(this.mTaskTreeObserver);
-
-          // remove the preference observer
-          let branch = Services.prefs.getBranch("");
-          branch.removeObserver("calendar.", this);
+    /**
+     * Custom element for table-style display of tasks (rows and columns).
+     *
+     * @extends {MozTree}
+     */
+    class CalendarTaskTree extends customElements.get("tree") {
+        connectedCallback() {
+            super.connectedCallback();
+            if (this.delayConnectedCallback() || this.hasConnected) {
+                return;
+            }
+            this.hasConnected = true;
+            this.appendChild(MozXULElement.parseXULToFragment(`
+                <treecols>
+                  <treecol is="treecol-image" id="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;"/>
+                  <splitter class="tree-splitter" ordinal="2"/>
+                  <treecol is="treecol-image" id="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;"/>
+                  <splitter class="tree-splitter" ordinal="4"/>
+                  <treecol class="calendar-task-tree-col-title"
+                           itemproperty="title"
+                           flex="1"
+                           label="&calendar.unifinder.tree.title.label;"
+                           tooltiptext="&calendar.unifinder.tree.title.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="6"/>
+                  <treecol class="calendar-task-tree-col-entrydate"
+                           itemproperty="entryDate"
+                           flex="1"
+                           label="&calendar.unifinder.tree.startdate.label;"
+                           tooltiptext="&calendar.unifinder.tree.startdate.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="8"/>
+                  <treecol class="calendar-task-tree-col-duedate"
+                           itemproperty="dueDate"
+                           flex="1"
+                           label="&calendar.unifinder.tree.duedate.label;"
+                           tooltiptext="&calendar.unifinder.tree.duedate.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="10"/>
+                  <treecol class="calendar-task-tree-col-duration"
+                           itemproperty="duration"
+                           sortKey="dueDate"
+                           flex="1"
+                           label="&calendar.unifinder.tree.duration.label;"
+                           tooltiptext="&calendar.unifinder.tree.duration.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="12"/>
+                  <treecol class="calendar-task-tree-col-completeddate"
+                           itemproperty="completedDate"
+                           flex="1"
+                           label="&calendar.unifinder.tree.completeddate.label;"
+                           tooltiptext="&calendar.unifinder.tree.completeddate.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="14"/>
+                  <treecol class="calendar-task-tree-col-percentcomplete"
+                           itemproperty="percentComplete"
+                           flex="1"
+                           minwidth="40"
+                           label="&calendar.unifinder.tree.percentcomplete.label;"
+                           tooltiptext="&calendar.unifinder.tree.percentcomplete.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="16"/>
+                  <treecol class="calendar-task-tree-col-categories"
+                           itemproperty="categories"
+                           flex="1"
+                           label="&calendar.unifinder.tree.categories.label;"
+                           tooltiptext="&calendar.unifinder.tree.categories.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="18"/>
+                  <treecol class="calendar-task-tree-col-location"
+                           itemproperty="location"
+                           flex="1"
+                           label="&calendar.unifinder.tree.location.label;"
+                           tooltiptext="&calendar.unifinder.tree.location.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="20"/>
+                  <treecol class="calendar-task-tree-col-status"
+                           itemproperty="status"
+                           flex="1"
+                           label="&calendar.unifinder.tree.status.label;"
+                           tooltiptext="&calendar.unifinder.tree.status.tooltip2;"/>
+                  <splitter class="tree-splitter" ordinal="22"/>
+                  <treecol class="calendar-task-tree-col-calendar"
+                           itemproperty="calendar"
+                           flex="1"
+                           label="&calendar.unifinder.tree.calendarname.label;"
+                           tooltiptext="&calendar.unifinder.tree.calendarname.tooltip2;"/>
+                </treecols>
+                <treechildren class="calendar-task-treechildren"
+                              tooltip="taskTreeTooltip"
+                              ondblclick="mTreeView.onDoubleClick(event)"/>
+                `,
+                [
+                    "chrome://calendar/locale/global.dtd",
+                    "chrome://calendar/locale/calendar.dtd"
+                ]
+            ));
 
-          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>
+            this.classList.add("calendar-task-tree");
+            // TODO: enableColumnDrag="false" does not seem to prevent dragging columns, remove?
+            this.setAttribute("enableColumnDrag", "false");
+            this.setAttribute("keepcurrentinview", "true");
+
+            this.addEventListener("select", (event) => {
+                this.mTreeView.onSelect(event);
+                if (calendarController.todo_tasktree_focused) {
+                    calendarController.onSelectionChanged({ detail: this.selectedTasks });
+                }
+            });
+
+            this.addEventListener("focus", (event) => {
+                this.updateFocus();
+            });
+
+            this.addEventListener("blur", (event) => {
+                this.updateFocus();
+            });
+
+            this.addEventListener("keypress", (event) => {
+                this.mTreeView.onKeyPress(event);
+            });
+
+            this.addEventListener("mousedown", (event) => {
+                this.mTreeView.onMouseDown(event);
+            });
+
+            this.addEventListener("dragstart", (event) => {
+                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;
+                }
+                invokeEventDragSession(item, event.target);
+            });
+
+            this.mTaskArray = [];
+            this.mHash2Index = {};
+            this.mPendingRefreshJobs = {};
+            this.mShowCompletedTasks = true;
+            this.mFilter = null;
+            this.mStartDate = null;
+            this.mEndDate = null;
+            this.mDateRangeFilter = null;
+            this.mTextFilterField = null;
+
+            this.mTreeView = null;
+            this.mTaskTreeObserver = new TaskTreeObserver(this);
 
-      <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>
+            // Observes and responds to changes to calendar preferences.
+            this.mPrefObserver = (subject, topic, prefName) => {
+                switch (prefName) {
+                    case "calendar.date.format":
+                    case "calendar.timezone.local":
+                        this.refresh();
+                        break;
+                }
+            };
+
+            // We want to make several attributes on the column
+            // elements persistent, but unfortunately there's no
+            // reliable way with the 'persist' feature.
+            // That's why we need to store the necessary bits and
+            // pieces on the calendar-task-tree element.
+            let visibleColumns = 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";
 
-      <property name="currentIndex">
-        <getter><![CDATA[
-          let tree = document.getAnonymousElementByAttribute(
-              this, "anonid", "calendar-task-tree");
-          return tree.currentIndex;
-      ]]></getter>
-      </property>
+            this.querySelectorAll("treecol").forEach((col) => {
+                const itemProperty = col.getAttribute("itemproperty");
+                if (visibleColumns.some(visCol => visCol == itemProperty)) {
+                    col.removeAttribute("hidden");
+                } else {
+                    col.setAttribute("hidden", "true");
+                }
+                if (ordinals && ordinals.length > 0) {
+                    col.ordinal = Number(ordinals.shift());
+                }
+                if (widths && widths.length > 0) {
+                    col.width = Number(widths.shift());
+                }
+                if (sorted && sorted == itemProperty) {
+                    this.mTreeView.sortDirection = sortDirection;
+                    this.mTreeView.selectedColumn = col;
+                }
+            });
 
-      <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>
+            // Set up the tree filter.
+            this.mFilter = new calFilter();
+
+            // This refresh call sets up the tree view and observers.
+            this.refresh();
+
+            this.dispatchEvent(new CustomEvent("bindingattached", { bubbles: false }));
+        }
 
-      <property name="selectedTasks" readonly="true">
-        <getter><![CDATA[
+        get currentTask() {
+            const index = this.currentIndex;
+
+            const isSelected = this.view && this.view.selection &&
+                this.view.selection.isSelected(index);
+
+            return isSelected ? this.mTaskArray[index] : null;
+        }
+
+        get selectedTasks() {
             let tasks = [];
             let start = {};
             let end = {};
             if (!this.mTreeView.selection) {
                 return tasks;
             }
 
-            let rangeCount = this.mTreeView.selection.getRangeCount();
+            const 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[
-            const { PluralForm } = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
-
-            if (aTask && aTask.dueDate && aTask.dueDate.isValid) {
-                let dur = aTask.dueDate.subtractDate(cal.dtz.now());
-                if (!dur.isNegative) {
-                    let minutes = Math.ceil(dur.inSeconds / 60);
-                    if (minutes >= 1440) { // 1 day or more
-                        let dueIn = PluralForm.get(dur.days, cal.l10n.getCalString("dueInDays"));
-                        return dueIn.replace("#1", dur.days);
-                    } else if (minutes >= 60) { // 1 hour or more
-                        let dueIn = PluralForm.get(dur.hours, cal.l10n.getCalString("dueInHours"));
-                        return dueIn.replace("#1", dur.hours);
-                    } else {
-                        // Less than one hour
-                        return cal.l10n.getCalString("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, cal.l10n.getCalString("dueInDays"));
-                        return "-" + dueIn.replace("#1", dur.days);
-                    } else if (minutes >= 60) { // 1 hour or more
-                        let dueIn = PluralForm.get(dur.hours, cal.l10n.getCalString("dueInHours"));
-                        return "-" + dueIn.replace("#1", dur.hours);
-                    } else {
-                        // Less than one hour
-                        return cal.l10n.getCalString("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[
-        ({
-            QueryInterface: ChromeUtils.generateQI([Ci.nsITreeView]),
-
-            /**
-             * 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);
-            },
+        set showCompleted(val) {
+            this.mShowCompletedTasks = val;
+        }
 
-            // 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 cal.item.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();
-                }
+        get showCompleted() {
+            return this.mShowCompletedTasks;
+        }
 
-                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(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());
-                }
+        set textFilterField(val) {
+            this.mTextFilterField = val;
+        }
 
-                // Alarm status atom
-                if (item.getAlarms({}).length) {
-                    properties.push("alarm");
-                }
-
-                // Task categories
-                properties = properties.concat(item.getCategories({})
-                                                   .map(cal.view.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(Ci.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
-                        if (task.title) {
-                            return task.title.replace(/\n/g, " ");
-                        } else {
-                            return cal.l10n.getCalString("eventUntitled");
-                        }
-                    case "entryDate":
-                        return task.recurrenceInfo ? cal.l10n.getDateFmtString("Repeating") : this._formatDateTime(task.entryDate);
-                    case "dueDate":
-                        return task.recurrenceInfo ? cal.l10n.getDateFmtString("Repeating") : this._formatDateTime(task.dueDate);
-                    case "completedDate":
-                        return task.recurrenceInfo ? cal.l10n.getDateFmtString("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;
-            },
+        get textFilterField() {
+            return this.mTextFilterField;
+        }
 
-            // 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 indentation.
-            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;
-            },
-
-            /**
-             * Task Tree Events
-             */
-            onSelect: function(event) {},
+        /**
+         * Calculates the text to display in the "Due In" column for the given task,
+         * the amount of time between now and when the task is due.
+         *
+         * @param {Object} task    A task object.
+         * @return {string}        A formatted string for the "Due In" column for the task.
+         */
+        duration(task) {
+            const noValidDueDate = !(task && task.dueDate && task.dueDate.isValid);
+            if (noValidDueDate) {
+                return "";
+            }
 
-            onDoubleClick: function(event) {
-                if (event.button == 0) {
-                    let initialDate = cal.dtz.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) {
-                let { col, row } = this.treebox.getCellAt(event.clientX, event.clientY);
-                if (aCol) {
-                    aCol.value = col;
-                }
-                if (aRow) {
-                    aRow.value = row;
-                }
-                return row > -1 && this.binding.mTaskArray[row];
-            },
-
-            // Helper function to display datetimes
-            _formatDateTime: function(aDateTime) {
-                let dateFormatter = Cc["@mozilla.org/calendar/datetime-formatter;1"]
-                                      .getService(Ci.calIDateTimeFormatter);
-
-                // datetime is from todo object, it is not a javascript date
-                if (aDateTime && aDateTime.isValid) {
-                    let dateTime = aDateTime.getInTimezone(cal.dtz.defaultTimezone);
-                    return dateFormatter.formatDateTime(dateTime);
-                }
+            const isCompleted = task.completedDate && task.completedDate.isValid;
+            const dur = task.dueDate.subtractDate(cal.dtz.now());
+            if (isCompleted && dur.isNegative) {
                 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: cal.generateQI([
-                Ci.calICompositeObserver,
-                Ci.calIObserver
-            ]),
-
-            /**
-             * calIObserver methods and properties
-             */
-            onStartBatch: function() {
-            },
-
-            onEndBatch: function() {
-            },
+            const absSeconds = Math.abs(dur.inSeconds);
+            const absMinutes = Math.ceil(absSeconds / 60);
+            const prefix = dur.isNegative ? "-" : "";
 
-            onLoad: function() {
-                this.binding.refresh();
-            },
-
-            onAddItem: function(aItem) {
-                if (cal.item.isToDo(aItem)) {
-                    this.binding.mTreeView.addItems(this.binding.mFilter.getOccurrences(aItem));
-                }
-            },
-
-            onModifyItem: function(aNewItem, aOldItem) {
-                if (cal.item.isToDo(aNewItem) || cal.item.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.item.isToDo(aDeletedItem)) {
-                    this.binding.mTreeView.removeItems(this.binding.mFilter.getOccurrences(aDeletedItem));
-                }
-            },
+            if (absMinutes >= 1440) {
+                // 1 day or more.
+                return prefix + PluralForm
+                    .get(dur.days, cal.l10n.getCalString("dueInDays"))
+                    .replace("#1", dur.days);
+            } else if (absMinutes >= 60) {
+                // 1 hour or more.
+                return prefix + PluralForm
+                    .get(dur.hours, cal.l10n.getCalString("dueInHours"))
+                    .replace("#1", dur.hours);
+            } else {
+                // Less than one hour.
+                return cal.l10n.getCalString("dueInLessThanOneHour");
+            }
+        }
 
-            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);
-                }
-            },
+        /**
+         * Return the task object at a given row.
+         *
+         * @param {number} row        The index number identifying the row.
+         * @return {Object | null}    A task object or null if none found.
+         */
+        getTaskAtRow(row) {
+            return row > -1 ? this.mTaskArray[row] : null;
+        }
 
-            onCalendarRemoved: function(aCalendar) {
-                this.binding.onCalendarRemoved(aCalendar);
-            },
-
-            onDefaultCalendarChanged: function(aNewDefaultCalendar) {}
-        })
-      ]]></field>
+        /**
+         * Return the task object related to a given event.
+         *
+         * @param {Event} event        The event.
+         * @return {Object | false}    The task object related to the event or false if none found.
+         */
+        getTaskFromEvent(event) {
+            return this.mTreeView.getItemFromEvent(event);
+        }
 
-      <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[
+        refreshFromCalendar(calendar) {
             let refreshJob = {
                 QueryInterface: ChromeUtils.generateQI([Ci.calIOperationListener]),
-                binding: this,
+                tree: 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];
+                onOperationComplete(opCalendar, status, operationType, id, dateTime) {
+                    if (opCalendar.id in this.tree.mPendingRefreshJobs) {
+                        delete this.tree.mPendingRefreshJobs[opCalendar.id];
                     }
 
-                    let oldItems = this.binding.mTaskArray.filter(item => item.calendar.id == aOpCalendar.id);
-                    this.binding.mTreeView.modifyItems(this.items, oldItems);
+                    let oldItems = this.tree.mTaskArray.filter(item => item.calendar.id == opCalendar.id);
+                    this.tree.mTreeView.modifyItems(this.items, oldItems);
                 },
 
-                onGetResult: function(aOpCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
-                    this.items = this.items.concat(aItems);
+                onGetResult(opCalendar, status, itemType, detail, count, items) {
+                    this.items = this.items.concat(items);
                 },
 
-                cancel: function() {
+                cancel() {
                     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();
+                execute() {
+                    if (calendar.id in this.tree.mPendingRefreshJobs) {
+                        this.tree.mPendingRefreshJobs[calendar.id].cancel();
                     }
-                    this.calendar = aCalendar;
+                    this.calendar = calendar;
                     this.items = [];
 
-                    let operation = this.binding.mFilter.getItems(aCalendar,
-                                                                  aCalendar.ITEM_FILTER_TYPE_TODO,
-                                                                  this);
+                    let operation = this.tree.mFilter.getItems(calendar,
+                        calendar.ITEM_FILTER_TYPE_TODO,
+                        this);
                     if (operation && operation.isPending) {
                         this.operation = operation;
-                        this.binding.mPendingRefreshJobs[aCalendar.id] = this;
+                        this.tree.mPendingRefreshJobs[calendar.id] = this;
                     }
                 }
             };
 
             refreshJob.execute();
-        ]]></body>
-      </method>
+        }
 
-      <method name="selectAll">
-        <body><![CDATA[
+        selectAll() {
             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 = cal.view.getCompositeCalendar(window).getCalendars({}) || [];
-            for (let calendar of cals) {
-                if (!calendar.getProperty("disabled")) {
-                    this.refreshFromCalendar(calendar, aFilter);
-                }
-            }
-        ]]></body>
-      </method>
+        /**
+         * Refreshes the display. Called during connectedCallback and by event observers.
+         * Sets up the tree view, calendar event observer, and preference observer.
+         */
+        refresh() {
+            let tree = this.closest(".calendar-task-tree");
+
+            // Note: attempting to merge this.mTreeView and tree.view did not work.
+            this.mTreeView = new CalendarTaskTreeView(tree);
+            tree.view = this.mTreeView;
+
+            cal.view.getCompositeCalendar(window).addObserver(this.mTaskTreeObserver);
+
+            Services.prefs.getBranch("").addObserver("calendar.", this.mPrefObserver);
 
-      <method name="onCalendarAdded">
-        <parameter name="aCalendar"/>
-        <parameter name="aFilter"/>
-        <body><![CDATA[
-            if (!aCalendar.getProperty("disabled")) {
-                this.refreshFromCalendar(aCalendar, aFilter);
-            }
-        ]]></body>
-      </method>
+            const cals = cal.view.getCompositeCalendar(window).getCalendars({}) || [];
+            const enabledCals = cals.filter(calendar => !calendar.getProperty("disabled"));
+
+            enabledCals.forEach(calendar => this.refreshFromCalendar(calendar));
+        }
 
-      <method name="onCalendarRemoved">
-        <parameter name="aCalendar"/>
-        <body><![CDATA[
-            let tasks = this.mTaskArray.filter(task => task.calendar.id == aCalendar.id);
+        onCalendarAdded(calendar) {
+            if (!calendar.getProperty("disabled")) {
+                this.refreshFromCalendar(calendar);
+            }
+        }
+
+        onCalendarRemoved(calendar) {
+            const tasks = this.mTaskArray.filter(task => task.calendar.id == calendar.id);
             this.mTreeView.removeItems(tasks);
-        ]]></body>
-      </method>
+        }
 
-      <method name="sortItems">
-        <body><![CDATA[
+        sortItems() {
             if (this.mTreeView.selectedColumn) {
                 let column = this.mTreeView.selectedColumn;
                 let modifier = this.mTreeView.sortDirection == "descending" ? -1 : 1;
                 let sortKey = column.getAttribute("sortKey") || column.getAttribute("itemproperty");
 
                 cal.unifinder.sortItems(this.mTaskArray, sortKey, modifier);
             }
 
             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();
+        recreateHashTable() {
+            this.mHash2Index = this.mTaskArray.reduce((hash2Index, task, i) => {
+                hash2Index[task.hashId] = i;
+                return hash2Index;
+            }, {});
+
+            if (this.mTreeView.tree) {
+                this.mTreeView.tree.invalidate();
             }
-        ]]></body>
-      </method>
+        }
 
-      <method name="getInitialDate">
-        <body><![CDATA[
-            let initialDate = currentView().selectedDay;
-            return initialDate ? initialDate : cal.dtz.now();
-        ]]></body>
-      </method>
+        getInitialDate() {
+            return currentView().selectedDay || cal.dtz.now();
+        }
 
-      <method name="doUpdateFilter">
-        <parameter name="aFilter"/>
-        <body><![CDATA[
+        doUpdateFilter(filter) {
             let needsRefresh = false;
             let oldStart = this.mFilter.mStartDate;
             let oldEnd = this.mFilter.mEndDate;
             let filterText = this.mFilter.filterText || "";
 
-            if (aFilter) {
+            if (filter) {
                 let props = this.mFilter.filterProperties;
-                this.mFilter.applyFilter(aFilter);
+                this.mFilter.applyFilter(filter);
                 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();
+                    needsRefresh = needsRefresh ||
+                        filterText.toLowerCase() != this.mFilter.filterText.toLowerCase();
                 }
             }
 
-            // we only need to refresh the tree if the filter properties or date range changed
+            // We only need to refresh the tree if the filter properties or date range changed.
+            const start = this.mFilter.startDate;
+            const end = this.mFilter.mEndDate;
+
+            const sameStartDates = start && oldStart && oldStart.compare(start) == 0;
+            const sameEndDates = end && oldEnd && oldEnd.compare(end) == 0;
+
             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))) {
+                ((start || oldStart) && !sameStartDates) ||
+                ((end || oldEnd) && !sameEndDates)) {
                 this.refresh();
             }
-        ]]></body>
-      </method>
+        }
 
-      <method name="updateFilter">
-        <parameter name="aFilter"/>
-        <body><![CDATA[
-            this.doUpdateFilter(aFilter);
-        ]]></body>
-      </method>
+        updateFilter(filter) {
+            this.doUpdateFilter(filter);
+        }
 
-      <method name="updateFocus">
-        <body><![CDATA[
-            let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
+        updateFocus() {
             let menuOpen = false;
 
-            // we need to consider the tree focused if the context menu is open.
+            // 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;
+            let focused = (document.activeElement == this) || menuOpen;
 
             calendarController.onSelectionChanged({ detail: focused ? this.selectedTasks : [] });
             calendarController.todo_tasktree_focused = focused;
-        ]]></body>
-      </method>
-    </implementation>
+        }
+
+        disconnectedCallback() {
+            super.disconnectedCallback();
+
+            this.mTreeView = null;
+
+            let widths = "";
+            let ordinals = "";
+            let visible = "";
+            let sorted = this.mTreeView.selectedColumn;
 
-    <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;
-          }
-          invokeEventDragSession(item, event.target);
-      ]]></handler>
-    </handlers>
+            this.querySelectorAll("treecol").forEach((col) => {
+                if (col.getAttribute("hidden") != "true") {
+                    let content = col.getAttribute("itemproperty");
+                    visible += visible.length > 0 ? " " + content : content;
+                }
+                if (ordinals.length > 0) {
+                    ordinals += " ";
+                }
+                ordinals += col.ordinal;
+                if (widths.length > 0) {
+                    widths += " ";
+                }
+                widths += col.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");
+            }
+        }
+    }
 
-  </binding>
+    customElements.define("calendar-task-tree", CalendarTaskTree, { "extends": "tree" });
 
-  <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 : cal.dtz.now();
-            return initialDate ? initialDate : cal.dtz.now();
-        ]]></body>
-      </method>
+    /**
+     * Custom element for the task tree that appears in the todaypane.
+     */
+    class CalendarTaskTreeTodaypane extends CalendarTaskTree {
+        getInitialDate() {
+            return (agendaListbox.today && agendaListbox.today.start) || cal.dtz.now();
+        }
 
-      <method name="updateFilter">
-        <parameter name="aFilter"/>
-        <body><![CDATA[
-            this.mFilter.selectedDate = agendaListbox.today && agendaListbox.today.start ?
-                                        agendaListbox.today.start : cal.dtz.now();
-            this.doUpdateFilter(aFilter);
-        ]]></body>
-      </method>
-    </implementation>
-  </binding>
-</bindings>
+        updateFilter(filter) {
+            this.mFilter.selectedDate = this.getInitialDate();
+            this.doUpdateFilter(filter);
+        }
+    }
+
+    customElements.define("calendar-task-tree-todaypane",
+        CalendarTaskTreeTodaypane, { "extends": "tree" });
+}
--- a/calendar/base/content/calendar-task-view.xul
+++ b/calendar/base/content/calendar-task-view.xul
@@ -10,22 +10,24 @@
 <!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"
   xmlns:html="http://www.w3.org/1999/xhtml">
+  <script type="application/javascript" src="chrome://calendar/content/calFilter.js"/>
   <script type="application/javascript" src="chrome://calendar/content/calendar-task-tree-utils.js"/>
+  <script type="application/javascript" src="chrome://calendar/content/calendar-task-tree-view.js"/>
+  <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/calendar-item-bindings.js"/>
-  <script type="application/javascript" src="chrome://calendar/content/calFilter.js"/>
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript" src="chrome://calendar/content/calendar-menus.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">
@@ -48,20 +50,21 @@
                    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"/>
+        <tree is="calendar-task-tree" id="calendar-task-tree"
+              flex="1"
+              visible-columns="completed priority title entryDate 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">
--- a/calendar/base/content/calendar-unifinder-todo.xul
+++ b/calendar/base/content/calendar-unifinder-todo.xul
@@ -28,20 +28,21 @@
         <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"/>
+        <tree is="calendar-task-tree-todaypane" 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 themeableSearchBox"
                  onfocus="taskEdit.onFocus(event)"
                  onblur="taskEdit.onBlur(event)"
                  onkeypress="taskEdit.onKeyPress(event)"/>
       </vbox>
     </vbox>
   </vbox>
--- a/calendar/base/jar.mn
+++ b/calendar/base/jar.mn
@@ -26,17 +26,18 @@ calendar.jar:
     content/calendar/calendar-menus.js                     (content/calendar-menus.js)
     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-base-view.js                 (content/calendar-base-view.js)
     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-tree-view.js            (content/calendar-task-tree-view.js)
     content/calendar/calendar-task-tree-utils.js           (content/calendar-task-tree-utils.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)
--- a/calendar/base/modules/calUtils.jsm
+++ b/calendar/base/modules/calUtils.jsm
@@ -155,23 +155,26 @@ var cal = {
      *       QueryInterface(aIID) { return cal.generateClassQI(this, aIID, [Ci.calIThing]); }
      *
      *       ...
      *     }
      *
      * The function is cached, once this is called QueryInterface is replaced with
      * cal.generateQI()'s result.
      *
-     * @param {Object} aGlobal      The object to define the method on
-     * @param {nsIIDRef} aIID       The IID to query for
-     * @param {nsIIDRef[]}          The interfaces that this object implements
-     * @return {nsQIResult}         The object queried for aIID
+     * @param {Object} aGlobal          The object to define the method on
+     * @param {nsIIDRef} aIID           The IID to query for
+     * @param {nsIIDRef[]} aInterfaces  The interfaces that this object implements
+     * @return {nsQIResult}             The object queried for aIID
      */
     generateClassQI: function(aGlobal, aIID, aInterfaces) {
-        Object.defineProperty(aGlobal, "QueryInterface", { value: cal.generateQI(aInterfaces) });
+        const generatedQI = aInterfaces.length > 1
+            ? cal.generateQI(aInterfaces)
+            : ChromeUtils.generateQI(aInterfaces);
+        Object.defineProperty(aGlobal, "QueryInterface", { value: generatedQI });
         return aGlobal.QueryInterface(aIID);
     },
 
 
     /**
      * Generates the QueryInterface function. This is a replacement for XPCOMUtils.generateQI, which
      * is being replaced. Unfortunately Lightning's code depends on some of its classes providing
      * nsIClassInfo, which causes xpconnect/xpcom to make all methods available, e.g. for an event
--- a/calendar/base/themes/common/calendar-task-tree.css
+++ b/calendar/base/themes/common/calendar-task-tree.css
@@ -70,11 +70,11 @@
     -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);
 }
 
-treecol[anonid="calendar-task-tree-col-percentcomplete"] {
-  text-align: end;
+treecol.calendar-task-tree-col-percentcomplete {
+    text-align: end;
 }
--- a/calendar/base/themes/osx/calendar-task-tree.css
+++ b/calendar/base/themes/osx/calendar-task-tree.css
@@ -1,16 +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/. */
 
 @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-checkboximg {
+.calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed) {
     list-style-image: url(chrome://calendar/skin/checkbox-images.png);
     width: 16px;
     height: 16px;
 }
 
 @media (-moz-mac-graphite-theme) {
     .calendar-task-tree > treechildren::-moz-tree-image(calendar-task-tree-col-completed),
     .calendar-task-tree-col-completed-checkboximg {
--- a/calendar/lightning/content/imip-bar-overlay.xul
+++ b/calendar/lightning/content/imip-bar-overlay.xul
@@ -3,18 +3,16 @@
    - License, v. 2.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 % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
     %lightningDTD;
 ]>
 
-<?xml-stylesheet href="chrome://lightning/content/lightning-widgets.css" type="text/css"?>
-
 <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
 
     <script type="application/javascript"
             src="chrome://lightning/content/lightning-utils.js"/>
     <script type="application/javascript"
             src="chrome://lightning/content/imip-bar.js"/>
     <script type="application/javascript"
             src="chrome://calendar/content/calendar-management.js"/>
deleted file mode 100644
--- a/calendar/lightning/content/lightning-widgets.css
+++ /dev/null
@@ -1,7 +0,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/. */
-
-#calendar-task-tree-detail > calendar-task-tree {
-  -moz-binding: url(chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree-todaypane);
-}
--- a/calendar/lightning/jar.mn
+++ b/calendar/lightning/jar.mn
@@ -35,17 +35,16 @@ lightning.jar:
     content/lightning/lightning-calendar-creation.js       (content/lightning-calendar-creation.js)
     content/lightning/lightning-calendar-properties.xul    (content/lightning-calendar-properties.xul)
     content/lightning/lightning-calendar-properties.js     (content/lightning-calendar-properties.js)
     content/lightning/lightning-invitation.xhtml           (content/lightning-invitation.xhtml)
     content/lightning/lightning-menus.xul                  (content/lightning-menus.xul)
     content/lightning/lightning-migration.xul              (content/lightning-migration.xul)
     content/lightning/lightning-toolbar.xul                (content/lightning-toolbar.xul)
     content/lightning/lightning-utils.js                   (content/lightning-utils.js)
-    content/lightning/lightning-widgets.css                (content/lightning-widgets.css)
     content/lightning/messenger-overlay-accountCentral.xul (content/messenger-overlay-accountCentral.xul)
     content/lightning/messenger-overlay-messageWindow.xul  (content/messenger-overlay-messageWindow.xul)
     content/lightning/messenger-overlay-sidebar.js         (content/messenger-overlay-sidebar.js)
     content/lightning/messenger-overlay-sidebar.xul        (content/messenger-overlay-sidebar.xul)
     content/lightning/messenger-overlay-preferences.js     (content/messenger-overlay-preferences.js)
 *   content/lightning/messenger-overlay-preferences.xul    (content/messenger-overlay-preferences.xul)
     content/lightning/suite-overlay-addons.xul             (content/suite-overlay-addons.xul)
     content/lightning/suite-overlay-preferences.xul        (content/suite-overlay-preferences.xul)
--- a/calendar/test/mozmill/testBasicFunctionality.js
+++ b/calendar/test/mozmill/testBasicFunctionality.js
@@ -71,18 +71,17 @@ function testSmokeTest() {
     controller.click(eid("task-tab-button"));
     // Should be possible to filter today's tasks.
     controller.waitForElement(eid("opt_today_filter"));
     // Check for task add button.
     controller.assertNode(eid("calendar-add-task-button"));
     // Check for filtered tasks list.
     controller.assertNode(lookup(`
         ${CALENDAR_PANEL}/id("calendarDisplayDeck")/id("calendar-task-box")/[1]/
-        id("calendar-task-tree")/anon({"anonid":"calendar-task-tree"})/
-        {"tooltip":"taskTreeTooltip"}
+        id("calendar-task-tree")/{"class":"calendar-task-treechildren"}
     `));
 
     // Create test calendar.
     plan_for_modal_dialog("Calendar:NewCalendarWizard", (wizard) => {
         handleNewCalendarWizard(wizard, CALENDARNAME);
     });
     let calendarList = lookup(CALENDARLIST);
     // Double click on bottom left.
--- a/calendar/test/mozmill/views/testTaskView.js
+++ b/calendar/test/mozmill/views/testTaskView.js
@@ -34,20 +34,17 @@ function setupModule(module) {
 
     createCalendar(controller, CALENDARNAME);
 }
 
 // Mozmill doesn't support trees yet, therefore completed checkbox and line-through style are not
 // checked.
 function testTaskView() {
     // paths
-    let treeChildren = `
-        ${TASK_VIEW}/[1]/id("calendar-task-tree")/anon({"anonid":"calendar-task-tree"})/
-        {"tooltip":"taskTreeTooltip"}
-    `;
+    let treeChildren = `${TASK_VIEW}/[1]/id("calendar-task-tree")/{"class":"calendar-task-treechildren"}`;
     let taskTree = TASK_VIEW + '[1]/id("calendar-task-tree")';
     let toolTip = '/id("messengerWindow")/id("calendar-popupset")/id("taskTreeTooltip")';
     let toolTipGrid = toolTip + '/{"class":"tooltipBox"}/{"class":"tooltipHeaderGrid"}/';
 
     // Open task view.
     controller.click(eid("task-tab-button"));
     sleep();
 
--- a/mail/installer/allowed-dupes.mn
+++ b/mail/installer/allowed-dupes.mn
@@ -60,18 +60,16 @@ extensions/{e2fda1a4-762b-4020-b5ad-a41d
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/imip.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/imip.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-event-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-event-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-invitations-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-invitations-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-toolbar.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/osx/lightning/lightning-toolbar.css
-extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-widgets.css
-extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/lightning-widgets.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/accountCentral.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/accountCentral.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-daypicker.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-daypicker.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-alarm-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-alarm-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-management.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-management.css
@@ -95,18 +93,16 @@ distribution/extensions/{e2fda1a4-762b-4
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/imip.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/imip.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-event-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-event-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-invitations-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-invitations-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-toolbar.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/osx/lightning/lightning-toolbar.css
-distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-widgets.css
-distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/lightning-widgets.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/accountCentral.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/accountCentral.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-daypicker.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-daypicker.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-alarm-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-alarm-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-management.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-management.css
--- a/suite/installer/allowed-dupes.mn
+++ b/suite/installer/allowed-dupes.mn
@@ -292,18 +292,16 @@ extensions/{e2fda1a4-762b-4020-b5ad-a41d
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-alarm-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-alarm-dialog.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-daypicker.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-daypicker.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-management.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-management.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/accountCentral.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/accountCentral.css
-extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-widgets.css
-extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/lightning-widgets.css
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/calendar-js/calFilter.js
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/calendar/content/calendar/calFilter.js
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/calendar-js/calUtils.js
 extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/calendar/content/calendar/calUtils.js
 # Lightning Release and Beta
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/imip.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/imip.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-invitations-dialog.css
@@ -311,18 +309,16 @@ distribution/extensions/{e2fda1a4-762b-4
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-alarm-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-alarm-dialog.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-daypicker.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-daypicker.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/calendar/calendar-management.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/calendar/calendar-management.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/accountCentral.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/accountCentral.css
-distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/linux/lightning/lightning-widgets.css
-distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/skin/windows/lightning/lightning-widgets.css
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/calendar-js/calFilter.js
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/calendar/content/calendar/calFilter.js
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/calendar-js/calUtils.js
 distribution/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/chrome/calendar/content/calendar/calUtils.js
 
 # DOMi
 distribution/extensions/inspector@mozilla.org/chrome/inspector/skin/classic/inspector/titledsplitter-close.gif
 distribution/extensions/inspector@mozilla.org/chrome/inspector/skin/modern/inspector/titledsplitter-close.gif