Tab bar & Reader: Rewrite and connect everything
This commit is contained in:
parent
edd4f27e09
commit
2f505862d7
10 changed files with 526 additions and 369 deletions
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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
3
scss/linux/_tabBar.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.tabs-holder {
|
||||
flex: 1;
|
||||
}
|
|
@ -7,4 +7,5 @@
|
|||
@import "linux/createParent";
|
||||
@import "linux/editable";
|
||||
@import "linux/search";
|
||||
@import "linux/tabBar";
|
||||
@import "linux/tagsBox";
|
||||
|
|
Loading…
Reference in a new issue