gecko-dev/browser/modules/TabsList.jsm

318 строки
8.2 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/. */
"use strict";
ChromeUtils.defineModuleGetter(
this,
"PanelMultiView",
"resource:///modules/PanelMultiView.jsm"
);
var EXPORTED_SYMBOLS = ["TabsPanel"];
function setAttributes(element, attrs) {
for (let [name, value] of Object.entries(attrs)) {
if (value) {
element.setAttribute(name, value);
} else {
element.removeAttribute(name);
}
}
}
class TabsListBase {
constructor({ className, filterFn, insertBefore, containerNode }) {
this.className = className;
this.filterFn = filterFn;
this.insertBefore = insertBefore;
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 "TabMove":
this._moveTab(event.target);
break;
case "TabPinned":
if (!this.filterFn(event.target)) {
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)) {
fragment.appendChild(this._createRow(tab));
}
}
this._addElement(fragment);
this._setupListeners();
}
_addElement(elementOrFragment) {
this.containerNode.insertBefore(elementOrFragment, this.insertBefore);
}
/*
* 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);
this.gBrowser.tabContainer.addEventListener("TabMove", this);
this.gBrowser.tabContainer.addEventListener("TabPinned", this);
}
_cleanupListeners() {
this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
this.gBrowser.tabContainer.removeEventListener("TabClose", this);
this.gBrowser.tabContainer.removeEventListener("TabMove", this);
this.gBrowser.tabContainer.removeEventListener("TabPinned", this);
this.listenersRegistered = false;
}
_tabAttrModified(tab) {
let item = this.tabToElement.get(tab);
if (item) {
if (!this.filterFn(tab)) {
// The tab no longer matches our criteria, remove it.
this._removeItem(item, tab);
} else {
this._setRowAttributes(item, tab);
}
} else if (this.filterFn(tab)) {
// The tab now matches our criteria, add a row for it.
this._addTab(tab);
}
}
_moveTab(tab) {
let item = this.tabToElement.get(tab);
if (item) {
this._removeItem(item, tab);
this._addTab(tab);
}
}
_addTab(newTab) {
if (!this.filterFn(newTab)) {
return;
}
let newRow = this._createRow(newTab);
let nextTab = newTab.nextElementSibling;
while (nextTab && !this.filterFn(nextTab)) {
nextTab = nextTab.nextElementSibling;
}
// If we found a tab after this one in the list, insert the new row before it.
let nextRow = this.tabToElement.get(nextTab);
if (nextRow) {
nextRow.parentNode.insertBefore(newRow, nextRow);
} else {
// If there's no next tab then insert it as usual.
this._addElement(newRow);
}
}
_tabClose(tab) {
let item = this.tabToElement.get(tab);
if (item) {
this._removeItem(item, tab);
}
}
_removeItem(item, tab) {
this.tabToElement.delete(tab);
item.remove();
}
}
const TABS_PANEL_EVENTS = {
show: "ViewShowing",
hide: "PanelMultiViewHidden",
};
class TabsPanel extends TabsListBase {
constructor(opts) {
super({
...opts,
containerNode: opts.containerNode || opts.view.firstElementChild,
});
this.view = opts.view;
this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
this.panelMultiView = null;
}
handleEvent(event) {
switch (event.type) {
case TABS_PANEL_EVENTS.hide:
if (event.target == this.panelMultiView) {
this._cleanup();
this.panelMultiView = null;
}
break;
case TABS_PANEL_EVENTS.show:
if (!this.listenersRegistered && event.target == this.view) {
this.panelMultiView = this.view.panelMultiView;
this._populate(event);
}
break;
case "command":
if (event.target.hasAttribute("toggle-mute")) {
event.target.tab.toggleMuteAudio();
break;
}
// fall through
default:
super.handleEvent(event);
break;
}
}
_populate(event) {
super._populate(event);
// The loading throbber can't be set until the toolbarbutton is rendered,
// so set the image attributes again now that the elements are in the DOM.
for (let row of this.rows) {
this._setImageAttributes(row, row.tab);
}
}
_selectTab(tab) {
super._selectTab(tab);
PanelMultiView.hidePopup(this.view.closest("panel"));
}
_setupListeners() {
super._setupListeners();
this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
}
_cleanupListeners() {
super._cleanupListeners();
this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, this);
}
_createRow(tab) {
let { doc } = this;
let row = doc.createXULElement("toolbaritem");
row.setAttribute("class", "all-tabs-item");
row.setAttribute("context", "tabContextMenu");
if (this.className) {
row.classList.add(this.className);
}
row.tab = tab;
row.addEventListener("command", this);
this.tabToElement.set(tab, row);
let button = doc.createXULElement("toolbarbutton");
button.setAttribute(
"class",
"all-tabs-button subviewbutton subviewbutton-iconic"
);
button.setAttribute("flex", "1");
button.setAttribute("crop", "right");
button.tab = tab;
row.appendChild(button);
let secondaryButton = doc.createXULElement("toolbarbutton");
secondaryButton.setAttribute(
"class",
"all-tabs-secondary-button subviewbutton subviewbutton-iconic"
);
secondaryButton.setAttribute("closemenu", "none");
secondaryButton.setAttribute("toggle-mute", "true");
secondaryButton.tab = tab;
row.appendChild(secondaryButton);
this._setRowAttributes(row, tab);
return row;
}
_setRowAttributes(row, tab) {
setAttributes(row, { selected: tab.selected });
let busy = tab.getAttribute("busy");
let button = row.firstElementChild;
setAttributes(button, {
busy,
label: tab.label,
image: !busy && tab.getAttribute("image"),
iconloadingprincipal: tab.getAttribute("iconloadingprincipal"),
});
this._setImageAttributes(row, tab);
let secondaryButton = row.querySelector(".all-tabs-secondary-button");
setAttributes(secondaryButton, {
muted: tab.muted,
soundplaying: tab.soundPlaying,
pictureinpicture: tab.pictureinpicture,
hidden: !(tab.muted || tab.soundPlaying),
});
}
_setImageAttributes(row, tab) {
let button = row.firstElementChild;
let image = button.icon;
if (image) {
let busy = tab.getAttribute("busy");
let progress = tab.getAttribute("progress");
setAttributes(image, { busy, progress });
if (busy) {
image.classList.add("tab-throbber-tabslist");
} else {
image.classList.remove("tab-throbber-tabslist");
}
}
}
}