Add tab context menu (#2162)

This commit is contained in:
Martynas Bagdonas 2021-08-26 08:49:39 +03:00 committed by Dan Stillman
parent b47f5f51e7
commit 50732d1479
6 changed files with 185 additions and 27 deletions

View file

@ -65,6 +65,15 @@ const TabBar = forwardRef(function (props, ref) {
});
function handleTabMouseDown(event, id) {
if (event.button === 2) {
let { screenX, screenY } = event;
// Popup gets immediately closed without this
setTimeout(() => {
props.onContextMenu(screenX, screenY, id);
}, 0);
return;
}
if (event.target.closest('.tab-close')) {
return;
}

View file

@ -51,6 +51,7 @@ var Zotero_Tabs = new function () {
}];
this._selectedID = 'zotero-pane';
this._prevSelectedID = null;
this._history = [];
this._getTab = function (id) {
var tabIndex = this._tabs.findIndex(tab => tab.id == id);
@ -78,6 +79,7 @@ var Zotero_Tabs = new function () {
onTabSelect={this.select.bind(this)}
onTabMove={this.move.bind(this)}
onTabClose={this.close.bind(this)}
onContextMenu={this._openMenu.bind(this)}
/>,
document.getElementById('tab-bar-container'),
() => {
@ -181,27 +183,41 @@ var Zotero_Tabs = new function () {
};
/**
* Close a tab
* Close tabs
*
* @param {String} id
* @param {String|Array<String>|undefined} ids One or more ids, or empty for the current tab
*/
this.close = function (id) {
var { tab, tabIndex } = this._getTab(id || this._selectedID);
if (tabIndex == 0) {
this.close = function (ids) {
if (!ids) {
ids = [this._selectedID];
}
else if (!Array.isArray(ids)) {
ids = [ids];
}
if (ids.includes('zotero-pane')) {
throw new Error('Library tab cannot be closed');
}
if (!tab) {
return;
var historyEntry = [];
var closedIDs = [];
var tmpTabs = this._tabs.slice();
for (var id of ids) {
var { tab, tabIndex } = this._getTab(id);
if (!tab) {
continue;
}
if (tab.id == this._selectedID) {
this.select(this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id);
}
this._tabs.splice(tabIndex, 1);
document.getElementById(tab.id).remove();
if (tab.onClose) {
tab.onClose();
}
historyEntry.push({ index: tmpTabs.indexOf(tab), data: tab.data });
closedIDs.push(id);
}
if (tab.id == this._selectedID) {
this.select(this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id);
}
this._tabs.splice(tabIndex, 1);
document.getElementById(tab.id).remove();
if (tab.onClose) {
tab.onClose();
}
Zotero.Notifier.trigger('close', 'tab', [tab.id], true);
this._history.push(historyEntry);
Zotero.Notifier.trigger('close', 'tab', [closedIDs]);
this._update();
};
@ -209,7 +225,27 @@ var Zotero_Tabs = new function () {
* Close all tabs except the first one
*/
this.closeAll = function () {
this._tabs.slice(1).map(tab => this.close(tab.id));
this.close(this._tabs.slice(1).map(x => x.id));
};
/**
* Undo tabs closing
*/
this.undoClose = function () {
var historyEntry = this._history.pop();
if (historyEntry) {
for (var tab of historyEntry) {
if (Zotero.Items.exists(tab.data.itemID)) {
Zotero.Reader.open(tab.data.itemID,
null,
{
tabIndex: tab.index,
openInBackground: true
}
);
}
}
}
};
/**
@ -283,10 +319,99 @@ var Zotero_Tabs = new function () {
*
* @param {Integer} index
*/
this.jump = function(index) {
this.jump = function (index) {
this.select(this._tabs[Math.min(index, this._tabs.length - 1)].id);
};
this._openMenu = function (x, y, id) {
var { tabIndex } = this._getTab(id);
window.Zotero_Tooltip.stop();
let menuitem;
let popup = document.createElement('menupopup');
document.querySelector('popupset').appendChild(popup);
popup.addEventListener('popuphidden', function (event) {
if (event.target === popup) {
popup.remove();
}
});
if (id !== 'zotero-pane') {
// Show in library
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('general.showInLibrary'));
menuitem.addEventListener('command', () => {
var reader = Zotero.Reader.getByTabID(id);
if (reader) {
ZoteroPane_Local.selectItem(reader.itemID);
this.select('zotero-pane');
}
});
popup.appendChild(menuitem);
// Move tab
let menu = document.createElement('menu');
menu.setAttribute('label', Zotero.getString('tabs.move'));
let menupopup = document.createElement('menupopup');
menu.append(menupopup);
popup.appendChild(menu);
// Move to start
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.moveToStart'));
menuitem.setAttribute('disabled', tabIndex == 1);
menuitem.addEventListener('command', () => {
this.move(id, 1);
});
menupopup.appendChild(menuitem);
// Move to end
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.moveToEnd'));
menuitem.setAttribute('disabled', tabIndex == this._tabs.length - 1);
menuitem.addEventListener('command', () => {
this.move(id, this._tabs.length);
});
menupopup.appendChild(menuitem);
// Move to new window
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.moveToWindow'));
menuitem.setAttribute('disabled', false);
menuitem.addEventListener('command', () => {
var reader = Zotero.Reader.getByTabID(id);
if (reader) {
this.close(id);
Zotero.Reader.open(reader.itemID, null, { openInWindow: true });
}
});
menupopup.appendChild(menuitem);
// Separator
popup.appendChild(document.createElement('menuseparator'));
}
// Undo close
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.undoClose'));
menuitem.setAttribute('disabled', !this._history.length);
menuitem.addEventListener('command', () => {
this.undoClose();
});
popup.appendChild(menuitem);
if (!(this._tabs.length == 2 && id != 'zotero-pane')) {
// Close other tabs
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.closeOther'));
menuitem.addEventListener('command', () => {
this.close(this._tabs.slice(1).filter(x => x.id != id).map(x => x.id));
});
popup.appendChild(menuitem);
}
if (id != 'zotero-pane') {
// Close
menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('general.close'));
menuitem.addEventListener('command', () => {
this.close(id);
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
};
/**
* Update state of the tab bar.
* Only used on Windows and Linux. On macOS, the tab bar is always shown.

View file

@ -664,7 +664,7 @@ class ReaderInstance {
}
class ReaderTab extends ReaderInstance {
constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, background }) {
constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, index, background }) {
super();
this._itemID = itemID;
this._sidebarWidth = sidebarWidth;
@ -675,6 +675,7 @@ class ReaderTab extends ReaderInstance {
let { id, container } = this._window.Zotero_Tabs.add({
type: 'reader',
title: title || '',
index,
data: {
itemID
},
@ -906,12 +907,17 @@ class Reader {
notify(event, type, ids, extraData) {
if (type === 'tab') {
let reader = Zotero.Reader.getByTabID(ids[0]);
if (reader) {
if (event === 'close') {
this._readers.splice(this._readers.indexOf(reader), 1);
if (event === 'close') {
for (let id of ids) {
let reader = Zotero.Reader.getByTabID(id);
if (reader) {
this._readers.splice(this._readers.indexOf(reader), 1);
}
}
else if (event === 'select') {
}
else if (event === 'select') {
let reader = Zotero.Reader.getByTabID(ids[0]);
if (reader) {
this.triggerAnnotationsImportCheck(reader._itemID);
}
}
@ -976,7 +982,7 @@ class Reader {
await this.open(item.id, location, options);
}
async open(itemID, location, { title, openInBackground, openInWindow } = {}) {
async open(itemID, location, { title, tabIndex, openInBackground, openInWindow } = {}) {
this._loadSidebarOpenState();
this.triggerAnnotationsImportCheck(itemID);
let reader;
@ -1017,6 +1023,7 @@ class Reader {
reader = new ReaderTab({
itemID,
title,
index: tabIndex,
background: openInBackground,
sidebarWidth: this._sidebarWidth,
sidebarOpen: this._sidebarOpen,

View file

@ -533,6 +533,15 @@ var ZoteroPane = new function()
return;
}
}
// Undo closed tabs
if ((Zotero.isMac && event.metaKey || !Zotero.isMac && event.ctrlKey)
&& event.shiftKey && !event.altKey && event.key.toLowerCase() == 't') {
Zotero_Tabs.undoClose();
event.preventDefault();
event.stopPropagation();
return;
}
// Tab navigation: Ctrl-PageUp / PageDown
// TODO: Select across tabs without selecting with Ctrl-Shift, as in Firefox?

View file

@ -50,6 +50,7 @@ general.export = Export
general.moreInformation = More Information
general.seeForMoreInformation = See %S for more information.
general.open = Open %S
general.close = Close
general.enable = Enable
general.disable = Disable
general.remove = Remove
@ -67,6 +68,7 @@ general.fix = Fix…
general.tryAgain = Try Again
general.tryLater = Try Later
general.showDirectory = Show Directory
general.showInLibrary = Show in Library
general.continue = Continue
general.copy = Copy
general.copyToClipboard = Copy to Clipboard
@ -1323,7 +1325,6 @@ noteEditor.set = Set
noteEditor.edit = Edit
noteEditor.addCitation = Add Citation
noteEditor.findAndReplace = Find and Replace
noteEditor.showInLibrary = Show in Library
noteEditor.editInWindow = Edit in a Separate Window
noteEditor.applyAnnotationColors = Apply Annotation Colors
noteEditor.removeAnnotationColors = Remove Annotation Colors
@ -1354,3 +1355,10 @@ spellCheck.addRemoveDictionaries = Add/Remove Dictionaries…
spellCheck.dictionaryManager.title = Add/Remove Dictionaries
spellCheck.dictionaryManager.updateAvailable = %S (update available)
spellCheck.dictionaryManager.error.unableToInstall = Unable to install “%S”:
tabs.move = Move Tab
tabs.moveToStart = Move to Start
tabs.moveToEnd = Move to End
tabs.moveToWindow = Move to New Window
tabs.undoClose = Reopen Closed Tab
tabs.closeOther = Close Other Tabs

@ -1 +1 @@
Subproject commit 9749a4b2be4baac5b16f28e70a7e7997b000ef9e
Subproject commit 4ba41a41689448aecc21a72aec0559d39ca4509e