Tab bar & Reader: Rewrite and connect everything

This commit is contained in:
Martynas Bagdonas 2020-09-24 17:47:47 +03:00 committed by Dan Stillman
parent edd4f27e09
commit 2f505862d7
10 changed files with 526 additions and 369 deletions

View file

@ -23,126 +23,107 @@
***** END LICENSE BLOCK *****
*/
"use strict";
'use strict';
import React, { forwardRef, useState, useImperativeHandle, useEffect } from 'react';
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect } from 'react';
import cx from 'classnames';
const TabBar = forwardRef(function (props, ref) {
const [tabs, setTabs] = useState(props.initialTabs);
const [selectedIndex, setSelectedIndex] = useState(0);
useImperativeHandle(ref, () => ({
get selectedIndex() {
return selectedIndex;
},
selectLeft() {
var newIndex = selectedIndex - 1;
if (newIndex < 0) {
newIndex = tabs.length + newIndex;
}
setSelectedIndex(newIndex);
},
selectRight() {
var newIndex = selectedIndex + 1;
if (newIndex >= tabs.length) {
newIndex = newIndex - tabs.length;
}
setSelectedIndex(newIndex);
},
select(index) {
if (index > tabs.length - 1) {
throw new Error("Tab index out of bounds");
}
setSelectedIndex(index);
},
add({ title, type }) {
var newTabs = [...tabs];
newTabs.push({ title, type });
setTabs(newTabs);
setSelectedIndex(newTabs.length - 1);
},
rename(title, index) {
var newTabs = tabs.map((tab, currentIndex) => {
let newTab = Object.assign({}, tab);
if (index == currentIndex) {
newTab.title = title;
}
return newTab;
});
setTabs(newTabs);
},
close(index) {
if (index == 0) {
return;
}
removeTab(index);
}
}));
const [tabs, setTabs] = useState([]);
const draggingID = useRef(null);
const tabsHolderRef = useRef();
const mouseMoveWaitUntil = useRef(0);
useEffect(() => {
if (props.onTabSelected) {
props.onTabSelected(selectedIndex);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
useImperativeHandle(ref, () => ({ setTabs }));
function handleMouseDown(event, id, index) {
if (index != 0) {
draggingID.current = id;
}
}, [selectedIndex]);
function removeTab(index) {
var newTabs = [...tabs];
newTabs.splice(index, 1);
setTabs(newTabs);
setSelectedIndex(Math.min(selectedIndex, newTabs.length - 1));
if (props.onTabClosed) {
props.onTabClosed(index);
}
}
function handleTabClick(event, index) {
setSelectedIndex(index);
props.onTabSelect(id);
event.stopPropagation();
}
function handleTabClose(event, index) {
removeTab(index);
if (props.onTabClose) {
props.onTabClose(index);
function handleMouseMove(event) {
if (!draggingID.current || mouseMoveWaitUntil.current > Date.now()) {
return;
}
let points = Array.from(tabsHolderRef.current.children).map((child) => {
let rect = child.getBoundingClientRect();
return rect.left + rect.width / 2;
});
let index = null;
for (let i = 0; i < points.length - 1; i++) {
let point1 = points[i];
let point2 = points[i + 1];
if (event.clientX > Math.min(point1, point2)
&& event.clientX < Math.max(point1, point2)) {
index = i + 1;
break;
}
}
if (index === null) {
let point1 = points[0];
let point2 = points[points.length - 1];
if ((point1 < point2 && event.clientX < point1
|| point1 > point2 && event.clientX > point1)) {
index = 0;
}
else {
index = points.length;
}
}
if (index == 0) {
index = 1;
}
props.onTabMove(draggingID.current, index);
mouseMoveWaitUntil.current = Date.now() + 100;
}
function handleMouseUp(event) {
draggingID.current = null;
event.stopPropagation();
}
function renderTab(tab, index) {
function handleTabClose(event, id) {
props.onTabClose(id);
event.stopPropagation();
}
function renderTab({ id, title, selected }, index) {
return (
<div
key={index}
className={cx("tab", { selected: index == selectedIndex })}
onClick={(event) => handleTabClick(event, index)}
key={id}
className={cx('tab', { selected })}
onMouseDown={(event) => handleMouseDown(event, id, index)}
>
<div className="tab-name">{tab.title}</div>
<div className="tab-name">{title}</div>
<div
className="tab-close"
onClick={(event) => handleTabClose(event, index)}
onClick={(event) => handleTabClose(event, id)}
>
x
</div>
</div>
);
}
return (
<div className="tabs">
<div className="tabs" onMouseMove={handleMouseMove}>
<div className="tabs-spacer-before"/>
<div className="tabs-holder">
{ tabs.map((tab, index) => renderTab(tab, index)) }
<div className="tabs-holder" ref={tabsHolderRef}>
{tabs.map((tab, index) => renderTab(tab, index))}
</div>
<div className="tabs-spacer-after"/>
</div>
</div>
);
});
export default TabBar;
export default TabBar;

View file

@ -23,6 +23,7 @@
Components.utils.import('resource://gre/modules/Services.jsm');
</script>
<!-- TODO: Localize -->
<tooltip id="iframeTooltip" onpopupshowing="if (tooltipTitleNode = document.tooltipNode.closest('*[title]')) {this.setAttribute('label', tooltipTitleNode.getAttribute('title')); return true; } return false"/>
<menubar>
<menu label="File">
<menupopup>
@ -62,22 +63,13 @@
<hbox flex="1">
<vbox id="zotero-reader" flex="3">
<browser id="reader"
tooltip="readerTooltip"
tooltip="iframeTooltip"
type="content"
primary="true"
transparent="transparent"
src="resource://zotero/pdf-reader/viewer.html"
flex="1"/>
<popupset>
<tooltip id="readerTooltip" onpopupshowing="return fillTooltip(this);"/>
<menupopup id="tagsPopup" ignorekeys="true"
style="min-width: 300px;"
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }"
onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }">
<tagsbox id="tags" flex="1" mode="edit"/>
</menupopup>
<menupopup id="annotationPopup"/>
<menupopup id="colorPopup"/>
<popupset id="zotero-reader-popupset">
</popupset>
</vbox>
<splitter id="zotero-reader-splitter"

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK *****
*/
"use strict";
'use strict';
// Using 'import' breaks hooks
var React = require('react');
@ -31,104 +31,185 @@ var ReactDOM = require('react-dom');
import TabBar from 'components/tabBar';
var Zotero_Tabs = new function () {
const HTML_NS = 'http://www.w3.org/1999/xhtml';
Object.defineProperty(this, 'selectedIndex', {
get: () => this._selectedIndex
Object.defineProperty(this, 'selectedID', {
get: () => this._selectedID
});
Object.defineProperty(this, 'selectedIndex', {
get: () => this._getTab(this._selectedID).tabIndex
});
Object.defineProperty(this, 'deck', {
get: () => document.getElementById('tabs-deck')
});
this._tabBarRef = {};
this._tabs = [
{
title: "",
type: 'library'
}
];
this._selectedIndex = 0;
this._tabBarRef = React.createRef();
this._tabs = [{
id: 'zotero-pane',
type: 'library',
title: ''
}];
this._selectedID = 'zotero-pane';
this._getTab = function (id) {
var tabIndex = this._tabs.findIndex(tab => tab.id == id);
return { tab: this._tabs[tabIndex], tabIndex };
};
this._update = function () {
this._tabBarRef.current.setTabs(this._tabs.map(tab => ({
id: tab.id,
type: tab.type,
title: tab.title,
selected: tab.id == this._selectedID
})));
};
this.init = function () {
ReactDOM.render(
<TabBar
ref={this._tabBarRef}
initialTabs={this._tabs}
onTabSelected={this._onTabSelected.bind(this)}
onTabClosed={this._onTabClosed.bind(this)}
onTabSelect={this.select.bind(this)}
onTabMove={this.move.bind(this)}
onTabClose={this.close.bind(this)}
/>,
document.getElementById('tab-bar-container')
document.getElementById('tab-bar-container'),
() => {
this._update();
}
);
};
this.selectLeft = function () {
this._tabBarRef.current.selectLeft();
};
this.selectRight = function () {
this._tabBarRef.current.selectRight();
};
this.select = function (index) {
this._tabBarRef.current.select(index);
},
/**
* @return {Element} - The element created in the deck
* Add a new tab
*
* @param {String} type
* @param {String} title
* @param {Integer} index
* @param {Boolean} select
* @param {Function} onClose
* @return {{ id: string, container: XULElement}} id - tab id, container - a new tab container created in the deck
*/
this.add = function ({ title, type, url, index }) {
this._tabBarRef.current.add({ title, type });
var elem;
if (url) {
elem = document.createElement('iframe');
elem.setAttribute('type', 'content');
elem.setAttribute('src', url);
this.add = function ({ type, title, index, select, onClose }) {
if (typeof type != 'string') {
throw new Error(`'type' should be a string (was ${typeof type})`);
}
else {
elem = document.createElementNS(HTML_NS, 'div');
elem.textContent = title;
if (typeof title != 'string') {
throw new Error(`'title' should be a string (was ${typeof title})`);
}
var deck = this.deck;
deck.insertBefore(elem, index === undefined ? null : deck.childNodes[index]);
return elem;
};
this.rename = function (title, index) {
if (index === undefined) {
index = this._selectedIndex;
if (index !== undefined && (!Number.isInteger(index) || index < 1)) {
throw new Error(`'index' should be an integer > 0 (was ${index} (${typeof index})`);
}
this._tabs[index].title = title;
this._tabBarRef.current.rename(title, index);
};
this.close = function (index) {
if (index === undefined) {
index = this._selectedIndex;
if (onClose !== undefined && typeof onClose != 'function') {
throw new Error(`'onClose' should be a function (was ${typeof onClose})`);
}
this._tabBarRef.current.close(index);
var id = 'tab-' + Zotero.Utilities.randomString();
var container = document.createElement('vbox');
container.id = id;
this.deck.appendChild(container);
var tab = { id, type, title, onClose };
index = index || this._tabs.length;
this._tabs.splice(index, 0, tab);
this._update();
if (select) {
this.select(id);
}
return { id, container };
};
this._onTabSelected = function (index) {
this._selectedIndex = index;
this.deck.selectedIndex = index;
/**
* Set a new tab title
*
* @param {String} id
* @param {String} title
*/
this.rename = function (id, title) {
if (typeof title != 'string') {
throw new Error(`'title' should be a string (was ${typeof title})`);
}
var { tab } = this._getTab(id);
if (!tab) {
return;
}
tab.title = title;
this._update();
};
this._onTabClosed = function (index) {
this._tabs.splice(index, 1);
var deck = this.deck;
deck.removeChild(deck.childNodes[index]);
/**
* Close a tab
*
* @param {String} id
*/
this.close = function (id) {
var { tab, tabIndex } = this._getTab(id || this._selectedID);
if (tabIndex == 0) {
throw new Error('Library tab cannot be closed');
}
if (!tab) {
return;
}
this.select((this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id);
this._tabs.splice(tabIndex, 1);
document.getElementById(tab.id).remove();
if (tab.onClose) {
tab.onClose();
}
this._update();
};
};
/**
* Move a tab to the specified index
*
* @param {String} id
* @param {Integer} newIndex
*/
this.move = function (id, newIndex) {
if (!Number.isInteger(newIndex) || newIndex < 1) {
throw new Error(`'newIndex' should be an interger > 0 (was ${newIndex} (${typeof newIndex})`);
}
var { tab, tabIndex } = this._getTab(id);
if (tabIndex == 0) {
throw new Error('Library tab cannot be moved');
}
if (!tab || tabIndex == newIndex) {
return;
}
if (newIndex > tabIndex) {
newIndex--;
}
this._tabs.splice(tabIndex, 1);
this._tabs.splice(newIndex, 0, tab);
this._update();
};
/**
* Select a tab
*
* @param {String} id
*/
this.select = function (id) {
var { tab } = this._getTab(id);
if (!tab) {
return;
}
this._selectedID = id;
this.deck.selectedIndex = Array.from(this.deck.children).findIndex(x => x.id == id);
this._update();
};
/**
* Select the previous tab (closer to the library tab)
*/
this.selectPrev = function () {
var { tabIndex } = this._getTab(this._selectedID);
this.select((this._tabs[tabIndex - 1] || this._tabs[this._tabs.length - 1]).id);
};
/**
* Select the next tab (farther to the library tab)
*/
this.selectNext = function () {
var { tabIndex } = this._getTab(this._selectedID);
this.select((this._tabs[tabIndex + 1] || this._tabs[0]).id);
};
};

View file

@ -249,9 +249,9 @@ class PDFWorker {
else {
Zotero.PDF.hasUnmachedAnnotations[itemID] = !!annotations.length;
}
for (let readerWindow of Zotero.Reader._readerWindows) {
if (readerWindow._itemID === itemID) {
readerWindow.toggleImportPrompt(!!Zotero.PDF.hasUnmachedAnnotations[itemID]);
for (let reader of Zotero.Reader._readers) {
if (reader._itemID === itemID) {
reader.toggleImportPrompt(!!Zotero.PDF.hasUnmachedAnnotations[itemID]);
}
}
Zotero.PDF.dateChecked[itemID] = Zotero.Date.dateToISO(new Date());

View file

@ -4,7 +4,7 @@ Zotero.PDF = {
hasUnmachedAnnotations: {}
};
class ReaderWindow {
class ReaderInstance {
constructor() {
this.pdfStateFileName = '.zotero-pdf-state';
this.annotationItemIDs = [];
@ -18,66 +18,6 @@ class ReaderWindow {
this._isReaderInitialized = false;
}
init() {
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (!win) return;
this._window = win.open(
'chrome://zotero/content/reader.xul', '', 'chrome,resizable'
);
this._window.addEventListener('DOMContentLoaded', (event) => {
this._window.addEventListener('dragover', this._handleDragOver, true);
this._window.addEventListener('drop', this._handleDrop, true);
this._window.addEventListener('keypress', this._handleKeyPress);
this._window.fillTooltip = (tooltip) => {
let node = this._window.document.tooltipNode.closest('*[title]');
if (!node) {
return false;
}
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
};
this._window.menuCmd = (cmd) => {
if (cmd === 'export') {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this._itemID);
return;
}
let data = {
action: 'menuCmd',
cmd
};
this._postMessage(data);
};
let readerIframe = this._window.document.getElementById('reader');
if (!(readerIframe && readerIframe.contentWindow && readerIframe.contentWindow.document === event.target)) {
return;
}
let editor = this._window.document.getElementById('zotero-reader-editor');
editor.navigateHandler = async (uri, location) => {
let item = await Zotero.URI.getURIItem(uri);
if (!item) {
return;
}
if (item.id === this._itemID) {
this.navigate(location);
}
else {
await this.open({
itemID: item.id,
location
});
}
};
this._iframeWindow = this._window.document.getElementById('reader').contentWindow;
this._iframeWindow.addEventListener('message', this._handleMessage);
});
}
async open({ itemID, state, location, skipHistory }) {
if (itemID === this._itemID) {
return false;
@ -86,9 +26,6 @@ class ReaderWindow {
if (!item) {
return false;
}
let path = await item.getFilePathAsync();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
if (this._itemID && !skipHistory) {
this._prevHistory.push({
itemID: this._itemID,
@ -97,15 +34,10 @@ class ReaderWindow {
this._nextHistory = [];
}
this._itemID = item.id;
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = await Zotero.Items.getAsync(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
this.updateTitle();
let path = await item.getFilePathAsync();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
@ -125,7 +57,7 @@ class ReaderWindow {
}, [buf]);
return true;
}
updateTitle() {
let item = Zotero.Items.get(this._itemID);
let title = item.getField('title');
@ -136,7 +68,7 @@ class ReaderWindow {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
this._setTitleValue(title);
}
async setAnnotations(ids) {
@ -166,17 +98,13 @@ class ReaderWindow {
this._postMessage({ action: 'toggleImportPrompt', enable });
}
close() {
this._window.close();
}
async _saveState(state) {
let item = Zotero.Items.get(this._itemID);
let file = Zotero.Attachments.getStorageDirectory(item);
file.append(this.pdfStateFileName);
await Zotero.File.putContentsAsync(file, JSON.stringify(state));
}
async _loadState() {
// TODO: Validate data to make sure the older format doesn't crash the future pdf-reader
let item = Zotero.Items.get(this._itemID);
@ -188,7 +116,8 @@ class ReaderWindow {
let state = JSON.parse(await Zotero.File.getContentsAsync(file));
return state;
}
} catch (e) {
}
catch (e) {
Zotero.logError(e);
}
return null;
@ -228,12 +157,25 @@ class ReaderWindow {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect shape-rendering="geometricPrecision" fill="${fill}" stroke-width="2" x="2" y="2" stroke="${stroke}" width="12" height="12" rx="3"/></svg>`;
}
_openTagsPopup(x, y, item) {
let menupopup = this._window.document.createElement('menupopup');
menupopup.style.minWidth = '300px';
menupopup.setAttribute('ignorekeys', true);
let tagsbox = this._window.document.createElement('tagsbox');
menupopup.appendChild(tagsbox);
tagsbox.setAttribute('flex', '1');
this._popupset.appendChild(menupopup);
menupopup.openPopupAtScreen(x, y, false);
tagsbox.mode = 'edit';
tagsbox.item = item;
}
_openAnnotationPopup(x, y, annotationId, colors, selectedColor) {
let popup = this._window.document.getElementById('annotationPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let popup = this._window.document.createElement('menupopup');
this._popupset.appendChild(popup);
popup.addEventListener('popuphidden', function () {
popup.remove();
});
let menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', 'Delete');
menuitem.addEventListener('command', () => {
@ -266,11 +208,11 @@ class ReaderWindow {
}
_openColorPopup(x, y, colors, selectedColor) {
let popup = this._window.document.getElementById('colorPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let popup = this._window.document.createElement('menupopup');
this._popupset.appendChild(popup);
popup.addEventListener('popuphidden', function () {
popup.remove();
});
let menuitem;
for (let color of colors) {
menuitem = this._window.document.createElement('menuitem');
@ -389,8 +331,7 @@ class ReaderWindow {
let libraryID = attachment.libraryID;
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
if (annotation) {
this._window.document.getElementById('tags').item = annotation;
this._window.document.getElementById('tagsPopup').openPopupAtScreen(x, y, false);
this._openTagsPopup(x, y, annotation);
}
return;
}
@ -449,6 +390,183 @@ class ReaderWindow {
}
}
async _waitForReader() {
if (this._isReaderInitialized) {
return;
}
let n = 0;
while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) {
if (n >= 500) {
throw new Error('Waiting for reader failed');
}
await Zotero.Promise.delay(10);
n++;
}
this._isReaderInitialized = true;
}
/**
* Return item JSON in the pdf-reader ready format
* @param itemID
* @returns {Object|null}
*/
async _getAnnotation(itemID) {
try {
let item = Zotero.Items.get(itemID);
if (!item || !item.isAnnotation()) {
return null;
}
// TODO: Remve when fixed
item._loaded.childItems = true;
item = await Zotero.Annotations.toJSON(item);
item.id = item.key;
item.image = item.image;
delete item.key;
for (let key in item) {
item[key] = item[key] || '';
}
item.tags = item.tags || [];
return item;
}
catch (e) {
Zotero.logError(e);
return null;
}
}
}
class ReaderTab extends ReaderInstance {
constructor() {
super();
this._window = Services.wm.getMostRecentWindow('navigator:browser');
let { id, container } = this._window.Zotero_Tabs.add({
type: 'reader',
title: '',
select: true,
onClose: () => {
this.tabID = null;
this.close();
}
});
this.tabID = id;
this._tabContainer = container;
this._iframe = this._window.document.createElement('iframe');
this._iframe.setAttribute('flex', '1');
this._iframe.setAttribute('type', 'content');
this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/viewer.html');
this._tabContainer.appendChild(this._iframe);
this._popupset = this._window.document.createElement('popupset');
this._tabContainer.appendChild(this._popupset);
this._window.addEventListener('DOMContentLoaded', (event) => {
if (this._iframe && this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) {
this._iframeWindow = this._iframe.contentWindow;
this._iframeWindow.addEventListener('message', this._handleMessage);
}
});
this._iframe.setAttribute('tooltip', 'iframeTooltip');
}
close() {
if (this.onClose) {
this.onClose();
}
if (this.tabID) {
this._window.Zotero_Tabs.close(this.tabID);
}
}
_setTitleValue(title) {
this._window.Zotero_Tabs.rename(this.tabID, title);
}
}
class ReaderWindow extends ReaderInstance {
constructor() {
super();
this.init();
}
init() {
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (!win) return;
this._window = win.open(
'chrome://zotero/content/reader.xul', '', 'chrome,resizable'
);
this._window.addEventListener('DOMContentLoaded', (event) => {
if (event.target === this._window.document) {
this._window.addEventListener('dragover', this._handleDragOver, true);
this._window.addEventListener('drop', this._handleDrop, true);
this._window.addEventListener('keypress', this._handleKeyPress);
this._popupset = this._window.document.getElementById('zotero-reader-popupset');
this._window.menuCmd = (cmd) => {
if (cmd === 'export') {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this._itemID);
return;
}
let data = {
action: 'menuCmd',
cmd
};
this._postMessage(data);
};
let editor = this._window.document.getElementById('zotero-reader-editor');
editor.navigateHandler = async (uri, location) => {
let item = await Zotero.URI.getURIItem(uri);
if (!item) {
return;
}
if (item.id === this._itemID) {
this.navigate(location);
}
else {
await this.open({
itemID: item.id,
location
});
}
};
this._iframe = this._window.document.getElementById('reader');
}
if (this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) {
this._iframeWindow = this._window.document.getElementById('reader').contentWindow;
this._iframeWindow.addEventListener('message', this._handleMessage);
}
});
}
close() {
this._window.close();
}
updateTitle() {
let item = Zotero.Items.get(this._itemID);
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = Zotero.Items.get(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
}
_handleDragOver = (event) => {
if (event.dataTransfer.getData('zotero/item')) {
event.preventDefault();
@ -512,60 +630,18 @@ class ReaderWindow {
}
}
async _waitForReader() {
if (this._isReaderInitialized) {
return;
}
let n = 0;
while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) {
if (n >= 500) {
throw new Error('Waiting for reader failed');
}
await Zotero.Promise.delay(10);
n++;
}
this._isReaderInitialized = true;
}
/**
* Return item JSON in the pdf-reader ready format
* @param itemID
* @returns {Object|null}
*/
async _getAnnotation(itemID) {
try {
let item = Zotero.Items.get(itemID);
if (!item || !item.isAnnotation()) {
return null;
}
// TODO: Remve when fixed
item._loaded.childItems = true;
item = await Zotero.Annotations.toJSON(item);
item.id = item.key;
item.image = item.image;
delete item.key;
for (let key in item) {
item[key] = item[key] || '';
}
item.tags = item.tags || [];
return item;
}
catch (e) {
Zotero.logError(e);
return null;
}
}
}
class Reader {
constructor() {
this._readerWindows = [];
this._readers = [];
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader');
}
notify(event, type, ids, extraData) {
// Listen for the parent item, PDF attachment and its annotation items updates
for (let readerWindow of this._readerWindows) {
for (let readerWindow of this._readers) {
if (event === 'delete') {
let disappearedIds = readerWindow.annotationItemIDs.filter(x => ids.includes(x));
if (disappearedIds.length) {
@ -608,36 +684,55 @@ class Reader {
}
}
_getReaderWindow(itemID) {
return this._readerWindows.find(v => v._itemID === itemID);
}
async openURI(itemURI, location) {
async openURI(itemURI, location, openWindow) {
let item = await Zotero.URI.getURIItem(itemURI);
if (!item) return;
this.open(item.id, location);
await this.open(item.id, location, openWindow);
}
async open(itemID, location) {
async open(itemID, location, openWindow) {
this.triggerAnnotationsImportCheck(itemID);
let reader = this._getReaderWindow(itemID);
let reader;
if (openWindow) {
reader = this._readers.find(r => r._itemID === itemID && (r instanceof ReaderWindow));
}
else {
reader = this._readers.find(r => r._itemID === itemID);
}
if (reader) {
if (location) {
reader.navigate(location);
}
}
else {
else if (openWindow) {
reader = new ReaderWindow();
reader.init();
if (!(await reader.open({ itemID, location }))) {
return;
}
this._readerWindows.push(reader);
this._readers.push(reader);
reader._window.addEventListener('unload', () => {
this._readerWindows.splice(this._readerWindows.indexOf(reader), 1);
this._readers.splice(this._readers.indexOf(reader), 1);
});
}
reader._window.focus();
else {
reader = new ReaderTab();
if (!(await reader.open({ itemID, location }))) {
return;
}
this._readers.push(reader);
reader.onClose = () => {
this._readers.splice(this._readers.indexOf(reader), 1);
};
}
if (reader instanceof ReaderWindow) {
reader._window.focus();
}
else {
reader._window.Zotero_Tabs.select(reader.tabID);
}
}
async triggerAnnotationsImportCheck(itemID) {

View file

@ -526,6 +526,21 @@ var ZoteroPane = new function()
* Trigger actions based on keyboard shortcuts
*/
function handleKeyDown(event, from) {
// Close current tab
if (event.key == 'w') {
let close = Zotero.isMac
? (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey)
: (event.ctrlKey && !event.shiftKey && !event.altKey);
if (close) {
if (Zotero_Tabs.selectedIndex > 0) {
Zotero_Tabs.close();
event.preventDefault();
event.stopPropagation();
}
return;
}
}
try {
// Ignore keystrokes outside of Zotero pane
if (!(event.originalTarget.ownerDocument instanceof XULDocument)) {
@ -547,30 +562,15 @@ var ZoteroPane = new function()
let ctrlOnly = event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
if (ctrlOnly) {
if (event.key == 'PageUp') {
Zotero_Tabs.selectLeft();
Zotero_Tabs.selectPrev();
}
else if (event.key == 'PageDown') {
Zotero_Tabs.selectRight();
Zotero_Tabs.selectNext();
}
event.preventDefault();
return;
}
// Close current tab
if (event.key == 'w') {
let close = Zotero.isMac
? (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey)
: (event.ctrlKey && !event.shiftKey && !event.altKey);
if (close) {
if (Zotero_Tabs.selectedIndex > 0) {
Zotero_Tabs.close();
event.preventDefault();
event.stopPropagation();
}
return;
}
}
// Highlight collections containing selected items
//
// We use Control (17) on Windows because Alt triggers the menubar;
@ -1197,8 +1197,7 @@ var ZoteroPane = new function()
}
// Rename tab
// TODO: What if PDF is in front?
Zotero_Tabs.rename(collectionTreeRow.getName());
Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName());
// Clear quick search and tag selector when switching views
document.getElementById('zotero-tb-search').value = "";
@ -4078,8 +4077,7 @@ var ZoteroPane = new function()
var launchFile = async (path, contentType, itemID) => {
// Custom PDF handler
if (contentType === 'application/pdf') {
this.viewPDF(itemID);
// TODO: Still leave an option to use an external PDF viewer
this.viewPDF(itemID, event.shiftKey);
return;
let pdfHandler = Zotero.Prefs.get("fileHandler.pdf");
if (pdfHandler) {
@ -4246,8 +4244,8 @@ var ZoteroPane = new function()
}
});
this.viewPDF = function (itemID) {
Zotero.Reader.open(itemID);
this.viewPDF = function (itemID, openWindow) {
Zotero.Reader.open(itemID, null, openWindow);
};

View file

@ -231,6 +231,8 @@
</vbox>
<popupset>
<!-- Allows iframes to show a tooltip popup for nodes with titles. `tooltip="iframeTooltip"` attribute has to be set for the iframe -->
<tooltip id="iframeTooltip" onpopupshowing="if (tooltipTitleNode = document.tooltipNode.closest('*[title]')) {this.setAttribute('label', tooltipTitleNode.getAttribute('title')); return true; } return false"/>
<menupopup id="zotero-collectionmenu"
oncommand="ZoteroPane.onCollectionContextMenuSelect(event)">
<!-- Keep order in sync with buildCollectionContextMenu, which adds additional attributes -->

View file

@ -12,16 +12,16 @@
.tab {
-moz-appearance: none;
width: 200px;
max-width: 200px;
flex: 1;
height: 30px;
line-height: 30px;
display: flex;
justify-content: center;
align-content: center;
position: relative;
background: #f9f9f9;
border-top: 2px solid transparent;
color: #000;
text-align: center;
padding: 0 30px;
&:not(:last-child) {
border-right: $tab-border;
@ -32,8 +32,12 @@
}
.tab-name {
margin-top: -2px;
line-height: 30px;
overflow-x: hidden;
text-overflow: ellipsis;
display: -moz-box;
position: relative;
top: -2px;
}
.tab-close {

3
scss/linux/_tabBar.scss Normal file
View file

@ -0,0 +1,3 @@
.tabs-holder {
flex: 1;
}

View file

@ -7,4 +7,5 @@
@import "linux/createParent";
@import "linux/editable";
@import "linux/search";
@import "linux/tabBar";
@import "linux/tagsBox";