bug 1446101 - Part 1: Extract a common TabsListBase class from TabsPopup r?dao draft
authorMark Striemer <mstriemer@mozilla.com>
Tue, 15 May 2018 18:40:07 -0500
changeset 809606 d84df0e392d2b55f82bc5e3b1fe776ca6e6d7b1e
parent 809605 39936912aca97f641840e8d41da261932143de36
child 809607 e88e11bada22be57273b8c03c4462fc34546d31d
push id113724
push userbmo:mstriemer@mozilla.com
push dateFri, 22 Jun 2018 14:48:42 +0000
reviewersdao
bugs1446101
milestone62.0a1
bug 1446101 - Part 1: Extract a common TabsListBase class from TabsPopup r?dao MozReview-Commit-ID: KPvk0fVoGzG
browser/modules/TabsPopup.jsm
--- a/browser/modules/TabsPopup.jsm
+++ b/browser/modules/TabsPopup.jsm
@@ -1,17 +1,139 @@
 /* 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/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["TabsPopup"];
+const NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-class TabsPopup {
+class TabsListBase {
+  constructor({className, filterFn, insertBefore, onPopulate, containerNode}) {
+    this.className = className;
+    this.filterFn = filterFn;
+    this.insertBefore = insertBefore;
+    this.onPopulate = onPopulate;
+    this.containerNode = containerNode;
+
+    this.doc = containerNode.ownerDocument;
+    this.gBrowser = this.doc.defaultView.gBrowser;
+    this.tabToElement = new Map();
+    this.listenersRegistered = false;
+  }
+
+  get rows() {
+    return this.tabToElement.values();
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "TabAttrModified":
+        this._tabAttrModified(event.target);
+        break;
+      case "TabClose":
+        this._tabClose(event.target);
+        break;
+      case "command":
+        this._selectTab(event.target.tab);
+        break;
+    }
+  }
+
+  _selectTab(tab) {
+    if (this.gBrowser.selectedTab != tab) {
+      this.gBrowser.selectedTab = tab;
+    } else {
+      this.gBrowser.tabContainer._handleTabSelect();
+    }
+  }
+
+  /*
+   * Populate the popup with menuitems and setup the listeners.
+   */
+  _populate(event) {
+    let fragment = this.doc.createDocumentFragment();
+
+    for (let tab of this.gBrowser.tabs) {
+      if (this.filterFn(tab)) {
+        let row = this._createRow(tab);
+        row.tab = tab;
+        row.addEventListener("command", this);
+        this.tabToElement.set(tab, row);
+        if (this.className) {
+          row.classList.add(this.className);
+        }
+
+        fragment.appendChild(row);
+      }
+    }
+
+    if (this.insertBefore) {
+      this.insertBefore.parentNode.insertBefore(fragment, this.insertBefore);
+    } else {
+      this.containerNode.appendChild(fragment);
+    }
+
+    this._setupListeners();
+
+    if (typeof this.onPopulate == "function") {
+      this.onPopulate(event);
+    }
+  }
+
+  /*
+   * Remove the menuitems from the DOM, cleanup internal state and listeners.
+   */
+  _cleanup() {
+    for (let item of this.rows) {
+      item.remove();
+    }
+    this.tabToElement = new Map();
+    this._cleanupListeners();
+  }
+
+  _setupListeners() {
+    this.listenersRegistered = true;
+    this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
+    this.gBrowser.tabContainer.addEventListener("TabClose", this);
+  }
+
+  _cleanupListeners() {
+    this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
+    this.gBrowser.tabContainer.removeEventListener("TabClose", this);
+    this.listenersRegistered = false;
+  }
+
+  _tabAttrModified(tab) {
+    let item = this.tabToElement.get(tab);
+    if (item) {
+      if (!this.filterFn(tab)) {
+        // If the tab is no longer in this set of tabs, hide the item.
+        this._removeItem(item, tab);
+      } else {
+        this._setRowAttributes(item, tab);
+      }
+    }
+  }
+
+  _tabClose(tab) {
+    let item = this.tabToElement.get(tab);
+    if (item) {
+      this._removeItem(item, tab);
+    }
+  }
+
+  _removeItem(item, tab) {
+    this.tabToElement.delete(tab);
+    item.remove();
+  }
+}
+
+class TabsPopup extends TabsListBase {
   /*
    * Handle menuitem rows for tab objects in a menupopup.
    *
    * @param {object} opts Options for configuring this instance.
    * @param {string} opts.className
    *                 An optional class name to be added to menuitem elements.
    * @param {function} opts.filterFn
    *                   A function to filter which tabs are added to the popup.
@@ -21,151 +143,56 @@ class TabsPopup {
    * @param {function} opts.onPopulate
    *                   An optional function that will be called with the
    *                   popupshowing event that caused the menu to be populated.
    * @param {object} opts.popup
    *                 A menupopup element to populate and register the show/hide
    *                 listeners on.
    */
   constructor({className, filterFn, insertBefore, onPopulate, popup}) {
-    this.className = className;
-    this.filterFn = filterFn;
-    this.insertBefore = insertBefore;
-    this.onPopulate = onPopulate;
-    this.popup = popup;
-
-    this.doc = popup.ownerDocument;
-    this.gBrowser = this.doc.defaultView.gBrowser;
-    this.tabToMenuitem = new Map();
-    this.popup.addEventListener("popupshowing", this);
+    super({className, filterFn, insertBefore, onPopulate, containerNode: popup});
+    this.containerNode.addEventListener("popupshowing", this);
   }
 
   handleEvent(event) {
     switch (event.type) {
-      case "TabAttrModified":
-        this._tabAttrModified(event.target);
-        break;
-      case "TabClose":
-        this._tabClose(event.target);
-        break;
-      case "command":
-        this._handleCommand(event.target.tab);
-        break;
       case "popuphidden":
-        if (event.target == this.popup) {
+        if (event.target == this.containerNode) {
           this._cleanup();
         }
         break;
       case "popupshowing":
-        if (event.target == this.popup) {
-          this._populate();
-          if (typeof this.onPopulate == "function") {
-            this.onPopulate(event);
-          }
+        if (event.target == this.containerNode) {
+          this._populate(event);
         }
         break;
+      default:
+        super.handleEvent(event);
+        break;
     }
   }
 
-  /*
-   * Populate the popup with menuitems and setup the listeners.
-   */
-  _populate() {
-    let fragment = this.doc.createDocumentFragment();
-
-    for (let tab of this.gBrowser.tabs) {
-      if (this.filterFn(tab)) {
-        fragment.appendChild(this._createMenuitem(tab));
-      }
-    }
-
-    if (this.insertBefore) {
-      this.popup.insertBefore(fragment, this.insertBefore);
-    } else {
-      this.popup.appendChild(fragment);
-    }
-
-    this._setupListeners();
-  }
-
-  /*
-   * Remove the menuitems from the DOM, cleanup internal state and listeners.
-   */
-  _cleanup() {
-    for (let item of this.tabToMenuitem.values()) {
-      item.remove();
-    }
-    this.tabToMenuitem = new Map();
-    this._cleanupListeners();
-  }
-
   _setupListeners() {
-    this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
-    this.gBrowser.tabContainer.addEventListener("TabClose", this);
-    this.popup.addEventListener("popuphidden", this);
+    super._setupListeners();
+    this.containerNode.addEventListener("popuphidden", this);
   }
 
   _cleanupListeners() {
-    this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
-    this.gBrowser.tabContainer.removeEventListener("TabClose", this);
-    this.popup.removeEventListener("popuphidden", this);
-  }
-
-  _tabAttrModified(tab) {
-    let item = this.tabToMenuitem.get(tab);
-    if (item) {
-      if (!this.filterFn(tab)) {
-        // If the tab is no longer in this set of tabs, hide the item.
-        this._removeItem(item, tab);
-      } else {
-        this._setMenuitemAttributes(item, tab);
-      }
-    }
-  }
-
-  _tabClose(tab) {
-    let item = this.tabToMenuitem.get(tab);
-    if (item) {
-      this._removeItem(item, tab);
-    }
+    super._cleanupListeners();
+    this.containerNode.removeEventListener("popuphidden", this);
   }
 
-  _removeItem(item, tab) {
-    this.tabToMenuitem.delete(tab);
-    item.remove();
-  }
-
-  _handleCommand(tab) {
-    if (this.gBrowser.selectedTab != tab) {
-      this.gBrowser.selectedTab = tab;
-    } else {
-      this.gBrowser.tabContainer._handleTabSelect();
-    }
-  }
-
-  _createMenuitem(tab) {
-    let item = this.doc.createElementNS(
-      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
-      "menuitem");
-    item.tab = tab;
-
+  _createRow(tab) {
+    let item = this.doc.createElementNS(NSXUL, "menuitem");
     item.setAttribute("class", "menuitem-iconic menuitem-with-favicon");
-    if (this.className) {
-      item.classList.add(this.className);
-    }
-    this._setMenuitemAttributes(item, tab);
-
-    this.tabToMenuitem.set(tab, item);
-
-    item.addEventListener("command", this);
-
+    this._setRowAttributes(item, tab);
     return item;
   }
 
-  _setMenuitemAttributes(item, tab) {
+  _setRowAttributes(item, tab) {
     item.setAttribute("label", tab.label);
     item.setAttribute("crop", "end");
 
     if (tab.hasAttribute("busy")) {
       item.setAttribute("busy", tab.getAttribute("busy"));
       item.removeAttribute("iconloadingprincipal");
       item.removeAttribute("image");
     } else {