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:
Adomas Ven 2021-08-30 13:01:08 +03:00 committed by GitHub
parent 6c5e35ad73
commit cb9df902bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 96 deletions

View file

@ -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.)
*/

View file

@ -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;
}

View file

@ -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");

View file

@ -51,6 +51,7 @@ function init() {
multiSelect={true}
columns={columns}
disableFontSizeScaling={true}
getRowString={index => getRowData(index).name}
onActivate={handleActivate}
/>
</IntlProvider>

View file

@ -129,6 +129,7 @@ Zotero_Preferences.Cite = {
disableFontSizeScaling={true}
onSelectionChange={() => document.getElementById('styleManager-delete').disabled = undefined}
onKeyDown={handleKeyDown}
getRowString={index => styles[index].title}
/>
</IntlProvider>
);

View file

@ -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>

View file

@ -322,6 +322,7 @@ Zotero_Preferences.Sync = {
showHeader={true}
columns={columns}
staticColumns={true}
getRowString={index => this._rows[index].name}
disableFontSizeScaling={true}
onKeyDown={handleKeyDown}
/>