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/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
11 const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
13 function setAttributes(element, attrs) {
14 for (let [name, value] of Object.entries(attrs)) {
16 element.setAttribute(name, value);
18 element.removeAttribute(name);
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;
42 this.doc = containerNode.ownerDocument;
43 this.gBrowser = this.doc.defaultView.gBrowser;
44 this.tabToElement = new Map();
45 this.listenersRegistered = false;
49 return this.tabToElement.values();
54 case "TabAttrModified":
55 this._tabAttrModified(event.target);
58 this._tabClose(event.target);
61 this._moveTab(event.target);
64 if (!this.filterFn(event.target)) {
65 this._tabClose(event.target);
69 this._selectTab(event.target.tab);
72 this._onDragStart(event);
75 this._onDragOver(event);
78 this._onDragLeave(event);
81 this._onDragEnd(event);
93 if (this.gBrowser.selectedTab != tab) {
94 this.gBrowser.selectedTab = tab;
96 this.gBrowser.tabContainer._handleTabSelect();
101 * Populate the popup with menuitems and setup the listeners.
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));
112 this._addElement(fragment);
113 this._setupListeners();
116 _addElement(elementOrFragment) {
117 this.containerNode.insertBefore(elementOrFragment, this.insertBefore);
121 * Remove the menuitems from the DOM, cleanup internal state and listeners.
124 for (let item of this.rows) {
127 this.tabToElement = new Map();
128 this._cleanupListeners();
129 this._clearDropTarget();
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);
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);
167 this.listenersRegistered = false;
170 _tabAttrModified(tab) {
171 let item = this.tabToElement.get(tab);
173 if (!this.filterFn(tab)) {
174 // The tab no longer matches our criteria, remove it.
175 this._removeItem(item, tab);
177 this._setRowAttributes(item, tab);
179 } else if (this.filterFn(tab)) {
180 // The tab now matches our criteria, add a row for it.
186 let item = this.tabToElement.get(tab);
188 this._removeItem(item, tab);
193 if (!this.filterFn(newTab)) {
196 let newRow = this._createRow(newTab);
197 let nextTab = newTab.nextElementSibling;
199 while (nextTab && !this.filterFn(nextTab)) {
200 nextTab = nextTab.nextElementSibling;
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);
206 nextRow.parentNode.insertBefore(newRow, nextRow);
208 // If there's no next tab then insert it as usual.
209 this._addElement(newRow);
213 let item = this.tabToElement.get(tab);
215 this._removeItem(item, tab);
219 _removeItem(item, tab) {
220 this.tabToElement.delete(tab);
225 const TABS_PANEL_EVENTS = {
227 hide: "PanelMultiViewHidden",
230 export class TabsPanel extends TabsListBase {
234 containerNode: opts.containerNode || opts.view.firstElementChild,
236 this.view = opts.view;
237 this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
238 this.panelMultiView = null;
242 switch (event.type) {
243 case TABS_PANEL_EVENTS.hide:
244 if (event.target == this.panelMultiView) {
246 this.panelMultiView = null;
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();
257 if (event.target.classList.contains("all-tabs-mute-button")) {
258 event.target.tab.toggleMuteAudio();
261 if (event.target.classList.contains("all-tabs-close-button")) {
262 this.gBrowser.removeTab(event.target.tab);
267 super.handleEvent(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);
283 super._selectTab(tab);
284 lazy.PanelMultiView.hidePopup(this.view.closest("panel"));
288 super._setupListeners();
289 this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
292 _cleanupListeners() {
293 super._cleanupListeners();
294 this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, 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);
306 row.addEventListener("command", this);
307 this.tabToElement.set(tab, row);
309 let button = doc.createXULElement("toolbarbutton");
312 "all-tabs-button subviewbutton subviewbutton-iconic"
314 button.setAttribute("flex", "1");
315 button.setAttribute("crop", "end");
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");
327 row.appendChild(button);
329 let muteButton = doc.createXULElement("toolbarbutton");
330 muteButton.classList.add(
331 "all-tabs-mute-button",
332 "all-tabs-secondary-button",
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",
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);
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, {
365 image: !busy && tab.getAttribute("image"),
366 iconloadingprincipal: tab.getAttribute("iconloadingprincipal"),
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, {
379 soundplaying: tab.soundPlaying,
380 hidden: !(tab.muted || tab.soundPlaying),
384 _setImageAttributes(row, tab) {
385 let button = row.firstElementChild;
386 let image = button.icon;
389 let busy = tab.getAttribute("busy");
390 let progress = tab.getAttribute("progress");
391 setAttributes(image, { busy, progress });
393 image.classList.add("tab-throbber-tabslist");
395 image.classList.remove("tab-throbber-tabslist");
400 _onDragStart(event) {
401 const row = this._getTargetRowFromEvent(event);
406 this.gBrowser.tabContainer.startTabDrag(event, row.firstElementChild.tab, {
411 _getTargetRowFromEvent(event) {
412 return event.target.closest("toolbaritem");
415 _isMovingTabs(event) {
416 var effects = this.gBrowser.tabContainer.getDropEffectForTabDrag(event);
417 return effects == "move";
421 if (!this._isMovingTabs(event)) {
425 if (!this._updateDropTarget(event)) {
429 event.preventDefault();
430 event.stopPropagation();
434 return Array.prototype.indexOf.call(this.containerNode.children, row);
438 if (!this._isMovingTabs(event)) {
442 if (!this._updateDropTarget(event)) {
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();
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`.
462 if (draggedTab._tPos < targetTab._tPos) {
463 pos = targetTab._tPos + this.dropTargetDirection;
465 pos = targetTab._tPos + this.dropTargetDirection + 1;
467 this.gBrowser.moveTabTo(draggedTab, pos);
469 this._clearDropTarget();
472 _onDragLeave(event) {
473 if (!this._isMovingTabs(event)) {
477 let target = event.relatedTarget;
478 while (target && target != this.containerNode) {
479 target = target.parentNode;
485 this._clearDropTarget();
489 if (!this._isMovingTabs(event)) {
493 this._clearDropTarget();
496 _updateDropTarget(event) {
497 const row = this._getTargetRowFromEvent(event);
502 const rect = row.getBoundingClientRect();
503 const index = this._getRowIndex(row);
508 const threshold = rect.height * 0.5;
509 if (event.clientY < rect.top + threshold) {
510 this._setDropTarget(row, -1);
512 this._setDropTarget(row, 0);
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.
527 if (this.dropTargetDirection === -1) {
528 if (this.dropTargetRow.previousSibling) {
529 const rect = this.dropTargetRow.previousSibling.getBoundingClientRect();
530 top = rect.top + rect.height;
532 const rect = this.dropTargetRow.getBoundingClientRect();
536 const rect = this.dropTargetRow.getBoundingClientRect();
537 top = rect.top + rect.height;
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;
551 if (this.dropTargetRow) {
552 this.dropTargetRow = null;
555 if (this.dropIndicator) {
556 this.dropIndicator.style.top = `0px`;
557 this.dropIndicator.collapsed = true;
562 if (event.button == 1) {
563 const row = this._getTargetRowFromEvent(event);
568 this.gBrowser.removeTab(row.tab, {