(core) Better UX in full-edit mode for the formula editor

Summary:
Improving UX for the formula editor
- Formula editor will go into full edit mode only on formula change (not on a mouse click)
- Adding column highlight and a tooltip when in full edit mode

Test Plan: nbrowser tests

Reviewers: cyprien

Reviewed By: cyprien

Differential Revision: https://phab.getgrist.com/D3194
This commit is contained in:
Jarosław Sadziński 2021-12-22 15:28:27 +01:00
parent e99cc3ae08
commit 0482c83771
5 changed files with 99 additions and 13 deletions

View File

@ -42,7 +42,6 @@
} }
.gridview_data_header { .gridview_data_header {
border-bottom: 1px solid lightgray;
position:relative; position:relative;
} }
@ -52,8 +51,9 @@
} }
.field.column_name { .field.column_name {
border-bottom: 1px solid lightgray;
line-height: var(--gridview-header-height); line-height: var(--gridview-header-height);
height: var(--gridview-header-height); /* Also should match height for overlay elements */ height: calc(var(--gridview-header-height) + 1px); /* Also should match height for overlay elements */
} }
/* also .field.column_name, style set in viewCommon */ /* also .field.column_name, style set in viewCommon */
@ -350,6 +350,24 @@
} }
} }
/* Column hover effect */
.gridview_row .field.hover-column, /* normal field in a row */
.gridview_row .field.frozen.hover-column, /* frozen field in a row */
.column_name.hover-column, /* column name */
.column_name.hover-column.selected /* selected column name */ {
/* for frozen fields can't use alpha channel */
background-color: var(--grist-color-selection-opaque);
}
/* For zebra stripes, make the selection little darker */
.record-zebra.record-even .field.hover-column {
background-color: var(--grist-color-selection-darker-opaque);
}
/* When column has a hover, remove menu button. */
.column_name.hover-column .menu_toggle {
visibility: hidden;
}
/* Etc */ /* Etc */
.g-column-main-menu { .g-column-main-menu {

View File

@ -2,6 +2,7 @@
var _ = require('underscore'); var _ = require('underscore');
var ko = require('knockout'); var ko = require('knockout');
const debounce = require('lodash/debounce');
var gutil = require('app/common/gutil'); var gutil = require('app/common/gutil');
var BinaryIndexedTree = require('app/common/BinaryIndexedTree'); var BinaryIndexedTree = require('app/common/BinaryIndexedTree');
@ -36,6 +37,7 @@ const {RowContextMenu} = require('../ui/RowContextMenu');
const {setPopupToCreateDom} = require('popweasel'); const {setPopupToCreateDom} = require('popweasel');
const {testId} = require('app/client/ui2018/cssVars'); const {testId} = require('app/client/ui2018/cssVars');
const {menuToggle} = require('app/client/ui/MenuToggle'); const {menuToggle} = require('app/client/ui/MenuToggle');
const {showTooltip} = require('app/client/ui/tooltips');
// A threshold for interpreting a motionless click as a click rather than a drag. // A threshold for interpreting a motionless click as a click rather than a drag.
@ -187,6 +189,20 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) {
return ko.pureComputed(() => field._index() < this.numFrozen()); return ko.pureComputed(() => field._index() < this.numFrozen());
}, this)); }, this));
// Holds column index that is hovered, works only in full-edit formula mode.
this.hoverColumn = this.autoDispose(ko.observable(-1));
// Debounced method to change current hover column, this is needed
// as mouse when moved from field to field will switch the hover-column
// observable from current index to -1 and then immediately back to current index.
// With debounced version, call to set -1 that is followed by call to set back to the field index
// will be discarded.
this.changeHover = debounce((index) => {
if (this.isDisposed()) { return; }
if (this.gristDoc.docModel.editingFormula()) {
this.hoverColumn(index);
}
}, 0);
//-------------------------------------------------- //--------------------------------------------------
// Create and attach the DOM for the view. // Create and attach the DOM for the view.
@ -852,7 +868,10 @@ GridView.prototype.buildDom = function() {
dom('div.gridview_left_border'), //these hide behind the actual headers to keep them from flashing dom('div.gridview_left_border'), //these hide behind the actual headers to keep them from flashing
// left shadow that will be visible on top of frozen columns // left shadow that will be visible on top of frozen columns
dom('div.scroll_shadow_frozen', kd.show(this.frozenShadow)), dom('div.scroll_shadow_frozen', kd.show(this.frozenShadow)),
// When cursor leaves the GridView, remove hover immediately (without debounce).
// This guards mouse leaving gridView from the top, as leaving from bottom or left, right, is
// guarded on the row level.
dom.on("mouseleave", () => !this.isDisposed() && this.hoverColumn(-1)),
// Drag indicators // Drag indicators
self.colLine = dom( self.colLine = dom(
'div.col_indicator_line', 'div.col_indicator_line',
@ -900,12 +919,30 @@ GridView.prototype.buildDom = function() {
write: val => editIndex(val ? field._index() : -1) write: val => editIndex(val ? field._index() : -1)
}).extend({ rateLimit: 0 }); }).extend({ rateLimit: 0 });
let filterTriggerCtl; let filterTriggerCtl;
const isTooltip = ko.pureComputed(() =>
self.gristDoc.docModel.editingFormula() &&
ko.unwrap(self.hoverColumn) === field._index());
return dom( return dom(
'div.column_name.field', 'div.column_name.field',
kd.style('--frozen-position', () => ko.unwrap(this.frozenPositions.at(field._index()))), kd.style('--frozen-position', () => ko.unwrap(this.frozenPositions.at(field._index()))),
kd.toggleClass("frozen", () => ko.unwrap(this.frozenMap.at(field._index()))), kd.toggleClass("frozen", () => ko.unwrap(this.frozenMap.at(field._index()))),
kd.toggleClass("hover-column", isTooltip),
dom.autoDispose(isEditingLabel), dom.autoDispose(isEditingLabel),
dom.autoDispose(isTooltip),
dom.testId("GridView_columnLabel"), dom.testId("GridView_columnLabel"),
(el) => {
const tooltip = new HoverColumnTooltip(el);
return [
dom.autoDispose(tooltip),
dom.autoDispose(isTooltip.subscribe((show) => {
if (show) {
tooltip.show(`Click to insert $${field.colId.peek()}`);
} else {
tooltip.hide();
}
})),
]
},
kd.style('width', field.widthPx), kd.style('width', field.widthPx),
kd.style('borderRightWidth', v.borderWidthPx), kd.style('borderRightWidth', v.borderWidthPx),
viewCommon.makeResizable(field.width, {shouldSave: !this.gristDoc.isReadonly.get()}), viewCommon.makeResizable(field.width, {shouldSave: !this.gristDoc.isReadonly.get()}),
@ -920,6 +957,8 @@ GridView.prototype.buildDom = function() {
kf.editableLabel(self.isPreview ? field.label : field.displayLabel, isEditingLabel, renameCommands), kf.editableLabel(self.isPreview ? field.label : field.displayLabel, isEditingLabel, renameCommands),
dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true) dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true)
), ),
dom.on("mouseenter", () => self.changeHover(field._index())),
dom.on("mouseleave", () => self.changeHover(-1)),
self.isPreview ? null : menuToggle(null, self.isPreview ? null : menuToggle(null,
kd.cssClass('g-column-main-menu'), kd.cssClass('g-column-main-menu'),
kd.cssClass('g-column-menu-btn'), kd.cssClass('g-column-menu-btn'),
@ -1043,7 +1082,12 @@ GridView.prototype.buildDom = function() {
kd.toggleClass('record-zebra', vZebraStripes), kd.toggleClass('record-zebra', vZebraStripes),
// even by 1-indexed rownum, so +1 (makes more sense for user-facing display stuff) // even by 1-indexed rownum, so +1 (makes more sense for user-facing display stuff)
kd.toggleClass('record-even', () => (row._index()+1) % 2 === 0 ), kd.toggleClass('record-even', () => (row._index()+1) % 2 === 0 ),
dom.on("mouseleave", (ev) => {
// Leave only when leaving record row.
if (!ev.relatedTarget || !ev.relatedTarget.classList.contains("record")){
self.changeHover(-1);
}
}),
self.comparison ? kd.cssClass(() => { self.comparison ? kd.cssClass(() => {
const rowType = self.extraRows.getRowType(row.id()); const rowType = self.extraRows.getRowType(row.id());
return rowType && `diff-${rowType}` || ''; return rowType && `diff-${rowType}` || '';
@ -1078,6 +1122,10 @@ GridView.prototype.buildDom = function() {
dom.autoDispose(isCellSelected), dom.autoDispose(isCellSelected),
dom.autoDispose(isCellActive), dom.autoDispose(isCellActive),
dom.autoDispose(isSelected), dom.autoDispose(isSelected),
dom.on("mouseenter", () => self.changeHover(field._index())),
kd.toggleClass("hover-column", () =>
self.gristDoc.docModel.editingFormula() &&
ko.unwrap(self.hoverColumn) === (field._index())),
kd.style('width', field.widthPx), kd.style('width', field.widthPx),
//TODO: Ensure that fields in a row resize when //TODO: Ensure that fields in a row resize when
//a cell in that row becomes larger //a cell in that row becomes larger
@ -1428,10 +1476,30 @@ GridView.prototype.maybeSelectRow = function(elem, rowId) {
} }
}; };
// End Context Menus
GridView.prototype.revealActiveRecord = function() { GridView.prototype.revealActiveRecord = function() {
return kd.doScrollChildIntoView(this.scrollPane, this.cursor.rowIndex()); return kd.doScrollChildIntoView(this.scrollPane, this.cursor.rowIndex());
} }
// End Context Menus // Helper to show tooltip over column selection in the full edit mode.
class HoverColumnTooltip {
constructor(el) {
this.el = el;
}
show(text) {
this.hide();
this.tooltip = showTooltip(this.el, () => dom("span", text, testId("column-formula-tooltip")))
}
hide() {
if (this.tooltip ) {
this.tooltip.close();
this.tooltip = null;
}
}
dispose() {
this.hide();
}
}
module.exports = GridView; module.exports = GridView;

View File

@ -47,6 +47,7 @@ export const colors = {
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'), selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'), selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'),
selectionDarkerOpaque: new CustomProp('color-selection-darker-opaque', '#d6eee5'),
inactiveCursor: new CustomProp('color-inactive-cursor', '#A2E1C9'), inactiveCursor: new CustomProp('color-inactive-cursor', '#A2E1C9'),

View File

@ -429,8 +429,13 @@ export function openFormulaEditor(options: {
}); });
editor.attach(refElem); editor.attach(refElem);
// Enter formula-editing mode (highlight formula icons; click on a column inserts its ID). // When formula is empty enter formula-editing mode (highlight formula icons; click on a column inserts its ID).
field.editingFormula(true); // This function is used for primarily for switching between diffrent column behaviors, so we want to enter full
// edit mode right away.
// TODO: consider converting it to parameter, when this will be used in diffrent scenarios.
if (!column.formula()) {
field.editingFormula(true);
}
setupCleanup(holder, gristDoc, field, saveEdit); setupCleanup(holder, gristDoc, field, saveEdit);
return holder; return holder;
} }

View File

@ -97,12 +97,6 @@ export class FormulaEditor extends NewBaseEditor {
this._formulaEditor.getEditor().focus(); this._formulaEditor.getEditor().focus();
}), }),
dom('div.formula_editor.formula_field_edit', testId('formula-editor'), dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
// We don't always enter editing mode immediately, e.g. not on double-clicking a cell.
// In those cases, we'll switch as soon as the user types or clicks into the editor.
dom.on('mousedown', () => {
// but don't do it when this is a readonly mode
options.field.editingFormula(true);
}),
this._formulaEditor.buildDom((aceObj: any) => { this._formulaEditor.buildDom((aceObj: any) => {
aceObj.setFontSize(11); aceObj.setFontSize(11);
aceObj.setHighlightActiveLine(false); aceObj.setHighlightActiveLine(false);