(core) Scrolling to the active record when editor is activated

Summary: When an editor is activated by typing, the active view should be scrolled to the active record.

Test Plan: new tests

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3196
pull/85/head
Jarosław Sadziński 2 years ago
parent 5cdc7b2ea4
commit 08881d9663

@ -246,8 +246,11 @@ _.extend(Base.prototype, BackboneEvents);
* These commands are common to GridView and DetailView. * These commands are common to GridView and DetailView.
*/ */
BaseView.commonCommands = { BaseView.commonCommands = {
input: function(input) { this.activateEditorAtCursor({init: input}); }, input: function(init) {
editField: function() { this.activateEditorAtCursor(); }, this.scrollToCursor(true).catch(reportError);
this.activateEditorAtCursor({init});
},
editField: function() { this.scrollToCursor(true); this.activateEditorAtCursor(); },
insertRecordBefore: function() { this.insertRow(this.cursor.rowIndex()); }, insertRecordBefore: function() { this.insertRow(this.cursor.rowIndex()); },
insertRecordAfter: function() { this.insertRow(this.cursor.rowIndex() + 1); }, insertRecordAfter: function() { this.insertRow(this.cursor.rowIndex() + 1); },
@ -695,8 +698,10 @@ BaseView.prototype.isFiltered = function() {
/** /**
* Makes sure that active record is in the view. * Makes sure that active record is in the view.
* @param {Boolean} sync If the scroll should be performed synchronously. For typing we should scroll synchronously,
* for other cases asynchronously as there might be some other operations pending (see doScrollChildIntoView in koDom).
*/ */
BaseView.prototype.revealActiveRecord = function() { BaseView.prototype.scrollToCursor = function() {
// to override // to override
return Promise.resolve(); return Promise.resolve();
}; };

@ -68,6 +68,7 @@ function DetailView(gristDoc, viewSectionModel) {
//-------------------------------------------------- //--------------------------------------------------
// Construct DOM // Construct DOM
this.scrollPane = null;
this.viewPane = this.autoDispose(this.buildDom()); this.viewPane = this.autoDispose(this.buildDom());
//-------------------------------------------------- //--------------------------------------------------
@ -286,7 +287,7 @@ DetailView.prototype.buildDom = function() {
}), }),
kd.maybe(() => !this.recordLayout.isEditingLayout(), () => { kd.maybe(() => !this.recordLayout.isEditingLayout(), () => {
if (!this._isSingle) { if (!this._isSingle) {
return dom('div.detailview_scroll_pane.flexitem', return this.scrollPane = dom('div.detailview_scroll_pane.flexitem',
kd.scrollChildIntoView(this.cursor.rowIndex), kd.scrollChildIntoView(this.cursor.rowIndex),
dom.onDispose(() => { dom.onDispose(() => {
// Save the previous scroll values to the section. // Save the previous scroll values to the section.
@ -414,4 +415,9 @@ DetailView.prototype._isAddRow = function(index = this.cursor.rowIndex()) {
return this.viewData.getRowId(index) === 'new'; return this.viewData.getRowId(index) === 'new';
}; };
DetailView.prototype.scrollToCursor = function(sync = true) {
if (!this.scrollPane) { return Promise.resolve(); }
return kd.doScrollChildIntoView(this.scrollPane, this.cursor.rowIndex(), sync);
}
module.exports = DetailView; module.exports = DetailView;

@ -267,7 +267,7 @@ GridView.gridCommands = {
fieldEditSave: function() { this.cursor.rowIndex(this.cursor.rowIndex() + 1); }, fieldEditSave: function() { this.cursor.rowIndex(this.cursor.rowIndex() + 1); },
// Re-define editField after fieldEditSave to make it take precedence for the Enter key. // Re-define editField after fieldEditSave to make it take precedence for the Enter key.
editField: function() { this.activateEditorAtCursor(); }, editField: function() { this.scrollToCursor(true); this.activateEditorAtCursor(); },
deleteRecords: function() { deleteRecords: function() {
const saved = this.cursor.getCursorPos(); const saved = this.cursor.getCursorPos();
@ -292,7 +292,10 @@ GridView.gridCommands = {
convertFormulasToData: function() { this._convertFormulasToData(this.getSelection()); }, convertFormulasToData: function() { this._convertFormulasToData(this.getSelection()); },
copy: function() { return this.copy(this.getSelection()); }, copy: function() { return this.copy(this.getSelection()); },
cut: function() { return this.cut(this.getSelection()); }, cut: function() { return this.cut(this.getSelection()); },
paste: function(pasteObj, cutCallback) { return this.paste(pasteObj, cutCallback); }, paste: async function(pasteObj, cutCallback) {
await this.paste(pasteObj, cutCallback);
await this.scrollToCursor(false);
},
cancel: function() { this.clearSelection(); }, cancel: function() { this.clearSelection(); },
sortAsc: function() { sortAsc: function() {
sortBy(this.viewSection.activeSortSpec, this.currentColumn().getRowId(), Sort.ASC); sortBy(this.viewSection.activeSortSpec, this.currentColumn().getRowId(), Sort.ASC);
@ -1478,8 +1481,8 @@ GridView.prototype.maybeSelectRow = function(elem, rowId) {
// End Context Menus // End Context Menus
GridView.prototype.revealActiveRecord = function() { GridView.prototype.scrollToCursor = function(sync = true) {
return kd.doScrollChildIntoView(this.scrollPane, this.cursor.rowIndex()); return kd.doScrollChildIntoView(this.scrollPane, this.cursor.rowIndex(), sync);
} }
// Helper to show tooltip over column selection in the full edit mode. // Helper to show tooltip over column selection in the full edit mode.

@ -72,7 +72,7 @@ declare module "app/client/components/BaseView" {
public onResize(): void; public onResize(): void;
public prepareToPrint(onOff: boolean): void; public prepareToPrint(onOff: boolean): void;
public moveEditRowToCursor(): DataRowModel; public moveEditRowToCursor(): DataRowModel;
public revealActiveRecord(): Promise<void>; public scrollToCursor(sync: boolean): Promise<void>;
} }
export = BaseView; export = BaseView;
} }

@ -278,25 +278,53 @@ exports.cssClass = cssClass;
function scrollChildIntoView(valueOrFunc) { function scrollChildIntoView(valueOrFunc) {
return makeBinding(valueOrFunc, doScrollChildIntoView); return makeBinding(valueOrFunc, doScrollChildIntoView);
} }
function doScrollChildIntoView(elem, index) { // Key at which we will store the index to scroll for async scrolling.
const indexKey = Symbol();
function doScrollChildIntoView(elem, index, sync) {
if (index === null) { if (index === null) {
return Promise.resolve(); return Promise.resolve();
} }
const scrolly = ko.utils.domData.get(elem, "scrolly"); const scrolly = ko.utils.domData.get(elem, "scrolly");
if (scrolly) { if (scrolly) {
// Delay this in case it's triggered while other changes are processed (e.g. splices). if (sync) {
return new Promise((resolve, reject) => { scrolly.scrollRowIntoView(index);
setTimeout(() => { // Clear async index for scrolling.
try { elem[indexKey] = null;
if (!scrolly.isDisposed()) { return Promise.resolve();
scrolly.scrollRowIntoView(index); } else {
// Delay this in case it's triggered while other changes are processed (e.g. splices).
// Scrolling is asynchronous, so in case there is already
// active scroll queued, we will change the target index.
// For example:
// doScrollChildIntoView(el, 10, false) # sets the index to 10 and queues a Promise1
// doScrollChildIntoView(el, 20, false) # updates index to 20 and queues a Promise2
// ....
// Promise1 moves to 20, and clears the index.
// Promise2 checks the index is null and just returns.
elem[indexKey] = index;
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
// If scroll was cancelled (there was another call after, that finished
// and cleared the index) return.
if (elem[indexKey] === null) {
resolve();
return;
}
if (!scrolly.isDisposed()) {
scrolly.scrollRowIntoView(elem[indexKey]);
}
resolve();
} catch(err) {
reject(err);
} finally {
// Clear the index, any subsequent async scrolls will be cancelled (on the if test above).
elem[indexKey] = null;
} }
resolve(); }, 0);
} catch(err) { });
reject(err); }
}
}, 0);
});
} else { } else {
const child = elem.children[index]; const child = elem.children[index];
if (child) { if (child) {
@ -312,8 +340,8 @@ function doScrollChildIntoView(elem, index) {
child.scrollIntoView(false); // ..bottom if scrolling down. child.scrollIntoView(false); // ..bottom if scrolling down.
} }
} }
return Promise.resolve();
} }
return Promise.resolve();
} }
exports.scrollChildIntoView = scrollChildIntoView; exports.scrollChildIntoView = scrollChildIntoView;
exports.doScrollChildIntoView = doScrollChildIntoView; exports.doScrollChildIntoView = doScrollChildIntoView;

@ -314,7 +314,7 @@ class FinderImpl implements IFinder {
if (this._aborted) { return; } if (this._aborted) { return; }
// Make sure we are at good place. This is important when the cursor // Make sure we are at good place. This is important when the cursor
// was already in a matched record, but the record was scrolled away. // was already in a matched record, but the record was scrolled away.
await viewInstance.revealActiveRecord(); viewInstance.scrollToCursor(true).catch(reportError);
const cursor = viewInstance.viewPane.querySelector('.selected_cursor'); const cursor = viewInstance.viewPane.querySelector('.selected_cursor');
if (cursor) { if (cursor) {

@ -1891,6 +1891,18 @@ export async function onNewTab(action: () => Promise<void>) {
await driver.switchTo().window(tabs[tabs.length - 2]); await driver.switchTo().window(tabs[tabs.length - 2]);
} }
/**
* Scrolls active Grid or Card list view.
*/
export async function scrollActiveView(x: number, y: number) {
await driver.executeScript(function(x1: number, y1: number) {
const view = document.querySelector(".active_section .grid_view_data") ||
document.querySelector(".active_section .detailview_scroll_pane");
view!.scrollBy(x1, y1);
}, x, y);
await driver.sleep(10); // wait a bit for the scroll to happen (this is async operation in Grist).
}
} // end of namespace gristUtils } // end of namespace gristUtils
stackWrapOwnMethods(gristUtils); stackWrapOwnMethods(gristUtils);

Loading…
Cancel
Save