8d3adb6f46
And fix shortcut key for selecting library
260 lines
7.2 KiB
JavaScript
260 lines
7.2 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2020 Corporation for Digital Scholarship
|
|
Vienna, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
const { TreeSelectionStub } = require('components/virtualized-table');
|
|
const React = require('react');
|
|
|
|
/**
|
|
* Common methods for Zotero.ItemTree and Zotero.CollectionTree
|
|
* @type {Zotero.LibraryTree}
|
|
*/
|
|
var LibraryTree = class LibraryTree extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this._rows = [];
|
|
this._rowMap = {};
|
|
|
|
this.domEl = props.domEl;
|
|
this._ownerDocument = props.domEl.ownerDocument;
|
|
|
|
this.onSelect = this.createEventBinding('select');
|
|
this.onRefresh = this.createEventBinding('refresh');
|
|
}
|
|
|
|
get window() {
|
|
return this._ownerDocument.defaultView;
|
|
}
|
|
|
|
get selection() {
|
|
return this.tree ? this.tree.selection : TreeSelectionStub;
|
|
}
|
|
|
|
get rowCount() {
|
|
return this._rows.length;
|
|
}
|
|
|
|
waitForSelect() {
|
|
return this._waitForEvent('select');
|
|
}
|
|
|
|
componentDidCatch(error, info) {
|
|
// Async operations might attempt to update the react components
|
|
// after window close in tests, which will cause unnecessary crashing
|
|
// so we set an unintialized flag that we check in select functions
|
|
// like #notify
|
|
if (this._uninitialized) return;
|
|
Zotero.debug("ItemTree: React threw an error");
|
|
Zotero.logError(error);
|
|
Zotero.debug(info);
|
|
if (this.type == 'item') Zotero.Prefs.clear('lastViewedFolder');
|
|
Zotero.crash();
|
|
}
|
|
|
|
focus() {
|
|
this.tree.focus();
|
|
// If no rows selected, select first visible row
|
|
if (!this.tree.selection.count) {
|
|
this.tree.selection.select(
|
|
// TODO: Return -1 when no rows, and skip selection here
|
|
this.tree._jsWindow.getFirstVisibleRow()
|
|
);
|
|
}
|
|
}
|
|
|
|
getParentIndex = (index) => {
|
|
var thisLevel = this.getLevel(index);
|
|
if (thisLevel == 0) return -1;
|
|
for (var i = index - 1; i >= 0; i--) {
|
|
if (this.getLevel(i) < thisLevel) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
getLevel(index) {
|
|
return this._rows[index].level;
|
|
}
|
|
|
|
/**
|
|
* Return a reference to the tree row at a given row
|
|
*
|
|
* @return {TreeRow}
|
|
*/
|
|
getRow(index) {
|
|
return this._rows[index];
|
|
}
|
|
|
|
/**
|
|
* Return the index of the row with a given ID (e.g., "C123" for collection 123)
|
|
*
|
|
* @param {String} - Row id
|
|
* @return {Integer|false}
|
|
*/
|
|
getRowIndexByID(id) {
|
|
if (!(id in this._rowMap)) {
|
|
Zotero.debug(`${this.name}: Trying to access a row with invalid ID ${id}`)
|
|
return false;
|
|
}
|
|
return this._rowMap[id];
|
|
}
|
|
|
|
/**
|
|
* Add a tree row to the main array, update the row count, tell the treebox that the row
|
|
* count changed, and update the row map
|
|
*
|
|
* @param {TreeRow} treeRow
|
|
* @param {Number} [beforeRow] - Row index to insert new row before
|
|
*/
|
|
_addRow(treeRow, beforeRow, skipRowMapRefresh) {
|
|
this._rows.splice(beforeRow, 0, treeRow);
|
|
if (!skipRowMapRefresh) {
|
|
// Increment all rows in map at or above insertion point
|
|
for (let i in this._rowMap) {
|
|
if (this._rowMap[i] >= beforeRow) {
|
|
this._rowMap[i]++;
|
|
}
|
|
}
|
|
// Add new row to map
|
|
this._rowMap[treeRow.id] = beforeRow;
|
|
}
|
|
}
|
|
|
|
_removeRows(rows) {
|
|
rows = Zotero.Utilities.arrayUnique(rows);
|
|
rows.sort((a, b) => a - b);
|
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
this._removeRow(rows[i], true);
|
|
}
|
|
this._refreshRowMap();
|
|
}
|
|
|
|
/**
|
|
* Remove a row from the main array and parent row children arrays,
|
|
* delete the row from the map, and optionally update all rows above it in the map
|
|
*/
|
|
_removeRow(index, skipMapUpdate) {
|
|
var id = this.getRow(index).id;
|
|
let level = this.getLevel(index);
|
|
|
|
if (index <= this.selection.focused) {
|
|
this.selection.select(this.selection.focused - 1);
|
|
}
|
|
|
|
this._rows.splice(index, 1);
|
|
if (index != 0
|
|
&& this.getLevel(index - 1) < level
|
|
&& (!this._rows[index] || this.getLevel(index) != level)) {
|
|
this._rows[index - 1].isOpen = false;
|
|
}
|
|
|
|
delete this._rowMap[id];
|
|
if (!skipMapUpdate) {
|
|
for (let i in this._rowMap) {
|
|
if (this._rowMap[i] > index) {
|
|
this._rowMap[i]--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_refreshRowMap() {
|
|
var rowMap = {};
|
|
for (var i = 0; i < this.rowCount; i++) {
|
|
let row = this.getRow(i);
|
|
let id = row.id;
|
|
if (rowMap[id] !== undefined) {
|
|
Zotero.debug(`WARNING: _refreshRowMap(): ${this.type} row ${rowMap[id]} already found for item ${id} at ${i}`, 2);
|
|
Zotero.debug(new Error().stack, 2);
|
|
}
|
|
rowMap[id] = i;
|
|
}
|
|
this._rowMap = rowMap;
|
|
}
|
|
|
|
_onSelectionChange = () => {
|
|
if (!this._uninitialized) {
|
|
this.props.onSelectionChange && this.props.onSelectionChange(this.selection);
|
|
}
|
|
}
|
|
|
|
_onSelectionChangeDebounced = Zotero.Utilities.debounce(this._onSelectionChange, 100)
|
|
|
|
handleTwistyMouseUp = (event, index) => {
|
|
this.toggleOpenState(index);
|
|
event.stopPropagation();
|
|
this.tree.focus();
|
|
}
|
|
|
|
// The caller has to ensure the tree is redrawn
|
|
ensureRowIsVisible(index) {
|
|
this.tree && this.tree.scrollToRow(index);
|
|
}
|
|
|
|
_updateHeight = () => {
|
|
this.forceUpdate(() => {
|
|
if (this.tree) {
|
|
this.tree.rerender();
|
|
}
|
|
});
|
|
}
|
|
|
|
updateHeight = Zotero.Utilities.debounce(this._updateHeight, 200);
|
|
|
|
updateFontSize() {
|
|
this.tree.updateFontSize();
|
|
}
|
|
|
|
setDropEffect(event, effect) {
|
|
// On Windows (in Fx26), Firefox uses 'move' for unmodified drags
|
|
// and 'copy'/'link' for drags with system-default modifier keys
|
|
// as long as the actions are allowed by the initial effectAllowed set
|
|
// in onDragStart, regardless of the effectAllowed or dropEffect set
|
|
// in onDragOver. It doesn't seem to be possible to use 'copy' for
|
|
// the default and 'move' for modified, as we need to in the collections
|
|
// tree. To prevent inaccurate cursor feedback, we set effectAllowed to
|
|
// 'copy' in onDragStart, which locks the cursor at 'copy'. ('none' still
|
|
// changes the cursor, but 'move'/'link' do not.) It'd be better to use
|
|
// the unadorned 'move', but we use 'copy' instead because with 'move' text
|
|
// can't be dragged to some external programs (e.g., Chrome, Notepad++),
|
|
// which seems worse than always showing 'copy' feedback.
|
|
//
|
|
// However, since effectAllowed is enforced, leaving it at 'copy'
|
|
// would prevent our modified 'move' in the collections tree from working,
|
|
// so we also have to set effectAllowed here (called from onDragOver) to
|
|
// the same action as the dropEffect. This allows the dropEffect setting
|
|
// (which we use in the tree's canDrop() and drop() to determine the desired
|
|
// action) to be changed, even if the cursor doesn't reflect the new setting.
|
|
if (Zotero.isWin || Zotero.isLinux) {
|
|
event.dataTransfer.effectAllowed = effect;
|
|
}
|
|
event.dataTransfer.dropEffect = effect;
|
|
}
|
|
};
|
|
|
|
Zotero.Utilities.Internal.makeClassEventDispatcher(LibraryTree);
|
|
|
|
module.exports = LibraryTree;
|
|
|