Bug 1890689 Don't pretend to pre-buffer with DynamicResampler r=pehrsons
[gecko.git] / browser / modules / TabsList.sys.mjs
blob44878afb8ff51b0c04b7d63e9c9d80a5efcdcc3e
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 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
9 });
11 const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
13 function setAttributes(element, attrs) {
14   for (let [name, value] of Object.entries(attrs)) {
15     if (value) {
16       element.setAttribute(name, value);
17     } else {
18       element.removeAttribute(name);
19     }
20   }
23 class TabsListBase {
24   constructor({
25     className,
26     filterFn,
27     insertBefore,
28     containerNode,
29     dropIndicator = null,
30   }) {
31     this.className = className;
32     this.filterFn = filterFn;
33     this.insertBefore = insertBefore;
34     this.containerNode = containerNode;
35     this.dropIndicator = dropIndicator;
37     if (this.dropIndicator) {
38       this.dropTargetRow = null;
39       this.dropTargetDirection = 0;
40     }
42     this.doc = containerNode.ownerDocument;
43     this.gBrowser = this.doc.defaultView.gBrowser;
44     this.tabToElement = new Map();
45     this.listenersRegistered = false;
46   }
48   get rows() {
49     return this.tabToElement.values();
50   }
52   handleEvent(event) {
53     switch (event.type) {
54       case "TabAttrModified":
55         this._tabAttrModified(event.target);
56         break;
57       case "TabClose":
58         this._tabClose(event.target);
59         break;
60       case "TabMove":
61         this._moveTab(event.target);
62         break;
63       case "TabPinned":
64         if (!this.filterFn(event.target)) {
65           this._tabClose(event.target);
66         }
67         break;
68       case "command":
69         this._selectTab(event.target.tab);
70         break;
71       case "dragstart":
72         this._onDragStart(event);
73         break;
74       case "dragover":
75         this._onDragOver(event);
76         break;
77       case "dragleave":
78         this._onDragLeave(event);
79         break;
80       case "dragend":
81         this._onDragEnd(event);
82         break;
83       case "drop":
84         this._onDrop(event);
85         break;
86       case "click":
87         this._onClick(event);
88         break;
89     }
90   }
92   _selectTab(tab) {
93     if (this.gBrowser.selectedTab != tab) {
94       this.gBrowser.selectedTab = tab;
95     } else {
96       this.gBrowser.tabContainer._handleTabSelect();
97     }
98   }
100   /*
101    * Populate the popup with menuitems and setup the listeners.
102    */
103   _populate() {
104     let fragment = this.doc.createDocumentFragment();
106     for (let tab of this.gBrowser.tabs) {
107       if (this.filterFn(tab)) {
108         fragment.appendChild(this._createRow(tab));
109       }
110     }
112     this._addElement(fragment);
113     this._setupListeners();
114   }
116   _addElement(elementOrFragment) {
117     this.containerNode.insertBefore(elementOrFragment, this.insertBefore);
118   }
120   /*
121    * Remove the menuitems from the DOM, cleanup internal state and listeners.
122    */
123   _cleanup() {
124     for (let item of this.rows) {
125       item.remove();
126     }
127     this.tabToElement = new Map();
128     this._cleanupListeners();
129     this._clearDropTarget();
130   }
132   _setupListeners() {
133     this.listenersRegistered = true;
135     this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
136     this.gBrowser.tabContainer.addEventListener("TabClose", this);
137     this.gBrowser.tabContainer.addEventListener("TabMove", this);
138     this.gBrowser.tabContainer.addEventListener("TabPinned", this);
140     this.containerNode.addEventListener("click", this);
142     if (this.dropIndicator) {
143       this.containerNode.addEventListener("dragstart", this);
144       this.containerNode.addEventListener("dragover", this);
145       this.containerNode.addEventListener("dragleave", this);
146       this.containerNode.addEventListener("dragend", this);
147       this.containerNode.addEventListener("drop", this);
148     }
149   }
151   _cleanupListeners() {
152     this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
153     this.gBrowser.tabContainer.removeEventListener("TabClose", this);
154     this.gBrowser.tabContainer.removeEventListener("TabMove", this);
155     this.gBrowser.tabContainer.removeEventListener("TabPinned", this);
157     this.containerNode.removeEventListener("click", this);
159     if (this.dropIndicator) {
160       this.containerNode.removeEventListener("dragstart", this);
161       this.containerNode.removeEventListener("dragover", this);
162       this.containerNode.removeEventListener("dragleave", this);
163       this.containerNode.removeEventListener("dragend", this);
164       this.containerNode.removeEventListener("drop", this);
165     }
167     this.listenersRegistered = false;
168   }
170   _tabAttrModified(tab) {
171     let item = this.tabToElement.get(tab);
172     if (item) {
173       if (!this.filterFn(tab)) {
174         // The tab no longer matches our criteria, remove it.
175         this._removeItem(item, tab);
176       } else {
177         this._setRowAttributes(item, tab);
178       }
179     } else if (this.filterFn(tab)) {
180       // The tab now matches our criteria, add a row for it.
181       this._addTab(tab);
182     }
183   }
185   _moveTab(tab) {
186     let item = this.tabToElement.get(tab);
187     if (item) {
188       this._removeItem(item, tab);
189       this._addTab(tab);
190     }
191   }
192   _addTab(newTab) {
193     if (!this.filterFn(newTab)) {
194       return;
195     }
196     let newRow = this._createRow(newTab);
197     let nextTab = newTab.nextElementSibling;
199     while (nextTab && !this.filterFn(nextTab)) {
200       nextTab = nextTab.nextElementSibling;
201     }
203     // If we found a tab after this one in the list, insert the new row before it.
204     let nextRow = this.tabToElement.get(nextTab);
205     if (nextRow) {
206       nextRow.parentNode.insertBefore(newRow, nextRow);
207     } else {
208       // If there's no next tab then insert it as usual.
209       this._addElement(newRow);
210     }
211   }
212   _tabClose(tab) {
213     let item = this.tabToElement.get(tab);
214     if (item) {
215       this._removeItem(item, tab);
216     }
217   }
219   _removeItem(item, tab) {
220     this.tabToElement.delete(tab);
221     item.remove();
222   }
225 const TABS_PANEL_EVENTS = {
226   show: "ViewShowing",
227   hide: "PanelMultiViewHidden",
230 export class TabsPanel extends TabsListBase {
231   constructor(opts) {
232     super({
233       ...opts,
234       containerNode: opts.containerNode || opts.view.firstElementChild,
235     });
236     this.view = opts.view;
237     this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
238     this.panelMultiView = null;
239   }
241   handleEvent(event) {
242     switch (event.type) {
243       case TABS_PANEL_EVENTS.hide:
244         if (event.target == this.panelMultiView) {
245           this._cleanup();
246           this.panelMultiView = null;
247         }
248         break;
249       case TABS_PANEL_EVENTS.show:
250         if (!this.listenersRegistered && event.target == this.view) {
251           this.panelMultiView = this.view.panelMultiView;
252           this._populate(event);
253           this.gBrowser.translateTabContextMenu();
254         }
255         break;
256       case "command":
257         if (event.target.classList.contains("all-tabs-mute-button")) {
258           event.target.tab.toggleMuteAudio();
259           break;
260         }
261         if (event.target.classList.contains("all-tabs-close-button")) {
262           this.gBrowser.removeTab(event.target.tab);
263           break;
264         }
265       // fall through
266       default:
267         super.handleEvent(event);
268         break;
269     }
270   }
272   _populate(event) {
273     super._populate(event);
275     // The loading throbber can't be set until the toolbarbutton is rendered,
276     // so set the image attributes again now that the elements are in the DOM.
277     for (let row of this.rows) {
278       this._setImageAttributes(row, row.tab);
279     }
280   }
282   _selectTab(tab) {
283     super._selectTab(tab);
284     lazy.PanelMultiView.hidePopup(this.view.closest("panel"));
285   }
287   _setupListeners() {
288     super._setupListeners();
289     this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
290   }
292   _cleanupListeners() {
293     super._cleanupListeners();
294     this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, this);
295   }
297   _createRow(tab) {
298     let { doc } = this;
299     let row = doc.createXULElement("toolbaritem");
300     row.setAttribute("class", "all-tabs-item");
301     row.setAttribute("context", "tabContextMenu");
302     if (this.className) {
303       row.classList.add(this.className);
304     }
305     row.tab = tab;
306     row.addEventListener("command", this);
307     this.tabToElement.set(tab, row);
309     let button = doc.createXULElement("toolbarbutton");
310     button.setAttribute(
311       "class",
312       "all-tabs-button subviewbutton subviewbutton-iconic"
313     );
314     button.setAttribute("flex", "1");
315     button.setAttribute("crop", "end");
316     button.tab = tab;
318     if (tab.userContextId) {
319       tab.classList.forEach(property => {
320         if (property.startsWith("identity-color")) {
321           button.classList.add(property);
322           button.classList.add("all-tabs-container-indicator");
323         }
324       });
325     }
327     row.appendChild(button);
329     let muteButton = doc.createXULElement("toolbarbutton");
330     muteButton.classList.add(
331       "all-tabs-mute-button",
332       "all-tabs-secondary-button",
333       "subviewbutton"
334     );
335     muteButton.setAttribute("closemenu", "none");
336     muteButton.tab = tab;
337     row.appendChild(muteButton);
339     let closeButton = doc.createXULElement("toolbarbutton");
340     closeButton.classList.add(
341       "all-tabs-close-button",
342       "all-tabs-secondary-button",
343       "subviewbutton"
344     );
345     closeButton.setAttribute("closemenu", "none");
346     doc.l10n.setAttributes(closeButton, "tabbrowser-manager-close-tab");
347     closeButton.tab = tab;
348     row.appendChild(closeButton);
350     this._setRowAttributes(row, tab);
352     return row;
353   }
355   _setRowAttributes(row, tab) {
356     setAttributes(row, { selected: tab.selected });
358     let tooltiptext = this.gBrowser.getTabTooltip(tab);
359     let busy = tab.getAttribute("busy");
360     let button = row.firstElementChild;
361     setAttributes(button, {
362       busy,
363       label: tab.label,
364       tooltiptext,
365       image: !busy && tab.getAttribute("image"),
366       iconloadingprincipal: tab.getAttribute("iconloadingprincipal"),
367     });
369     this._setImageAttributes(row, tab);
371     let muteButton = row.querySelector(".all-tabs-mute-button");
372     let muteButtonTooltipString = tab.muted
373       ? "tabbrowser-manager-unmute-tab"
374       : "tabbrowser-manager-mute-tab";
375     this.doc.l10n.setAttributes(muteButton, muteButtonTooltipString);
377     setAttributes(muteButton, {
378       muted: tab.muted,
379       soundplaying: tab.soundPlaying,
380       hidden: !(tab.muted || tab.soundPlaying),
381     });
382   }
384   _setImageAttributes(row, tab) {
385     let button = row.firstElementChild;
386     let image = button.icon;
388     if (image) {
389       let busy = tab.getAttribute("busy");
390       let progress = tab.getAttribute("progress");
391       setAttributes(image, { busy, progress });
392       if (busy) {
393         image.classList.add("tab-throbber-tabslist");
394       } else {
395         image.classList.remove("tab-throbber-tabslist");
396       }
397     }
398   }
400   _onDragStart(event) {
401     const row = this._getTargetRowFromEvent(event);
402     if (!row) {
403       return;
404     }
406     this.gBrowser.tabContainer.startTabDrag(event, row.firstElementChild.tab, {
407       fromTabList: true,
408     });
409   }
411   _getTargetRowFromEvent(event) {
412     return event.target.closest("toolbaritem");
413   }
415   _isMovingTabs(event) {
416     var effects = this.gBrowser.tabContainer.getDropEffectForTabDrag(event);
417     return effects == "move";
418   }
420   _onDragOver(event) {
421     if (!this._isMovingTabs(event)) {
422       return;
423     }
425     if (!this._updateDropTarget(event)) {
426       return;
427     }
429     event.preventDefault();
430     event.stopPropagation();
431   }
433   _getRowIndex(row) {
434     return Array.prototype.indexOf.call(this.containerNode.children, row);
435   }
437   _onDrop(event) {
438     if (!this._isMovingTabs(event)) {
439       return;
440     }
442     if (!this._updateDropTarget(event)) {
443       return;
444     }
446     event.preventDefault();
447     event.stopPropagation();
449     let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
451     if (draggedTab === this.dropTargetRow.firstElementChild.tab) {
452       this._clearDropTarget();
453       return;
454     }
456     const targetTab = this.dropTargetRow.firstElementChild.tab;
458     // NOTE: Given the list is opened only when the window is focused,
459     //       we don't have to check `draggedTab.container`.
461     let pos;
462     if (draggedTab._tPos < targetTab._tPos) {
463       pos = targetTab._tPos + this.dropTargetDirection;
464     } else {
465       pos = targetTab._tPos + this.dropTargetDirection + 1;
466     }
467     this.gBrowser.moveTabTo(draggedTab, pos);
469     this._clearDropTarget();
470   }
472   _onDragLeave(event) {
473     if (!this._isMovingTabs(event)) {
474       return;
475     }
477     let target = event.relatedTarget;
478     while (target && target != this.containerNode) {
479       target = target.parentNode;
480     }
481     if (target) {
482       return;
483     }
485     this._clearDropTarget();
486   }
488   _onDragEnd(event) {
489     if (!this._isMovingTabs(event)) {
490       return;
491     }
493     this._clearDropTarget();
494   }
496   _updateDropTarget(event) {
497     const row = this._getTargetRowFromEvent(event);
498     if (!row) {
499       return false;
500     }
502     const rect = row.getBoundingClientRect();
503     const index = this._getRowIndex(row);
504     if (index === -1) {
505       return false;
506     }
508     const threshold = rect.height * 0.5;
509     if (event.clientY < rect.top + threshold) {
510       this._setDropTarget(row, -1);
511     } else {
512       this._setDropTarget(row, 0);
513     }
515     return true;
516   }
518   _setDropTarget(row, direction) {
519     this.dropTargetRow = row;
520     this.dropTargetDirection = direction;
522     const holder = this.dropIndicator.parentNode;
523     const holderOffset = holder.getBoundingClientRect().top;
525     // Set top to before/after the target row.
526     let top;
527     if (this.dropTargetDirection === -1) {
528       if (this.dropTargetRow.previousSibling) {
529         const rect = this.dropTargetRow.previousSibling.getBoundingClientRect();
530         top = rect.top + rect.height;
531       } else {
532         const rect = this.dropTargetRow.getBoundingClientRect();
533         top = rect.top;
534       }
535     } else {
536       const rect = this.dropTargetRow.getBoundingClientRect();
537       top = rect.top + rect.height;
538     }
540     // Avoid overflowing the sub view body.
541     const indicatorHeight = 12;
542     const subViewBody = holder.parentNode;
543     const subViewBodyRect = subViewBody.getBoundingClientRect();
544     top = Math.min(top, subViewBodyRect.bottom - indicatorHeight);
546     this.dropIndicator.style.top = `${top - holderOffset - 12}px`;
547     this.dropIndicator.collapsed = false;
548   }
550   _clearDropTarget() {
551     if (this.dropTargetRow) {
552       this.dropTargetRow = null;
553     }
555     if (this.dropIndicator) {
556       this.dropIndicator.style.top = `0px`;
557       this.dropIndicator.collapsed = true;
558     }
559   }
561   _onClick(event) {
562     if (event.button == 1) {
563       const row = this._getTargetRowFromEvent(event);
564       if (!row) {
565         return;
566       }
568       this.gBrowser.removeTab(row.tab, {
569         animate: true,
570       });
571     }
572   }