зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1771831 - Make it possible to reorder tabs in "List all tabs" list by drag-and-drop. r=NeilDeakin
Also the tab list item can be dragged to the tab bar to reorder, or moved to other window, or detached. Differential Revision: https://phabricator.services.mozilla.com/D147709
This commit is contained in:
Родитель
8a8963929a
Коммит
d50c882977
|
@ -21,6 +21,9 @@
|
|||
oncommand="PanelUI.showSubView('allTabsMenu-hiddenTabsView', this);"
|
||||
data-l10n-id="all-tabs-menu-hidden-tabs"/>
|
||||
<toolbarseparator id="allTabsMenu-tabsSeparator"/>
|
||||
<vbox id="allTabsMenu-dropIndicatorHolder">
|
||||
<vbox id="allTabsMenu-dropIndicator" collapsed="true"/>
|
||||
</vbox>
|
||||
<vbox id="allTabsMenu-allTabsViewTabs" class="panel-subview-body"/>
|
||||
</vbox>
|
||||
</panelview>
|
||||
|
|
|
@ -16,6 +16,7 @@ var gTabsPanel = {
|
|||
allTabsButton: "alltabs-button",
|
||||
allTabsView: "allTabsMenu-allTabsView",
|
||||
allTabsViewTabs: "allTabsMenu-allTabsViewTabs",
|
||||
dropIndicator: "allTabsMenu-dropIndicator",
|
||||
containerTabsView: "allTabsMenu-containerTabsView",
|
||||
hiddenTabsButton: "allTabsMenu-hiddenTabsButton",
|
||||
hiddenTabsView: "allTabsMenu-hiddenTabsView",
|
||||
|
@ -56,6 +57,7 @@ var gTabsPanel = {
|
|||
containerNode: this.allTabsViewTabs,
|
||||
filterFn: tab =>
|
||||
!tab.hidden && (!tab.pinned || (showPinnedTabs && tab.pinned)),
|
||||
dropIndicator: this.dropIndicator,
|
||||
});
|
||||
|
||||
this.allTabsView.addEventListener("ViewShowing", e => {
|
||||
|
|
|
@ -511,6 +511,22 @@ toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-b
|
|||
position: absolute;
|
||||
}
|
||||
|
||||
#allTabsMenu-dropIndicatorHolder {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#allTabsMenu-dropIndicator {
|
||||
background: url(chrome://browser/skin/tabbrowser/tab-drag-indicator.svg) no-repeat center;
|
||||
display: block;
|
||||
position: absolute;
|
||||
transform: rotate(-90deg);
|
||||
width: 12px;
|
||||
height: 29px;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#nav-bar-customization-target > #personal-bookmarks,
|
||||
toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks,
|
||||
toolbar:not(#TabsToolbar) > #personal-bookmarks {
|
||||
|
|
|
@ -409,6 +409,10 @@
|
|||
return;
|
||||
}
|
||||
|
||||
this.startTabDrag(event, tab);
|
||||
}
|
||||
|
||||
startTabDrag(event, tab, { fromTabList = false } = {}) {
|
||||
let selectedTabs = gBrowser.selectedTabs;
|
||||
let otherSelectedTabs = selectedTabs.filter(
|
||||
selectedTab => selectedTab != tab
|
||||
|
@ -529,13 +533,14 @@
|
|||
movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
|
||||
t => t.pinned == tab.pinned
|
||||
),
|
||||
fromTabList,
|
||||
};
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
on_dragover(event) {
|
||||
var effects = this._getDropEffectForTabDrag(event);
|
||||
var effects = this.getDropEffectForTabDrag(event);
|
||||
|
||||
var ind = this._tabDropIndicator;
|
||||
if (effects == "" || effects == "none") {
|
||||
|
@ -571,7 +576,8 @@
|
|||
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
if (
|
||||
(effects == "move" || effects == "copy") &&
|
||||
this == draggedTab.container
|
||||
this == draggedTab.container &&
|
||||
!draggedTab._dragData.fromTabList
|
||||
) {
|
||||
ind.hidden = true;
|
||||
|
||||
|
@ -692,9 +698,14 @@
|
|||
newTranslateX -= tabWidth;
|
||||
}
|
||||
|
||||
let dropIndex =
|
||||
"animDropIndex" in draggedTab._dragData &&
|
||||
draggedTab._dragData.animDropIndex;
|
||||
let dropIndex;
|
||||
if (draggedTab._dragData.fromTabList) {
|
||||
dropIndex = this._getDropIndex(event, false);
|
||||
} else {
|
||||
dropIndex =
|
||||
"animDropIndex" in draggedTab._dragData &&
|
||||
draggedTab._dragData.animDropIndex;
|
||||
}
|
||||
let incrementDropIndex = true;
|
||||
if (dropIndex && dropIndex > movingTabs[0]._tPos) {
|
||||
dropIndex--;
|
||||
|
@ -1982,7 +1993,7 @@
|
|||
return tabs.length;
|
||||
}
|
||||
|
||||
_getDropEffectForTabDrag(event) {
|
||||
getDropEffectForTabDrag(event) {
|
||||
var dt = event.dataTransfer;
|
||||
|
||||
let isMovingTabs = dt.mozItemCount > 0;
|
||||
|
|
|
@ -148,3 +148,4 @@ skip-if = (verify && os == 'mac')
|
|||
[browser_bfcache_exemption_about_pages.js]
|
||||
skip-if = !fission
|
||||
[browser_tab_tooltips.js]
|
||||
[browser_tab_manager_drag.js]
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* Test reordering the tabs in the Tab Manager, moving the tab between the
|
||||
* Tab Manager and tab bar.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const URL1 = "data:text/plain,tab1";
|
||||
const URL2 = "data:text/plain,tab2";
|
||||
const URL3 = "data:text/plain,tab3";
|
||||
const URL4 = "data:text/plain,tab4";
|
||||
const URL5 = "data:text/plain,tab5";
|
||||
|
||||
function assertOrder(order, expected, message) {
|
||||
is(
|
||||
JSON.stringify(order),
|
||||
JSON.stringify(expected),
|
||||
`The order of the tabs ${message}`
|
||||
);
|
||||
}
|
||||
|
||||
function toIndex(url) {
|
||||
const m = url.match(/^data:text\/plain,tab(\d)/);
|
||||
if (m) {
|
||||
return parseInt(m[1]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getOrderOfList(list) {
|
||||
return [...list.querySelectorAll("toolbaritem")].map(row => {
|
||||
const url = row.firstElementChild.tab.linkedBrowser.currentURI.spec;
|
||||
return toIndex(url);
|
||||
});
|
||||
}
|
||||
|
||||
function getOrderOfTabs(tabs) {
|
||||
return tabs.map(tab => {
|
||||
const url = tab.linkedBrowser.currentURI.spec;
|
||||
return toIndex(url);
|
||||
});
|
||||
}
|
||||
|
||||
async function testWithNewWindow(func) {
|
||||
Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", true);
|
||||
|
||||
const newWindow = await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
|
||||
|
||||
await Promise.all([
|
||||
addTabTo(newWindow.gBrowser, URL1),
|
||||
addTabTo(newWindow.gBrowser, URL2),
|
||||
addTabTo(newWindow.gBrowser, URL3),
|
||||
addTabTo(newWindow.gBrowser, URL4),
|
||||
addTabTo(newWindow.gBrowser, URL5),
|
||||
]);
|
||||
|
||||
newWindow.gTabsPanel.init();
|
||||
|
||||
const button = newWindow.document.getElementById("alltabs-button");
|
||||
|
||||
const allTabsView = newWindow.document.getElementById(
|
||||
"allTabsMenu-allTabsView"
|
||||
);
|
||||
const allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
|
||||
allTabsView,
|
||||
"ViewShown"
|
||||
);
|
||||
button.click();
|
||||
await allTabsPopupShownPromise;
|
||||
|
||||
await func(newWindow);
|
||||
|
||||
await BrowserTestUtils.closeWindow(newWindow);
|
||||
|
||||
Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
|
||||
}
|
||||
|
||||
add_task(async function test_reorder() {
|
||||
await testWithNewWindow(async function(newWindow) {
|
||||
const list = newWindow.document.getElementById(
|
||||
"allTabsMenu-allTabsViewTabs"
|
||||
);
|
||||
|
||||
assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder");
|
||||
|
||||
let rows;
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[3],
|
||||
rows[1],
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(getOrderOfList(list), [0, 3, 1, 2, 4, 5], "after moving up");
|
||||
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[1],
|
||||
rows[5],
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(getOrderOfList(list), [0, 1, 2, 4, 3, 5], "after moving down");
|
||||
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[4],
|
||||
rows[3],
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfList(list),
|
||||
[0, 1, 2, 3, 4, 5],
|
||||
"after moving up again"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function tabOf(row) {
|
||||
return row.firstElementChild.tab;
|
||||
}
|
||||
|
||||
add_task(async function test_move_to_tab_bar() {
|
||||
await testWithNewWindow(async function(newWindow) {
|
||||
const list = newWindow.document.getElementById(
|
||||
"allTabsMenu-allTabsViewTabs"
|
||||
);
|
||||
|
||||
assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder");
|
||||
|
||||
let rows;
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[3],
|
||||
tabOf(rows[1]),
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfList(list),
|
||||
[0, 3, 1, 2, 4, 5],
|
||||
"after moving up with tab bar"
|
||||
);
|
||||
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[1],
|
||||
tabOf(rows[4]),
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfList(list),
|
||||
[0, 1, 2, 3, 4, 5],
|
||||
"after moving down with tab bar"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_move_to_different_tab_bar() {
|
||||
const newWindow2 = await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
|
||||
|
||||
await testWithNewWindow(async function(newWindow) {
|
||||
const list = newWindow.document.getElementById(
|
||||
"allTabsMenu-allTabsViewTabs"
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfList(list),
|
||||
[0, 1, 2, 3, 4, 5],
|
||||
"before reorder in newWindow"
|
||||
);
|
||||
assertOrder(
|
||||
getOrderOfTabs(newWindow2.gBrowser.tabs),
|
||||
[0],
|
||||
"before reorder in newWindow2"
|
||||
);
|
||||
|
||||
let rows;
|
||||
rows = list.querySelectorAll("toolbaritem");
|
||||
EventUtils.synthesizeDrop(
|
||||
rows[3],
|
||||
newWindow2.gBrowser.tabs[0],
|
||||
null,
|
||||
"move",
|
||||
newWindow,
|
||||
newWindow2,
|
||||
{ clientX: 0, clientY: 0 }
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfList(list),
|
||||
[0, 1, 2, 4, 5],
|
||||
"after moving to other window in newWindow"
|
||||
);
|
||||
|
||||
assertOrder(
|
||||
getOrderOfTabs(newWindow2.gBrowser.tabs),
|
||||
[3, 0],
|
||||
"after moving to other window in newWindow2"
|
||||
);
|
||||
});
|
||||
|
||||
await BrowserTestUtils.closeWindow(newWindow2);
|
||||
});
|
|
@ -26,10 +26,18 @@ function triggerClickOn(target, options) {
|
|||
return promise;
|
||||
}
|
||||
|
||||
async function addTab(url = "http://mochi.test:8888/", params = {}) {
|
||||
async function addTab(url = "http://mochi.test:8888/", params) {
|
||||
return addTabTo(gBrowser, url, params);
|
||||
}
|
||||
|
||||
async function addTabTo(
|
||||
targetBrowser,
|
||||
url = "http://mochi.test:8888/",
|
||||
params = {}
|
||||
) {
|
||||
params.skipAnimation = true;
|
||||
const tab = BrowserTestUtils.addTab(gBrowser, url, params);
|
||||
const browser = gBrowser.getBrowserForTab(tab);
|
||||
const tab = BrowserTestUtils.addTab(targetBrowser, url, params);
|
||||
const browser = targetBrowser.getBrowserForTab(tab);
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
return tab;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ ChromeUtils.defineModuleGetter(
|
|||
|
||||
var EXPORTED_SYMBOLS = ["TabsPanel"];
|
||||
|
||||
const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
|
||||
|
||||
function setAttributes(element, attrs) {
|
||||
for (let [name, value] of Object.entries(attrs)) {
|
||||
if (value) {
|
||||
|
@ -25,11 +27,23 @@ function setAttributes(element, attrs) {
|
|||
}
|
||||
|
||||
class TabsListBase {
|
||||
constructor({ className, filterFn, insertBefore, containerNode }) {
|
||||
constructor({
|
||||
className,
|
||||
filterFn,
|
||||
insertBefore,
|
||||
containerNode,
|
||||
dropIndicator = null,
|
||||
}) {
|
||||
this.className = className;
|
||||
this.filterFn = filterFn;
|
||||
this.insertBefore = insertBefore;
|
||||
this.containerNode = containerNode;
|
||||
this.dropIndicator = dropIndicator;
|
||||
|
||||
if (this.dropIndicator) {
|
||||
this.dropTargetRow = null;
|
||||
this.dropTargetDirection = 0;
|
||||
}
|
||||
|
||||
this.doc = containerNode.ownerDocument;
|
||||
this.gBrowser = this.doc.defaultView.gBrowser;
|
||||
|
@ -60,6 +74,21 @@ class TabsListBase {
|
|||
case "command":
|
||||
this._selectTab(event.target.tab);
|
||||
break;
|
||||
case "dragstart":
|
||||
this._onDragStart(event);
|
||||
break;
|
||||
case "dragover":
|
||||
this._onDragOver(event);
|
||||
break;
|
||||
case "dragleave":
|
||||
this._onDragLeave(event);
|
||||
break;
|
||||
case "dragend":
|
||||
this._onDragEnd(event);
|
||||
break;
|
||||
case "drop":
|
||||
this._onDrop(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,10 +133,19 @@ class TabsListBase {
|
|||
|
||||
_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);
|
||||
|
||||
if (this.dropIndicator) {
|
||||
this.containerNode.addEventListener("dragstart", this);
|
||||
this.containerNode.addEventListener("dragover", this);
|
||||
this.containerNode.addEventListener("dragleave", this);
|
||||
this.containerNode.addEventListener("dragend", this);
|
||||
this.containerNode.addEventListener("drop", this);
|
||||
}
|
||||
}
|
||||
|
||||
_cleanupListeners() {
|
||||
|
@ -115,6 +153,15 @@ class TabsListBase {
|
|||
this.gBrowser.tabContainer.removeEventListener("TabClose", this);
|
||||
this.gBrowser.tabContainer.removeEventListener("TabMove", this);
|
||||
this.gBrowser.tabContainer.removeEventListener("TabPinned", this);
|
||||
|
||||
if (this.dropIndicator) {
|
||||
this.containerNode.removeEventListener("dragstart", this);
|
||||
this.containerNode.removeEventListener("dragover", this);
|
||||
this.containerNode.removeEventListener("dragleave", this);
|
||||
this.containerNode.removeEventListener("dragend", this);
|
||||
this.containerNode.removeEventListener("drop", this);
|
||||
}
|
||||
|
||||
this.listenersRegistered = false;
|
||||
}
|
||||
|
||||
|
@ -317,4 +364,158 @@ class TabsPanel extends TabsListBase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onDragStart(event) {
|
||||
const row = this._getDragTargetRow(event);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gBrowser.tabContainer.startTabDrag(event, row.firstElementChild.tab, {
|
||||
fromTabList: true,
|
||||
});
|
||||
}
|
||||
|
||||
_getDragTargetRow(event) {
|
||||
let row = event.target;
|
||||
while (row && row.localName !== "toolbaritem") {
|
||||
row = row.parentNode;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
_isMovingTabs(event) {
|
||||
var effects = this.gBrowser.tabContainer.getDropEffectForTabDrag(event);
|
||||
return effects == "move";
|
||||
}
|
||||
|
||||
_onDragOver(event) {
|
||||
if (!this._isMovingTabs(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._updateDropTarget(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_getRowIndex(row) {
|
||||
return Array.prototype.indexOf.call(this.containerNode.children, row);
|
||||
}
|
||||
|
||||
_onDrop(event) {
|
||||
if (!this._isMovingTabs(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._updateDropTarget(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
|
||||
if (draggedTab === this.dropTargetRow.firstElementChild.tab) {
|
||||
this._cleanupDragDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTab = this.dropTargetRow.firstElementChild.tab;
|
||||
|
||||
// NOTE: Given the list is opened only when the window is focused,
|
||||
// we don't have to check `draggedTab.container`.
|
||||
|
||||
let pos;
|
||||
if (draggedTab._tPos < targetTab._tPos) {
|
||||
pos = targetTab._tPos + this.dropTargetDirection;
|
||||
} else {
|
||||
pos = targetTab._tPos + this.dropTargetDirection + 1;
|
||||
}
|
||||
this.gBrowser.moveTabTo(draggedTab, pos);
|
||||
|
||||
this._cleanupDragDetails();
|
||||
}
|
||||
|
||||
_onDragLeave(event) {
|
||||
if (!this._isMovingTabs(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target !== this.containerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearDropTarget();
|
||||
}
|
||||
|
||||
_onDragEnd(event) {
|
||||
if (!this._isMovingTabs(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._cleanupDragDetails();
|
||||
}
|
||||
|
||||
_updateDropTarget(event) {
|
||||
const row = this._getDragTargetRow(event);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = row.getBoundingClientRect();
|
||||
const index = this._getRowIndex(row);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const threshold = rect.height * 0.5;
|
||||
if (event.clientY < rect.top + threshold) {
|
||||
this._setDropTarget(row, -1);
|
||||
} else {
|
||||
this._setDropTarget(row, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_setDropTarget(row, direction) {
|
||||
this._clearDropTarget();
|
||||
|
||||
this.dropTargetRow = row;
|
||||
this.dropTargetDirection = direction;
|
||||
|
||||
let offset = this.dropIndicator.parentNode.getBoundingClientRect().top;
|
||||
let top;
|
||||
if (this.dropTargetDirection === -1) {
|
||||
if (this.dropTargetRow.previousSibling) {
|
||||
const rect = this.dropTargetRow.previousSibling.getBoundingClientRect();
|
||||
top = rect.top + rect.height;
|
||||
} else {
|
||||
const rect = this.dropTargetRow.getBoundingClientRect();
|
||||
top = rect.top;
|
||||
}
|
||||
} else {
|
||||
const rect = this.dropTargetRow.getBoundingClientRect();
|
||||
top = rect.top + rect.height;
|
||||
}
|
||||
|
||||
this.dropIndicator.style.top = `${top - offset - 12}px`;
|
||||
this.dropIndicator.collapsed = false;
|
||||
}
|
||||
|
||||
_clearDropTarget() {
|
||||
if (this.dropTargetRow) {
|
||||
this.dropTargetRow = null;
|
||||
}
|
||||
this.dropIndicator.collapsed = true;
|
||||
}
|
||||
|
||||
_cleanupDragDetails() {
|
||||
this._clearDropTarget();
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче