Bug 1700051: part 45) Remove outdated part of comment in <mozInlineSpellChecker.cpp...
[gecko.git] / browser / modules / TabsList.jsm
blobaab7956b7a52e8081d72519eade5ab78216570cf
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 ChromeUtils.defineModuleGetter(
8   this,
9   "PanelMultiView",
10   "resource:///modules/PanelMultiView.jsm"
13 var EXPORTED_SYMBOLS = ["TabsPanel"];
15 function setAttributes(element, attrs) {
16   for (let [name, value] of Object.entries(attrs)) {
17     if (value) {
18       element.setAttribute(name, value);
19     } else {
20       element.removeAttribute(name);
21     }
22   }
25 class TabsListBase {
26   constructor({ className, filterFn, insertBefore, containerNode }) {
27     this.className = className;
28     this.filterFn = filterFn;
29     this.insertBefore = insertBefore;
30     this.containerNode = containerNode;
32     this.doc = containerNode.ownerDocument;
33     this.gBrowser = this.doc.defaultView.gBrowser;
34     this.tabToElement = new Map();
35     this.listenersRegistered = false;
36   }
38   get rows() {
39     return this.tabToElement.values();
40   }
42   handleEvent(event) {
43     switch (event.type) {
44       case "TabAttrModified":
45         this._tabAttrModified(event.target);
46         break;
47       case "TabClose":
48         this._tabClose(event.target);
49         break;
50       case "TabMove":
51         this._moveTab(event.target);
52         break;
53       case "TabPinned":
54         if (!this.filterFn(event.target)) {
55           this._tabClose(event.target);
56         }
57         break;
58       case "command":
59         this._selectTab(event.target.tab);
60         break;
61     }
62   }
64   _selectTab(tab) {
65     if (this.gBrowser.selectedTab != tab) {
66       this.gBrowser.selectedTab = tab;
67     } else {
68       this.gBrowser.tabContainer._handleTabSelect();
69     }
70   }
72   /*
73    * Populate the popup with menuitems and setup the listeners.
74    */
75   _populate(event) {
76     let fragment = this.doc.createDocumentFragment();
78     for (let tab of this.gBrowser.tabs) {
79       if (this.filterFn(tab)) {
80         fragment.appendChild(this._createRow(tab));
81       }
82     }
84     this._addElement(fragment);
85     this._setupListeners();
86   }
88   _addElement(elementOrFragment) {
89     this.containerNode.insertBefore(elementOrFragment, this.insertBefore);
90   }
92   /*
93    * Remove the menuitems from the DOM, cleanup internal state and listeners.
94    */
95   _cleanup() {
96     for (let item of this.rows) {
97       item.remove();
98     }
99     this.tabToElement = new Map();
100     this._cleanupListeners();
101   }
103   _setupListeners() {
104     this.listenersRegistered = true;
105     this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
106     this.gBrowser.tabContainer.addEventListener("TabClose", this);
107     this.gBrowser.tabContainer.addEventListener("TabMove", this);
108     this.gBrowser.tabContainer.addEventListener("TabPinned", this);
109   }
111   _cleanupListeners() {
112     this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
113     this.gBrowser.tabContainer.removeEventListener("TabClose", this);
114     this.gBrowser.tabContainer.removeEventListener("TabMove", this);
115     this.gBrowser.tabContainer.removeEventListener("TabPinned", this);
116     this.listenersRegistered = false;
117   }
119   _tabAttrModified(tab) {
120     let item = this.tabToElement.get(tab);
121     if (item) {
122       if (!this.filterFn(tab)) {
123         // The tab no longer matches our criteria, remove it.
124         this._removeItem(item, tab);
125       } else {
126         this._setRowAttributes(item, tab);
127       }
128     } else if (this.filterFn(tab)) {
129       // The tab now matches our criteria, add a row for it.
130       this._addTab(tab);
131     }
132   }
134   _moveTab(tab) {
135     let item = this.tabToElement.get(tab);
136     if (item) {
137       this._removeItem(item, tab);
138       this._addTab(tab);
139     }
140   }
141   _addTab(newTab) {
142     if (!this.filterFn(newTab)) {
143       return;
144     }
145     let newRow = this._createRow(newTab);
146     let nextTab = newTab.nextElementSibling;
148     while (nextTab && !this.filterFn(nextTab)) {
149       nextTab = nextTab.nextElementSibling;
150     }
152     // If we found a tab after this one in the list, insert the new row before it.
153     let nextRow = this.tabToElement.get(nextTab);
154     if (nextRow) {
155       nextRow.parentNode.insertBefore(newRow, nextRow);
156     } else {
157       // If there's no next tab then insert it as usual.
158       this._addElement(newRow);
159     }
160   }
161   _tabClose(tab) {
162     let item = this.tabToElement.get(tab);
163     if (item) {
164       this._removeItem(item, tab);
165     }
166   }
168   _removeItem(item, tab) {
169     this.tabToElement.delete(tab);
170     item.remove();
171   }
174 const TABS_PANEL_EVENTS = {
175   show: "ViewShowing",
176   hide: "PanelMultiViewHidden",
179 class TabsPanel extends TabsListBase {
180   constructor(opts) {
181     super({
182       ...opts,
183       containerNode: opts.containerNode || opts.view.firstElementChild,
184     });
185     this.view = opts.view;
186     this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
187     this.panelMultiView = null;
188   }
190   handleEvent(event) {
191     switch (event.type) {
192       case TABS_PANEL_EVENTS.hide:
193         if (event.target == this.panelMultiView) {
194           this._cleanup();
195           this.panelMultiView = null;
196         }
197         break;
198       case TABS_PANEL_EVENTS.show:
199         if (!this.listenersRegistered && event.target == this.view) {
200           this.panelMultiView = this.view.panelMultiView;
201           this._populate(event);
202         }
203         break;
204       case "command":
205         if (event.target.hasAttribute("toggle-mute")) {
206           event.target.tab.toggleMuteAudio();
207           break;
208         }
209       // fall through
210       default:
211         super.handleEvent(event);
212         break;
213     }
214   }
216   _populate(event) {
217     super._populate(event);
219     // The loading throbber can't be set until the toolbarbutton is rendered,
220     // so set the image attributes again now that the elements are in the DOM.
221     for (let row of this.rows) {
222       this._setImageAttributes(row, row.tab);
223     }
224   }
226   _selectTab(tab) {
227     super._selectTab(tab);
228     PanelMultiView.hidePopup(this.view.closest("panel"));
229   }
231   _setupListeners() {
232     super._setupListeners();
233     this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
234   }
236   _cleanupListeners() {
237     super._cleanupListeners();
238     this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, this);
239   }
241   _createRow(tab) {
242     let { doc } = this;
243     let row = doc.createXULElement("toolbaritem");
244     row.setAttribute("class", "all-tabs-item");
245     row.setAttribute("context", "tabContextMenu");
246     if (this.className) {
247       row.classList.add(this.className);
248     }
249     row.tab = tab;
250     row.addEventListener("command", this);
251     this.tabToElement.set(tab, row);
253     let button = doc.createXULElement("toolbarbutton");
254     button.setAttribute(
255       "class",
256       "all-tabs-button subviewbutton subviewbutton-iconic"
257     );
258     button.setAttribute("flex", "1");
259     button.setAttribute("crop", "right");
260     button.tab = tab;
262     row.appendChild(button);
264     let secondaryButton = doc.createXULElement("toolbarbutton");
265     secondaryButton.setAttribute(
266       "class",
267       "all-tabs-secondary-button subviewbutton subviewbutton-iconic"
268     );
269     secondaryButton.setAttribute("closemenu", "none");
270     secondaryButton.setAttribute("toggle-mute", "true");
271     secondaryButton.tab = tab;
272     row.appendChild(secondaryButton);
274     this._setRowAttributes(row, tab);
276     return row;
277   }
279   _setRowAttributes(row, tab) {
280     setAttributes(row, { selected: tab.selected });
282     let busy = tab.getAttribute("busy");
283     let button = row.firstElementChild;
284     setAttributes(button, {
285       busy,
286       label: tab.label,
287       image: !busy && tab.getAttribute("image"),
288       iconloadingprincipal: tab.getAttribute("iconloadingprincipal"),
289     });
291     this._setImageAttributes(row, tab);
293     let secondaryButton = row.querySelector(".all-tabs-secondary-button");
294     setAttributes(secondaryButton, {
295       muted: tab.muted,
296       soundplaying: tab.soundPlaying,
297       pictureinpicture: tab.pictureinpicture,
298       hidden: !(tab.muted || tab.soundPlaying),
299     });
300   }
302   _setImageAttributes(row, tab) {
303     let button = row.firstElementChild;
304     let image = button.icon;
306     if (image) {
307       let busy = tab.getAttribute("busy");
308       let progress = tab.getAttribute("progress");
309       setAttributes(image, { busy, progress });
310       if (busy) {
311         image.classList.add("tab-throbber-tabslist");
312       } else {
313         image.classList.remove("tab-throbber-tabslist");
314       }
315     }
316   }