HTML Tree: make find as you type a virtualized table functionality (#2176)
Closes #2168, closes #2169. Adds find-as-you-type to locate, preference style and export formats managers. To enable find as you type you need to specify the getRowString(index) prop on the VirtualizedTable. See prop comment for more info.
This commit is contained in:
parent
6c5e35ad73
commit
cb9df902bc
7 changed files with 73 additions and 96 deletions
|
@ -35,7 +35,6 @@ const { getDragTargetOrient } = require('components/utils');
|
|||
const { Cc, Ci, Cu } = require('chrome');
|
||||
|
||||
const CHILD_INDENT = 15;
|
||||
const TYPING_TIMEOUT = 1000;
|
||||
|
||||
var CollectionTree = class CollectionTree extends LibraryTree {
|
||||
static async init(domEl, opts) {
|
||||
|
@ -89,7 +88,6 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this._editing = null;
|
||||
this._editingInput = null;
|
||||
this._dropRow = null;
|
||||
this._typingString = "";
|
||||
this._typingTimeout = null;
|
||||
|
||||
this.onLoad = this.createEventBinding('load', true, true);
|
||||
|
@ -153,9 +151,6 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
else if (event.key == "F2" && !Zotero.isMac && treeRow.isCollection()) {
|
||||
this.handleActivate(event, [this.selection.focused]);
|
||||
}
|
||||
else if (event.key.length == 1 && !(event.ctrlKey || event.metaKey || event.altKey)) {
|
||||
this.handleTyping(event.key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -205,48 +200,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
handleEditingChange = (event, index) => {
|
||||
this.getRow(index).editingName = event.target.value;
|
||||
}
|
||||
|
||||
async handleTyping(char) {
|
||||
char = char.toLowerCase();
|
||||
this._typingString += char;
|
||||
let allSameChar = true;
|
||||
for (let i = this._typingString.length - 1; i >= 0; i--) {
|
||||
if (char != this._typingString[i]) {
|
||||
allSameChar = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allSameChar) {
|
||||
for (let i = this.selection.focused + 1, checked = 0; checked < this._rows.length; i++, checked++) {
|
||||
i %= this._rows.length;
|
||||
let row = this.getRow(i);
|
||||
if (this.isSelectable(i) && row.getName().toLowerCase().indexOf(char) == 0) {
|
||||
if (i != this.selection.focused) {
|
||||
this.ensureRowIsVisible(i);
|
||||
await this.selectWait(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < this._rows.length; i++) {
|
||||
let row = this.getRow(i);
|
||||
if (this.isSelectable(i) && row.getName().toLowerCase().indexOf(this._typingString) == 0) {
|
||||
if (i != this.selection.focused) {
|
||||
this.ensureRowIsVisible(i);
|
||||
await this.selectWait(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
clearTimeout(this._typingTimeout);
|
||||
this._typingTimeout = setTimeout(() => {
|
||||
this._typingString = "";
|
||||
}, TYPING_TIMEOUT);
|
||||
}
|
||||
|
||||
|
||||
async commitEditingName() {
|
||||
let treeRow = this._editing;
|
||||
if (!treeRow.editingName) return;
|
||||
|
@ -385,6 +339,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
isContainerEmpty: this.isContainerEmpty,
|
||||
isContainerOpen: this.isContainerOpen,
|
||||
toggleOpenState: this.toggleOpenState,
|
||||
getRowString: this.getRowString.bind(this),
|
||||
|
||||
onItemContextMenu: (e) => this.props.onContextMenu && this.props.onContextMenu(e),
|
||||
|
||||
onKeyDown: this.handleKeyDown,
|
||||
|
@ -1112,6 +1068,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
return this.getRow(this.selection.focused).editable;
|
||||
}
|
||||
|
||||
getRowString(index) {
|
||||
return this.getRow(index).getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return libraryID of selected row (which could be a collection, etc.)
|
||||
*/
|
||||
|
|
|
@ -33,6 +33,7 @@ const Draggable = require('./draggable');
|
|||
const { injectIntl } = require('react-intl');
|
||||
const { IconDownChevron, getDOMElement } = require('components/icons');
|
||||
|
||||
const TYPING_TIMEOUT = 1000;
|
||||
const DEFAULT_ROW_HEIGHT = 20; // px
|
||||
const RESIZER_WIDTH = 5; // px
|
||||
|
||||
|
@ -286,6 +287,7 @@ class VirtualizedTable extends React.Component {
|
|||
this.state = {
|
||||
resizing: null
|
||||
};
|
||||
this._typingString = "";
|
||||
this._jsWindowID = `virtualized-table-list-${Zotero.Utilities.randomString(5)}`;
|
||||
this._containerWidth = props.containerWidth || window.innerWidth;
|
||||
|
||||
|
@ -405,6 +407,10 @@ class VirtualizedTable extends React.Component {
|
|||
isContainerEmpty: PropTypes.func,
|
||||
isContainerOpen: PropTypes.func,
|
||||
toggleOpenState: PropTypes.func,
|
||||
|
||||
// A function with signature (index:Number) => result:String which will be used
|
||||
// for find-as-you-type navigation. Find-as-you-type is disabled if prop is undefined.
|
||||
getRowString: PropTypes.func,
|
||||
|
||||
// If you want to perform custom key handling it should be in this function
|
||||
// if it returns false then virtualized-table's own key handler won't run
|
||||
|
@ -559,10 +565,16 @@ class VirtualizedTable extends React.Component {
|
|||
break;
|
||||
|
||||
case " ":
|
||||
this.onSelection(this.selection.focused, false, true);
|
||||
if (this._typingString.length <= 0) {
|
||||
this.onSelection(this.selection.focused, false, true);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (this.props.getRowString && !(e.ctrlKey || e.metaKey) && e.key.length == 1) {
|
||||
this._handleTyping(e.key);
|
||||
}
|
||||
|
||||
if (shiftSelect || movePivot) return;
|
||||
|
||||
|
@ -597,6 +609,48 @@ class VirtualizedTable extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_handleTyping = (char) => {
|
||||
char = char.toLowerCase();
|
||||
this._typingString += char;
|
||||
let allSameChar = true;
|
||||
for (let i = this._typingString.length - 1; i >= 0; i--) {
|
||||
if (char != this._typingString[i]) {
|
||||
allSameChar = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const rowCount = this.props.getRowCount();
|
||||
if (allSameChar) {
|
||||
for (let i = this.selection.pivot + 1, checked = 0; checked < rowCount; i++, checked++) {
|
||||
i %= rowCount;
|
||||
let rowString = this.props.getRowString(i);
|
||||
if (rowString.toLowerCase().indexOf(char) == 0) {
|
||||
if (i != this.selection.pivot) {
|
||||
this.scrollToRow(i);
|
||||
this.onSelection(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
let rowString = this.props.getRowString(i);
|
||||
if (rowString.toLowerCase().indexOf(this._typingString) == 0) {
|
||||
if (i != this.selection.pivot) {
|
||||
this.scrollToRow(i);
|
||||
this.onSelection(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
clearTimeout(this._typingTimeout);
|
||||
this._typingTimeout = setTimeout(() => {
|
||||
this._typingString = "";
|
||||
}, TYPING_TIMEOUT);
|
||||
}
|
||||
|
||||
_onDragStart = () => {
|
||||
this._isMouseDrag = true;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ const { COLUMNS } = require('./itemTreeColumns');
|
|||
const { Cc, Ci, Cu } = require('chrome');
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
const TYPING_TIMEOUT = 1000;
|
||||
const CHILD_INDENT = 12;
|
||||
const COLORED_TAGS_RE = new RegExp("^[0-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1}$");
|
||||
const COLUMN_PREFS_FILEPATH = OS.Path.join(Zotero.Profile.dir, "treePrefs.json");
|
||||
|
@ -88,7 +87,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
this.type = 'item';
|
||||
this.name = 'ItemTree';
|
||||
|
||||
this._typingString = "";
|
||||
this._skipKeypress = false;
|
||||
this._initialized = false;
|
||||
|
||||
|
@ -824,47 +822,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
this.selection.selectEventsSuppressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleTyping(char) {
|
||||
char = char.toLowerCase();
|
||||
this._typingString += char;
|
||||
let allSameChar = true;
|
||||
for (let i = this._typingString.length - 1; i >= 0; i--) {
|
||||
if (char != this._typingString[i]) {
|
||||
allSameChar = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allSameChar) {
|
||||
for (let i = this.selection.pivot + 1, checked = 0; checked < this._rows.length; i++, checked++) {
|
||||
i %= this._rows.length;
|
||||
let row = this.getRow(i);
|
||||
if (row.getField('title').toLowerCase().indexOf(char) == 0) {
|
||||
if (i != this.selection.pivot) {
|
||||
this.ensureRowIsVisible(i);
|
||||
this.selectItem([row.ref.id]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < this._rows.length; i++) {
|
||||
let row = this.getRow(i);
|
||||
if (row.getField('title').toLowerCase().indexOf(this._typingString) == 0) {
|
||||
if (i != this.selection.pivot) {
|
||||
this.ensureRowIsVisible(i);
|
||||
this.selectItem([row.ref.id]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
clearTimeout(this._typingTimeout);
|
||||
this._typingTimeout = setTimeout(() => {
|
||||
this._typingString = "";
|
||||
}, TYPING_TIMEOUT);
|
||||
}
|
||||
|
||||
handleActivate = (event, indices) => {
|
||||
// Ignore double-clicks in duplicates view on everything except attachments
|
||||
|
@ -932,10 +889,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
this.collapseAllRows();
|
||||
return false;
|
||||
}
|
||||
else if (!(event.ctrlKey || event.metaKey || event.altKey) && event.key.length == 1 && (event.key != " " || this._typingString.length > 1)) {
|
||||
this.handleTyping(event.key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -998,6 +951,8 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
isContainerOpen: this.isContainerOpen,
|
||||
toggleOpenState: this.toggleOpenState,
|
||||
|
||||
getRowString: this.getRowString.bind(this),
|
||||
|
||||
onDragOver: e => this.props.dragAndDrop && this.onDragOver(e, -1),
|
||||
onDrop: e => this.props.dragAndDrop && this.onDrop(e, -1),
|
||||
onKeyDown: this.handleKeyDown,
|
||||
|
@ -1718,6 +1673,10 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return this._getRowData(index)[column];
|
||||
}
|
||||
|
||||
getRowString(index) {
|
||||
return this.getCellText(index, this.getSortField())
|
||||
}
|
||||
|
||||
async deleteSelection(force) {
|
||||
if (arguments.length > 1) {
|
||||
throw new Error("ItemTree.deleteSelection() no longer takes two parameters");
|
||||
|
|
|
@ -51,6 +51,7 @@ function init() {
|
|||
multiSelect={true}
|
||||
columns={columns}
|
||||
disableFontSizeScaling={true}
|
||||
getRowString={index => getRowData(index).name}
|
||||
onActivate={handleActivate}
|
||||
/>
|
||||
</IntlProvider>
|
||||
|
|
|
@ -129,6 +129,7 @@ Zotero_Preferences.Cite = {
|
|||
disableFontSizeScaling={true}
|
||||
onSelectionChange={() => document.getElementById('styleManager-delete').disabled = undefined}
|
||||
onKeyDown={handleKeyDown}
|
||||
getRowString={index => styles[index].title}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
|
|
@ -316,6 +316,7 @@ Zotero_Preferences.Export = {
|
|||
disableFontSizeScaling={true}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
getRowString={index => this._rows[index].domain}
|
||||
onActivate={(event, indices) => Zotero_Preferences.Export.showQuickCopySiteEditor()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
|
|
|
@ -322,6 +322,7 @@ Zotero_Preferences.Sync = {
|
|||
showHeader={true}
|
||||
columns={columns}
|
||||
staticColumns={true}
|
||||
getRowString={index => this._rows[index].name}
|
||||
disableFontSizeScaling={true}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue