mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1002 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1002 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
/**
 | 
						|
 * SidebarController handles logic such as toggling sidebar panels,
 | 
						|
 * dynamically adding menubar menu items for the View -> Sidebar menu,
 | 
						|
 * and provides APIs for sidebar extensions, etc.
 | 
						|
 */
 | 
						|
var SidebarController = {
 | 
						|
  makeSidebar({ elementId, ...rest }) {
 | 
						|
    return {
 | 
						|
      get sourceL10nEl() {
 | 
						|
        return document.getElementById(elementId);
 | 
						|
      },
 | 
						|
      get title() {
 | 
						|
        return document.getElementById(elementId).getAttribute("label");
 | 
						|
      },
 | 
						|
      ...rest,
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  get sidebars() {
 | 
						|
    if (this._sidebars) {
 | 
						|
      return this._sidebars;
 | 
						|
    }
 | 
						|
 | 
						|
    this._sidebars = new Map([
 | 
						|
      [
 | 
						|
        "viewHistorySidebar",
 | 
						|
        this.makeSidebar({
 | 
						|
          elementId: "sidebar-switcher-history",
 | 
						|
          url: this.sidebarRevampEnabled
 | 
						|
            ? "chrome://browser/content/sidebar/sidebar-history.html"
 | 
						|
            : "chrome://browser/content/places/historySidebar.xhtml",
 | 
						|
          menuId: "menu_historySidebar",
 | 
						|
          triggerButtonId: "appMenuViewHistorySidebar",
 | 
						|
          keyId: "key_gotoHistory",
 | 
						|
          menuL10nId: "menu-view-history-button",
 | 
						|
          revampL10nId: "sidebar-menu-history",
 | 
						|
          icon: `url("chrome://browser/content/firefoxview/view-history.svg")`,
 | 
						|
        }),
 | 
						|
      ],
 | 
						|
      [
 | 
						|
        "viewTabsSidebar",
 | 
						|
        this.makeSidebar({
 | 
						|
          elementId: "sidebar-switcher-tabs",
 | 
						|
          url: this.sidebarRevampEnabled
 | 
						|
            ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html"
 | 
						|
            : "chrome://browser/content/syncedtabs/sidebar.xhtml",
 | 
						|
          menuId: "menu_tabsSidebar",
 | 
						|
          classAttribute: "sync-ui-item",
 | 
						|
          menuL10nId: "menu-view-synced-tabs-sidebar",
 | 
						|
          revampL10nId: "sidebar-menu-synced-tabs",
 | 
						|
          icon: `url("chrome://browser/content/firefoxview/view-syncedtabs.svg")`,
 | 
						|
        }),
 | 
						|
      ],
 | 
						|
    ]);
 | 
						|
 | 
						|
    if (!this.sidebarRevampEnabled) {
 | 
						|
      this._sidebars.set(
 | 
						|
        "viewBookmarksSidebar",
 | 
						|
        this.makeSidebar({
 | 
						|
          elementId: "sidebar-switcher-bookmarks",
 | 
						|
          url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
 | 
						|
          menuId: "menu_bookmarksSidebar",
 | 
						|
          keyId: "viewBookmarksSidebarKb",
 | 
						|
          menuL10nId: "menu-view-bookmarks",
 | 
						|
          revampL10nId: "sidebar-menu-bookmarks",
 | 
						|
        })
 | 
						|
      );
 | 
						|
      if (this.megalistEnabled) {
 | 
						|
        this._sidebars.set(
 | 
						|
          "viewMegalistSidebar",
 | 
						|
          this.makeSidebar({
 | 
						|
            elementId: "sidebar-switcher-megalist",
 | 
						|
            url: "chrome://global/content/megalist/megalist.html",
 | 
						|
            menuId: "menu_megalistSidebar",
 | 
						|
            menuL10nId: "menu-view-megalist-sidebar",
 | 
						|
            revampL10nId: "sidebar-menu-megalist",
 | 
						|
          })
 | 
						|
        );
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      this._sidebars.set(
 | 
						|
        "viewCustomizeSidebar",
 | 
						|
        this.makeSidebar({
 | 
						|
          url: "chrome://browser/content/sidebar/sidebar-customize.html",
 | 
						|
          revampL10nId: "sidebar-menu-customize",
 | 
						|
          icon: `url("chrome://browser/skin/preferences/category-general.svg")`,
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return this._sidebars;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns a map of tools and extensions for use in the sidebar
 | 
						|
   */
 | 
						|
  get toolsAndExtensions() {
 | 
						|
    if (this._toolsAndExtensions) {
 | 
						|
      return this._toolsAndExtensions;
 | 
						|
    }
 | 
						|
 | 
						|
    this._toolsAndExtensions = new Map();
 | 
						|
    this.getSidebarPanels(["viewHistorySidebar", "viewTabsSidebar"]).forEach(
 | 
						|
      tool => {
 | 
						|
        this._toolsAndExtensions.set(tool.commandID, {
 | 
						|
          view: tool.commandID,
 | 
						|
          icon: tool.icon,
 | 
						|
          l10nId: tool.revampL10nId,
 | 
						|
          disabled: false,
 | 
						|
        });
 | 
						|
      }
 | 
						|
    );
 | 
						|
    this.getExtensions().forEach(extension => {
 | 
						|
      this._toolsAndExtensions.set(extension.commandID, {
 | 
						|
        view: extension.commandID,
 | 
						|
        extensionId: extension.extensionId,
 | 
						|
        icon: extension.icon,
 | 
						|
        tooltiptext: extension.label,
 | 
						|
        disabled: false,
 | 
						|
      });
 | 
						|
    });
 | 
						|
    return this._toolsAndExtensions;
 | 
						|
  },
 | 
						|
 | 
						|
  // Avoid getting the browser element from init() to avoid triggering the
 | 
						|
  // <browser> constructor during startup if the sidebar is hidden.
 | 
						|
  get browser() {
 | 
						|
    if (this._browser) {
 | 
						|
      return this._browser;
 | 
						|
    }
 | 
						|
    return (this._browser = document.getElementById("sidebar"));
 | 
						|
  },
 | 
						|
  POSITION_START_PREF: "sidebar.position_start",
 | 
						|
  DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
 | 
						|
 | 
						|
  // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
 | 
						|
  // and isn't persisted across windows
 | 
						|
  lastOpenedId: null,
 | 
						|
 | 
						|
  _box: null,
 | 
						|
  // The constructor of this label accesses the browser element due to the
 | 
						|
  // control="sidebar" attribute, so avoid getting this label during startup.
 | 
						|
  get _title() {
 | 
						|
    if (this.__title) {
 | 
						|
      return this.__title;
 | 
						|
    }
 | 
						|
    return (this.__title = document.getElementById("sidebar-title"));
 | 
						|
  },
 | 
						|
  _splitter: null,
 | 
						|
  _reversePositionButton: null,
 | 
						|
  _switcherPanel: null,
 | 
						|
  _switcherTarget: null,
 | 
						|
  _switcherArrow: null,
 | 
						|
  _inited: false,
 | 
						|
 | 
						|
  /**
 | 
						|
   * @type {MutationObserver | null}
 | 
						|
   */
 | 
						|
  _observer: null,
 | 
						|
 | 
						|
  _initDeferred: Promise.withResolvers(),
 | 
						|
 | 
						|
  get promiseInitialized() {
 | 
						|
    return this._initDeferred.promise;
 | 
						|
  },
 | 
						|
 | 
						|
  get initialized() {
 | 
						|
    return this._inited;
 | 
						|
  },
 | 
						|
 | 
						|
  async init() {
 | 
						|
    this._box = document.getElementById("sidebar-box");
 | 
						|
    this._splitter = document.getElementById("sidebar-splitter");
 | 
						|
    this._reversePositionButton = document.getElementById(
 | 
						|
      "sidebar-reverse-position"
 | 
						|
    );
 | 
						|
    this._switcherPanel = document.getElementById("sidebarMenu-popup");
 | 
						|
    this._switcherTarget = document.getElementById("sidebar-switcher-target");
 | 
						|
    this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
 | 
						|
 | 
						|
    const menubar = document.getElementById("viewSidebarMenu");
 | 
						|
    for (const [commandID, sidebar] of this.sidebars.entries()) {
 | 
						|
      if (!Object.hasOwn(sidebar, "extensionId")) {
 | 
						|
        // registerExtension() already creates menu items for extensions.
 | 
						|
        const menuitem = this.createMenuItem(commandID, sidebar);
 | 
						|
        menubar.appendChild(menuitem);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.sidebarRevampEnabled) {
 | 
						|
      await import("chrome://browser/content/sidebar/sidebar-main.mjs");
 | 
						|
      document.getElementById("sidebar-main").hidden = false;
 | 
						|
      document.getElementById("sidebar-header").hidden = true;
 | 
						|
    } else {
 | 
						|
      this._switcherTarget.addEventListener("command", () => {
 | 
						|
        this.toggleSwitcherPanel();
 | 
						|
      });
 | 
						|
      this._switcherTarget.addEventListener("keydown", event => {
 | 
						|
        this.handleKeydown(event);
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    this._inited = true;
 | 
						|
 | 
						|
    Services.obs.addObserver(this, "intl:app-locales-changed");
 | 
						|
 | 
						|
    this._initDeferred.resolve();
 | 
						|
  },
 | 
						|
 | 
						|
  toggleMegalistItem() {
 | 
						|
    const sideMenuPopupItem = document.getElementById(
 | 
						|
      "sidebar-switcher-megalist"
 | 
						|
    );
 | 
						|
    sideMenuPopupItem.style.display = this.megalistEnabled ? "" : "none";
 | 
						|
  },
 | 
						|
 | 
						|
  setMegalistMenubarVisibility(aEvent) {
 | 
						|
    const popup = aEvent.target;
 | 
						|
    if (popup != aEvent.currentTarget) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Show the megalist item if enabled
 | 
						|
    const megalistItem = popup.querySelector("#menu_megalistSidebar");
 | 
						|
    megalistItem.hidden = !this.megalistEnabled;
 | 
						|
  },
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    // If this is the last browser window, persist various values that should be
 | 
						|
    // remembered for after a restart / reopening a browser window.
 | 
						|
    let enumerator = Services.wm.getEnumerator("navigator:browser");
 | 
						|
    if (!enumerator.hasMoreElements()) {
 | 
						|
      let xulStore = Services.xulStore;
 | 
						|
 | 
						|
      xulStore.persist(this._box, "style");
 | 
						|
      xulStore.persist(this._title, "value");
 | 
						|
    }
 | 
						|
 | 
						|
    Services.obs.removeObserver(this, "intl:app-locales-changed");
 | 
						|
 | 
						|
    if (this._observer) {
 | 
						|
      this._observer.disconnect();
 | 
						|
      this._observer = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * The handler for Services.obs.addObserver.
 | 
						|
   */
 | 
						|
  observe(_subject, topic, _data) {
 | 
						|
    switch (topic) {
 | 
						|
      case "intl:app-locales-changed": {
 | 
						|
        if (this.isOpen) {
 | 
						|
          // The <tree> component used in history and bookmarks, but it does not
 | 
						|
          // support live switching the app locale. Reload the entire sidebar to
 | 
						|
          // invalidate any old text.
 | 
						|
          this.hide();
 | 
						|
          this.showInitially(this.lastOpenedId);
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Ensure the title stays in sync with the source element, which updates for
 | 
						|
   * l10n changes.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} [element]
 | 
						|
   */
 | 
						|
  observeTitleChanges(element) {
 | 
						|
    if (!element) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let observer = this._observer;
 | 
						|
    if (!observer) {
 | 
						|
      observer = new MutationObserver(() => {
 | 
						|
        this.title = this.sidebars.get(this.lastOpenedId).title;
 | 
						|
      });
 | 
						|
      // Re-use the observer.
 | 
						|
      this._observer = observer;
 | 
						|
    }
 | 
						|
    observer.disconnect();
 | 
						|
    observer.observe(element, {
 | 
						|
      attributes: true,
 | 
						|
      attributeFilter: ["label"],
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Opens the switcher panel if it's closed, or closes it if it's open.
 | 
						|
   */
 | 
						|
  toggleSwitcherPanel() {
 | 
						|
    if (
 | 
						|
      this._switcherPanel.state == "open" ||
 | 
						|
      this._switcherPanel.state == "showing"
 | 
						|
    ) {
 | 
						|
      this.hideSwitcherPanel();
 | 
						|
    } else if (this._switcherPanel.state == "closed") {
 | 
						|
      this.showSwitcherPanel();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles keydown on the the switcherTarget button
 | 
						|
   *
 | 
						|
   * @param  {Event} event
 | 
						|
   */
 | 
						|
  handleKeydown(event) {
 | 
						|
    switch (event.key) {
 | 
						|
      case "Enter":
 | 
						|
      case " ": {
 | 
						|
        this.toggleSwitcherPanel();
 | 
						|
        event.stopPropagation();
 | 
						|
        event.preventDefault();
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "Escape": {
 | 
						|
        this.hideSwitcherPanel();
 | 
						|
        event.stopPropagation();
 | 
						|
        event.preventDefault();
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  hideSwitcherPanel() {
 | 
						|
    this._switcherPanel.hidePopup();
 | 
						|
  },
 | 
						|
 | 
						|
  showSwitcherPanel() {
 | 
						|
    this.toggleMegalistItem();
 | 
						|
    this._switcherPanel.addEventListener(
 | 
						|
      "popuphiding",
 | 
						|
      () => {
 | 
						|
        this._switcherTarget.classList.remove("active");
 | 
						|
        this._switcherTarget.setAttribute("aria-expanded", false);
 | 
						|
      },
 | 
						|
      { once: true }
 | 
						|
    );
 | 
						|
 | 
						|
    // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
 | 
						|
    let label =
 | 
						|
      this._positionStart == RTL_UI
 | 
						|
        ? gNavigatorBundle.getString("sidebar.moveToLeft")
 | 
						|
        : gNavigatorBundle.getString("sidebar.moveToRight");
 | 
						|
    this._reversePositionButton.setAttribute("label", label);
 | 
						|
 | 
						|
    // Open the sidebar switcher popup, anchored off the switcher toggle
 | 
						|
    this._switcherPanel.hidden = false;
 | 
						|
    this._switcherPanel.openPopup(this._switcherTarget);
 | 
						|
 | 
						|
    this._switcherTarget.classList.add("active");
 | 
						|
    this._switcherTarget.setAttribute("aria-expanded", true);
 | 
						|
  },
 | 
						|
 | 
						|
  updateShortcut({ keyId }) {
 | 
						|
    let menuitem = this._switcherPanel?.querySelector(`[key="${keyId}"]`);
 | 
						|
    if (!menuitem) {
 | 
						|
      // If the menu item doesn't exist yet then the accel text will be set correctly
 | 
						|
      // upon creation so there's nothing to do now.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    menuitem.removeAttribute("acceltext");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Change the pref that will trigger a call to setPosition
 | 
						|
   */
 | 
						|
  reversePosition() {
 | 
						|
    Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Read the positioning pref and position the sidebar and the splitter
 | 
						|
   * appropriately within the browser container.
 | 
						|
   */
 | 
						|
  setPosition() {
 | 
						|
    // First reset all ordinals to match DOM ordering.
 | 
						|
    let browser = document.getElementById("browser");
 | 
						|
    [...browser.children].forEach((node, i) => {
 | 
						|
      node.style.order = i + 1;
 | 
						|
    });
 | 
						|
    let sidebarMain = document.querySelector("sidebar-main");
 | 
						|
 | 
						|
    if (!this._positionStart) {
 | 
						|
      // DOM ordering is:     sidebar-main |  sidebar-box  | splitter |   appcontent  |
 | 
						|
      // Want to display as:  |   appcontent  | splitter |  sidebar-box  | sidebar-main
 | 
						|
      // So we just swap box and appcontent ordering and move sidebar-main to the end
 | 
						|
      let appcontent = document.getElementById("appcontent");
 | 
						|
      let boxOrdinal = this._box.style.order;
 | 
						|
      this._box.style.order = appcontent.style.order;
 | 
						|
 | 
						|
      appcontent.style.order = boxOrdinal;
 | 
						|
      // the launcher should be on the right of the sidebar-box
 | 
						|
      sidebarMain.style.order = parseInt(this._box.style.order) + 1;
 | 
						|
      // Indicate we've switched ordering to the box
 | 
						|
      this._box.setAttribute("positionend", true);
 | 
						|
      sidebarMain.setAttribute("positionend", true);
 | 
						|
    } else {
 | 
						|
      this._box.removeAttribute("positionend");
 | 
						|
      sidebarMain.removeAttribute("positionend");
 | 
						|
    }
 | 
						|
 | 
						|
    this.hideSwitcherPanel();
 | 
						|
 | 
						|
    let content = SidebarController.browser.contentWindow;
 | 
						|
    if (content && content.updatePosition) {
 | 
						|
      content.updatePosition();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try and adopt the status of the sidebar from another window.
 | 
						|
   *
 | 
						|
   * @param {Window} sourceWindow - Window to use as a source for sidebar status.
 | 
						|
   * @returns {boolean} true if we adopted the state, or false if the caller should
 | 
						|
   * initialize the state itself.
 | 
						|
   */
 | 
						|
  adoptFromWindow(sourceWindow) {
 | 
						|
    // If the opener had a sidebar, open the same sidebar in our window.
 | 
						|
    // The opener can be the hidden window too, if we're coming from the state
 | 
						|
    // where no windows are open, and the hidden window has no sidebar box.
 | 
						|
    let sourceUI = sourceWindow.SidebarController;
 | 
						|
    if (!sourceUI || !sourceUI._box) {
 | 
						|
      // no source UI or no _box means we also can't adopt the state.
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Set sidebar command even if hidden, so that we keep the same sidebar
 | 
						|
    // even if it's currently closed.
 | 
						|
    let commandID = sourceUI._box.getAttribute("sidebarcommand");
 | 
						|
    if (commandID) {
 | 
						|
      this._box.setAttribute("sidebarcommand", commandID);
 | 
						|
    }
 | 
						|
 | 
						|
    if (sourceUI._box.hidden) {
 | 
						|
      // just hidden means we have adopted the hidden state.
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // dynamically generated sidebars will fail this check, but we still
 | 
						|
    // consider it adopted.
 | 
						|
    if (!this.sidebars.has(commandID)) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    this._box.style.width = sourceUI._box.getBoundingClientRect().width + "px";
 | 
						|
    this.showInitially(commandID);
 | 
						|
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  windowPrivacyMatches(w1, w2) {
 | 
						|
    return (
 | 
						|
      PrivateBrowsingUtils.isWindowPrivate(w1) ===
 | 
						|
      PrivateBrowsingUtils.isWindowPrivate(w2)
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If loading a sidebar was delayed on startup, start the load now.
 | 
						|
   */
 | 
						|
  startDelayedLoad() {
 | 
						|
    let sourceWindow = window.opener;
 | 
						|
    // No source window means this is the initial window.  If we're being
 | 
						|
    // opened from another window, check that it is one we might open a sidebar
 | 
						|
    // for.
 | 
						|
    if (sourceWindow) {
 | 
						|
      if (
 | 
						|
        sourceWindow.closed ||
 | 
						|
        sourceWindow.location.protocol != "chrome:" ||
 | 
						|
        !this.windowPrivacyMatches(sourceWindow, window)
 | 
						|
      ) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // Try to adopt the sidebar state from the source window
 | 
						|
      if (this.adoptFromWindow(sourceWindow)) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If we're not adopting settings from a parent window, set them now.
 | 
						|
    let wasOpen = this._box.getAttribute("checked");
 | 
						|
    if (!wasOpen) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let commandID = this._box.getAttribute("sidebarcommand");
 | 
						|
    if (commandID && this.sidebars.has(commandID)) {
 | 
						|
      this.showInitially(commandID);
 | 
						|
    } else {
 | 
						|
      this._box.removeAttribute("checked");
 | 
						|
      // Remove the |sidebarcommand| attribute, because the element it
 | 
						|
      // refers to no longer exists, so we should assume this sidebar
 | 
						|
      // panel has been uninstalled. (249883)
 | 
						|
      // We use setAttribute rather than removeAttribute so it persists
 | 
						|
      // correctly.
 | 
						|
      this._box.setAttribute("sidebarcommand", "");
 | 
						|
      // On a startup in which the startup cache was invalidated (e.g. app update)
 | 
						|
      // extensions will not be started prior to delayedLoad, thus the
 | 
						|
      // sidebarcommand element will not exist yet.  Store the commandID so
 | 
						|
      // extensions may reopen if necessary.  A startup cache invalidation
 | 
						|
      // can be forced (for testing) by deleting compatibility.ini from the
 | 
						|
      // profile.
 | 
						|
      this.lastOpenedId = commandID;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fire a "SidebarShown" event on the sidebar to give any interested parties
 | 
						|
   * a chance to update the button or whatever.
 | 
						|
   */
 | 
						|
  _fireShowEvent() {
 | 
						|
    let event = new CustomEvent("SidebarShown", { bubbles: true });
 | 
						|
    this._switcherTarget.dispatchEvent(event);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
 | 
						|
   * a chance to adjust focus as needed. An additional event is needed, because
 | 
						|
   * we don't want to focus the sidebar when it's opened on startup or in a new
 | 
						|
   * window, only when the user opens the sidebar.
 | 
						|
   */
 | 
						|
  _fireFocusedEvent() {
 | 
						|
    let event = new CustomEvent("SidebarFocused", { bubbles: true });
 | 
						|
    this.browser.contentWindow.dispatchEvent(event);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * True if the sidebar is currently open.
 | 
						|
   */
 | 
						|
  get isOpen() {
 | 
						|
    return !this._box.hidden;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * The ID of the current sidebar.
 | 
						|
   */
 | 
						|
  get currentID() {
 | 
						|
    return this.isOpen ? this._box.getAttribute("sidebarcommand") : "";
 | 
						|
  },
 | 
						|
 | 
						|
  get title() {
 | 
						|
    return this._title.value;
 | 
						|
  },
 | 
						|
 | 
						|
  set title(value) {
 | 
						|
    this._title.value = value;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
 | 
						|
   * with a different commandID, then the sidebar will be opened using the
 | 
						|
   * specified commandID. Otherwise the sidebar will be hidden.
 | 
						|
   *
 | 
						|
   * @param  {string}  commandID     ID of the sidebar.
 | 
						|
   * @param  {DOMNode} [triggerNode] Node, usually a button, that triggered the
 | 
						|
   *                                 visibility toggling of the sidebar.
 | 
						|
   * @returns {Promise}
 | 
						|
   */
 | 
						|
  toggle(commandID = this.lastOpenedId, triggerNode) {
 | 
						|
    if (
 | 
						|
      CustomizationHandler.isCustomizing() ||
 | 
						|
      CustomizationHandler.isExitingCustomizeMode
 | 
						|
    ) {
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
    // First priority for a default value is this.lastOpenedId which is set during show()
 | 
						|
    // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
 | 
						|
    // have a persisted command either, or the command doesn't exist anymore, then
 | 
						|
    // fallback to a default sidebar.
 | 
						|
    if (!commandID) {
 | 
						|
      commandID = this._box.getAttribute("sidebarcommand");
 | 
						|
    }
 | 
						|
    if (!commandID || !this.sidebars.has(commandID)) {
 | 
						|
      commandID = this.DEFAULT_SIDEBAR_ID;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.isOpen && commandID == this.currentID) {
 | 
						|
      this.hide(triggerNode);
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
    return this.show(commandID, triggerNode);
 | 
						|
  },
 | 
						|
 | 
						|
  _loadSidebarExtension(commandID) {
 | 
						|
    let sidebar = this.sidebars.get(commandID);
 | 
						|
    if (typeof sidebar.onload === "function") {
 | 
						|
      sidebar.onload();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets the disabled property for a tool when customizing sidebar options
 | 
						|
   *
 | 
						|
   * @param {string} commandID
 | 
						|
   */
 | 
						|
  toggleTool(commandID) {
 | 
						|
    let toggledTool = this.toolsAndExtensions.get(commandID);
 | 
						|
    toggledTool.disabled = !toggledTool.disabled;
 | 
						|
    if (!toggledTool.disabled) {
 | 
						|
      // If re-enabling tool, remove from the map and add it to the end
 | 
						|
      this.toolsAndExtensions.delete(commandID);
 | 
						|
      this.toolsAndExtensions.set(commandID, toggledTool);
 | 
						|
    }
 | 
						|
    window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
 | 
						|
  },
 | 
						|
 | 
						|
  addOrUpdateExtension(commandID, extension) {
 | 
						|
    if (this.toolsAndExtensions.has(commandID)) {
 | 
						|
      // Update existing extension
 | 
						|
      let extensionToUpdate = this.toolsAndExtensions.get(commandID);
 | 
						|
      extensionToUpdate.icon = extension.icon;
 | 
						|
      extensionToUpdate.tooltiptext = extension.label;
 | 
						|
      window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
 | 
						|
    } else {
 | 
						|
      // Add new extension
 | 
						|
      this.toolsAndExtensions.set(commandID, {
 | 
						|
        view: commandID,
 | 
						|
        extensionId: extension.extensionId,
 | 
						|
        icon: extension.icon,
 | 
						|
        tooltiptext: extension.label,
 | 
						|
        disabled: false,
 | 
						|
      });
 | 
						|
      window.dispatchEvent(new CustomEvent("SidebarItemAdded"));
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add menu items for a browser extension. Add the extension to the
 | 
						|
   * `sidebars` map.
 | 
						|
   *
 | 
						|
   * @param {string} commandID
 | 
						|
   * @param {object} props
 | 
						|
   */
 | 
						|
  registerExtension(commandID, props) {
 | 
						|
    const sidebar = {
 | 
						|
      title: props.title,
 | 
						|
      url: "chrome://browser/content/webext-panels.xhtml",
 | 
						|
      menuId: props.menuId,
 | 
						|
      switcherMenuId: `sidebarswitcher_menu_${commandID}`,
 | 
						|
      keyId: `ext-key-id-${commandID}`,
 | 
						|
      label: props.title,
 | 
						|
      icon: props.icon,
 | 
						|
      classAttribute: "menuitem-iconic webextension-menuitem",
 | 
						|
      // The following properties are specific to extensions
 | 
						|
      extensionId: props.extensionId,
 | 
						|
      onload: props.onload,
 | 
						|
    };
 | 
						|
    this.sidebars.set(commandID, sidebar);
 | 
						|
 | 
						|
    // Insert a menuitem for View->Show Sidebars.
 | 
						|
    const menuitem = this.createMenuItem(commandID, sidebar);
 | 
						|
    document.getElementById("viewSidebarMenu").appendChild(menuitem);
 | 
						|
    this.addOrUpdateExtension(commandID, sidebar);
 | 
						|
 | 
						|
    if (!this.sidebarRevampEnabled) {
 | 
						|
      // Insert a toolbarbutton for the sidebar dropdown selector.
 | 
						|
      let switcherMenuitem = this.createMenuItem(commandID, sidebar);
 | 
						|
      switcherMenuitem.setAttribute("id", sidebar.switcherMenuId);
 | 
						|
      switcherMenuitem.removeAttribute("type");
 | 
						|
 | 
						|
      let separator = document.getElementById("sidebar-extensions-separator");
 | 
						|
      separator.parentNode.insertBefore(switcherMenuitem, separator);
 | 
						|
    }
 | 
						|
    this._setExtensionAttributes(
 | 
						|
      commandID,
 | 
						|
      { icon: props.icon, label: props.title },
 | 
						|
      sidebar
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create a menu item for the View>Sidebars submenu in the menubar.
 | 
						|
   *
 | 
						|
   * @param {string} commandID
 | 
						|
   * @param {object} sidebar
 | 
						|
   * @returns {Element}
 | 
						|
   */
 | 
						|
  createMenuItem(commandID, sidebar) {
 | 
						|
    const menuitem = document.createXULElement("menuitem");
 | 
						|
    menuitem.setAttribute("id", sidebar.menuId);
 | 
						|
    menuitem.setAttribute("type", "checkbox");
 | 
						|
    menuitem.addEventListener("command", () => this.toggle(commandID));
 | 
						|
    if (sidebar.classAttribute) {
 | 
						|
      menuitem.setAttribute("class", sidebar.classAttribute);
 | 
						|
    }
 | 
						|
    if (sidebar.keyId) {
 | 
						|
      menuitem.setAttribute("key", sidebar.keyId);
 | 
						|
    }
 | 
						|
    if (sidebar.menuL10nId) {
 | 
						|
      menuitem.dataset.l10nId = sidebar.menuL10nId;
 | 
						|
    }
 | 
						|
    return menuitem;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Update attributes on all existing menu items for a browser extension.
 | 
						|
   *
 | 
						|
   * @param {string} commandID
 | 
						|
   * @param {object} attributes
 | 
						|
   * @param {string} attributes.icon
 | 
						|
   * @param {string} attributes.label
 | 
						|
   * @param {boolean} needsRefresh
 | 
						|
   */
 | 
						|
  setExtensionAttributes(commandID, attributes, needsRefresh) {
 | 
						|
    const sidebar = this.sidebars.get(commandID);
 | 
						|
    this._setExtensionAttributes(commandID, attributes, sidebar, needsRefresh);
 | 
						|
    this.addOrUpdateExtension(commandID, sidebar);
 | 
						|
  },
 | 
						|
 | 
						|
  _setExtensionAttributes(
 | 
						|
    commandID,
 | 
						|
    { icon, label },
 | 
						|
    sidebar,
 | 
						|
    needsRefresh = false
 | 
						|
  ) {
 | 
						|
    sidebar.icon = icon;
 | 
						|
    sidebar.label = label;
 | 
						|
 | 
						|
    const updateAttributes = el => {
 | 
						|
      el.style.setProperty("--webextension-menuitem-image", sidebar.icon);
 | 
						|
      el.setAttribute("label", sidebar.label);
 | 
						|
    };
 | 
						|
 | 
						|
    updateAttributes(document.getElementById(sidebar.menuId), sidebar);
 | 
						|
    const switcherMenu = document.getElementById(sidebar.switcherMenuId);
 | 
						|
    if (switcherMenu) {
 | 
						|
      updateAttributes(switcherMenu, sidebar);
 | 
						|
    }
 | 
						|
    if (this.initialized && this.currentID === commandID) {
 | 
						|
      // Update the sidebar if this extension is the current sidebar.
 | 
						|
      updateAttributes(this._switcherTarget, sidebar);
 | 
						|
      this.title = label;
 | 
						|
      if (this.isOpen && needsRefresh) {
 | 
						|
        this.show(commandID);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieve the list of registered browser extensions.
 | 
						|
   *
 | 
						|
   * @returns {Array}
 | 
						|
   */
 | 
						|
  getExtensions() {
 | 
						|
    const extensions = [];
 | 
						|
    for (const [commandID, sidebar] of this.sidebars.entries()) {
 | 
						|
      if (Object.hasOwn(sidebar, "extensionId")) {
 | 
						|
        extensions.push({ commandID, ...sidebar });
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return extensions;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieve the list of sidebar panels
 | 
						|
   *
 | 
						|
   * @param {Array} commandIds
 | 
						|
   * @returns {Array}
 | 
						|
   */
 | 
						|
  getSidebarPanels(commandIds) {
 | 
						|
    const tools = [];
 | 
						|
    for (const commandID of commandIds) {
 | 
						|
      const sidebar = this.sidebars.get(commandID);
 | 
						|
      if (sidebar) {
 | 
						|
        tools.push({ commandID, ...sidebar });
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return tools;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove a browser extension.
 | 
						|
   *
 | 
						|
   * @param {string} commandID
 | 
						|
   */
 | 
						|
  removeExtension(commandID) {
 | 
						|
    const sidebar = this.sidebars.get(commandID);
 | 
						|
    if (!sidebar) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (this.currentID === commandID) {
 | 
						|
      this.hide();
 | 
						|
    }
 | 
						|
    document.getElementById(sidebar.menuId)?.remove();
 | 
						|
    document.getElementById(sidebar.switcherMenuId)?.remove();
 | 
						|
    this.sidebars.delete(commandID);
 | 
						|
    this.toolsAndExtensions.delete(commandID);
 | 
						|
    window.dispatchEvent(new CustomEvent("SidebarItemRemoved"));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Show the sidebar.
 | 
						|
   *
 | 
						|
   * This wraps the internal method, including a ping to telemetry.
 | 
						|
   *
 | 
						|
   * @param {string}  commandID     ID of the sidebar to use.
 | 
						|
   * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
 | 
						|
   *                                showing of the sidebar.
 | 
						|
   * @returns {Promise<boolean>}
 | 
						|
   */
 | 
						|
  async show(commandID, triggerNode) {
 | 
						|
    let panelType = commandID.substring(4, commandID.length - 7);
 | 
						|
    Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
 | 
						|
 | 
						|
    // Extensions without private window access wont be in the
 | 
						|
    // sidebars map.
 | 
						|
    if (!this.sidebars.has(commandID)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return this._show(commandID).then(() => {
 | 
						|
      this._loadSidebarExtension(commandID);
 | 
						|
 | 
						|
      if (triggerNode) {
 | 
						|
        updateToggleControlLabel(triggerNode);
 | 
						|
      }
 | 
						|
 | 
						|
      this._fireFocusedEvent();
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Show the sidebar, without firing the focused event or logging telemetry.
 | 
						|
   * This is intended to be used when the sidebar is opened automatically
 | 
						|
   * when a window opens (not triggered by user interaction).
 | 
						|
   *
 | 
						|
   * @param {string} commandID ID of the sidebar.
 | 
						|
   * @returns {Promise<boolean>}
 | 
						|
   */
 | 
						|
  async showInitially(commandID) {
 | 
						|
    let panelType = commandID.substring(4, commandID.length - 7);
 | 
						|
    Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
 | 
						|
 | 
						|
    // Extensions without private window access wont be in the
 | 
						|
    // sidebars map.
 | 
						|
    if (!this.sidebars.has(commandID)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return this._show(commandID).then(() => {
 | 
						|
      this._loadSidebarExtension(commandID);
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Implementation for show. Also used internally for sidebars that are shown
 | 
						|
   * when a window is opened and we don't want to ping telemetry.
 | 
						|
   *
 | 
						|
   * @param {string} commandID ID of the sidebar.
 | 
						|
   * @returns {Promise<void>}
 | 
						|
   */
 | 
						|
  _show(commandID) {
 | 
						|
    return new Promise(resolve => {
 | 
						|
      if (this.sidebarRevampEnabled) {
 | 
						|
        this._box.dispatchEvent(
 | 
						|
          new CustomEvent("sidebar-show", { detail: { viewId: commandID } })
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        this.hideSwitcherPanel();
 | 
						|
      }
 | 
						|
 | 
						|
      this.selectMenuItem(commandID);
 | 
						|
      this._box.hidden = this._splitter.hidden = false;
 | 
						|
      // sets the sidebar to the left or right, based on a pref
 | 
						|
      this.setPosition();
 | 
						|
 | 
						|
      this._box.setAttribute("checked", "true");
 | 
						|
      this._box.setAttribute("sidebarcommand", commandID);
 | 
						|
 | 
						|
      let { icon, url, title, sourceL10nEl } = this.sidebars.get(commandID);
 | 
						|
      if (icon) {
 | 
						|
        this._switcherTarget.style.setProperty(
 | 
						|
          "--webextension-menuitem-image",
 | 
						|
          icon
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        this._switcherTarget.style.removeProperty(
 | 
						|
          "--webextension-menuitem-image"
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      // use to live update <tree> elements if the locale changes
 | 
						|
      this.lastOpenedId = commandID;
 | 
						|
      this.title = title;
 | 
						|
      // Keep the title element in the switcher in sync with any l10n changes.
 | 
						|
      this.observeTitleChanges(sourceL10nEl);
 | 
						|
 | 
						|
      this.browser.setAttribute("src", url); // kick off async load
 | 
						|
 | 
						|
      if (this.browser.contentDocument.location.href != url) {
 | 
						|
        this.browser.addEventListener(
 | 
						|
          "load",
 | 
						|
          () => {
 | 
						|
            // We're handling the 'load' event before it bubbles up to the usual
 | 
						|
            // (non-capturing) event handlers. Let it bubble up before resolving.
 | 
						|
            setTimeout(() => {
 | 
						|
              resolve();
 | 
						|
 | 
						|
              // Now that the currentId is updated, fire a show event.
 | 
						|
              this._fireShowEvent();
 | 
						|
            }, 0);
 | 
						|
          },
 | 
						|
          { capture: true, once: true }
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        resolve();
 | 
						|
 | 
						|
        // Now that the currentId is updated, fire a show event.
 | 
						|
        this._fireShowEvent();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Hide the sidebar.
 | 
						|
   *
 | 
						|
   * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
 | 
						|
   *                                hiding of the sidebar.
 | 
						|
   */
 | 
						|
  hide(triggerNode) {
 | 
						|
    if (!this.isOpen) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.hideSwitcherPanel();
 | 
						|
    if (this.sidebarRevampEnabled) {
 | 
						|
      this._box.dispatchEvent(new CustomEvent("sidebar-hide"));
 | 
						|
    }
 | 
						|
    this.selectMenuItem("");
 | 
						|
 | 
						|
    // Replace the document currently displayed in the sidebar with about:blank
 | 
						|
    // so that we can free memory by unloading the page. We need to explicitly
 | 
						|
    // create a new content viewer because the old one doesn't get destroyed
 | 
						|
    // until about:blank has loaded (which does not happen as long as the
 | 
						|
    // element is hidden).
 | 
						|
    this.browser.setAttribute("src", "about:blank");
 | 
						|
    this.browser.docShell.createAboutBlankDocumentViewer(null, null);
 | 
						|
 | 
						|
    this._box.removeAttribute("checked");
 | 
						|
    this._box.hidden = this._splitter.hidden = true;
 | 
						|
 | 
						|
    let selBrowser = gBrowser.selectedBrowser;
 | 
						|
    selBrowser.focus();
 | 
						|
    if (triggerNode) {
 | 
						|
      updateToggleControlLabel(triggerNode);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets the checked state only on the menu items of the specified sidebar, or
 | 
						|
   * none if the argument is an empty string.
 | 
						|
   */
 | 
						|
  selectMenuItem(commandID) {
 | 
						|
    for (let [id, { menuId, triggerButtonId }] of this.sidebars) {
 | 
						|
      let menu = document.getElementById(menuId);
 | 
						|
      let triggerbutton =
 | 
						|
        triggerButtonId && document.getElementById(triggerButtonId);
 | 
						|
      if (id == commandID) {
 | 
						|
        menu.setAttribute("checked", "true");
 | 
						|
        if (triggerbutton) {
 | 
						|
          triggerbutton.setAttribute("checked", "true");
 | 
						|
          updateToggleControlLabel(triggerbutton);
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        menu.removeAttribute("checked");
 | 
						|
        if (triggerbutton) {
 | 
						|
          triggerbutton.removeAttribute("checked");
 | 
						|
          updateToggleControlLabel(triggerbutton);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
// Add getters related to the position here, since we will want them
 | 
						|
// available for both startDelayedLoad and init.
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  SidebarController,
 | 
						|
  "_positionStart",
 | 
						|
  SidebarController.POSITION_START_PREF,
 | 
						|
  true,
 | 
						|
  SidebarController.setPosition.bind(SidebarController)
 | 
						|
);
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  SidebarController,
 | 
						|
  "sidebarRevampEnabled",
 | 
						|
  "sidebar.revamp",
 | 
						|
  false
 | 
						|
);
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  SidebarController,
 | 
						|
  "megalistEnabled",
 | 
						|
  "browser.megalist.enabled",
 | 
						|
  false
 | 
						|
);
 |