From cb9df902bcf5ca8c454bc8bc3a844201baca37e5 Mon Sep 17 00:00:00 2001 From: Adomas Ven Date: Mon, 30 Aug 2021 13:01:08 +0300 Subject: [PATCH] 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. --- chrome/content/zotero/collectionTree.jsx | 54 +++-------------- .../zotero/components/virtualized-table.jsx | 58 ++++++++++++++++++- chrome/content/zotero/itemTree.jsx | 53 ++--------------- chrome/content/zotero/locateManager.jsx | 1 + .../zotero/preferences/preferences_cite.jsx | 1 + .../zotero/preferences/preferences_export.jsx | 1 + .../zotero/preferences/preferences_sync.jsx | 1 + 7 files changed, 73 insertions(+), 96 deletions(-) diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index 8e0c3bc8d2..8b79889644 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -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.) */ diff --git a/chrome/content/zotero/components/virtualized-table.jsx b/chrome/content/zotero/components/virtualized-table.jsx index 800ef59cf4..1193e8f5e2 100644 --- a/chrome/content/zotero/components/virtualized-table.jsx +++ b/chrome/content/zotero/components/virtualized-table.jsx @@ -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; } diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index 724cfb8f63..5b61aa80cc 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -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"); diff --git a/chrome/content/zotero/locateManager.jsx b/chrome/content/zotero/locateManager.jsx index 74bfc6dfcf..0297e6eb08 100644 --- a/chrome/content/zotero/locateManager.jsx +++ b/chrome/content/zotero/locateManager.jsx @@ -51,6 +51,7 @@ function init() { multiSelect={true} columns={columns} disableFontSizeScaling={true} + getRowString={index => getRowData(index).name} onActivate={handleActivate} /> diff --git a/chrome/content/zotero/preferences/preferences_cite.jsx b/chrome/content/zotero/preferences/preferences_cite.jsx index f492a6df7f..bf07acbf89 100644 --- a/chrome/content/zotero/preferences/preferences_cite.jsx +++ b/chrome/content/zotero/preferences/preferences_cite.jsx @@ -129,6 +129,7 @@ Zotero_Preferences.Cite = { disableFontSizeScaling={true} onSelectionChange={() => document.getElementById('styleManager-delete').disabled = undefined} onKeyDown={handleKeyDown} + getRowString={index => styles[index].title} /> ); diff --git a/chrome/content/zotero/preferences/preferences_export.jsx b/chrome/content/zotero/preferences/preferences_export.jsx index 4d59c33d8a..6176f2f951 100644 --- a/chrome/content/zotero/preferences/preferences_export.jsx +++ b/chrome/content/zotero/preferences/preferences_export.jsx @@ -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()} /> diff --git a/chrome/content/zotero/preferences/preferences_sync.jsx b/chrome/content/zotero/preferences/preferences_sync.jsx index 456ac5d2c7..af8a17ff99 100644 --- a/chrome/content/zotero/preferences/preferences_sync.jsx +++ b/chrome/content/zotero/preferences/preferences_sync.jsx @@ -322,6 +322,7 @@ Zotero_Preferences.Sync = { showHeader={true} columns={columns} staticColumns={true} + getRowString={index => this._rows[index].name} disableFontSizeScaling={true} onKeyDown={handleKeyDown} />