diff --git a/app/client/components/GridView.css b/app/client/components/GridView.css index 90a4a4aa..56d6ba11 100644 --- a/app/client/components/GridView.css +++ b/app/client/components/GridView.css @@ -42,7 +42,6 @@ } .gridview_data_header { - border-bottom: 1px solid lightgray; position:relative; } @@ -52,8 +51,9 @@ } .field.column_name { + border-bottom: 1px solid lightgray; 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 */ @@ -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 */ .g-column-main-menu { diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 52b93fb2..50bc84ad 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -2,6 +2,7 @@ var _ = require('underscore'); var ko = require('knockout'); +const debounce = require('lodash/debounce'); var gutil = require('app/common/gutil'); var BinaryIndexedTree = require('app/common/BinaryIndexedTree'); @@ -36,6 +37,7 @@ const {RowContextMenu} = require('../ui/RowContextMenu'); const {setPopupToCreateDom} = require('popweasel'); const {testId} = require('app/client/ui2018/cssVars'); 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. @@ -187,6 +189,20 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) { return ko.pureComputed(() => field._index() < this.numFrozen()); }, 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. @@ -852,7 +868,10 @@ GridView.prototype.buildDom = function() { 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 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 self.colLine = dom( 'div.col_indicator_line', @@ -900,12 +919,30 @@ GridView.prototype.buildDom = function() { write: val => editIndex(val ? field._index() : -1) }).extend({ rateLimit: 0 }); let filterTriggerCtl; + const isTooltip = ko.pureComputed(() => + self.gristDoc.docModel.editingFormula() && + ko.unwrap(self.hoverColumn) === field._index()); return dom( 'div.column_name.field', kd.style('--frozen-position', () => ko.unwrap(this.frozenPositions.at(field._index()))), kd.toggleClass("frozen", () => ko.unwrap(this.frozenMap.at(field._index()))), + kd.toggleClass("hover-column", isTooltip), dom.autoDispose(isEditingLabel), + dom.autoDispose(isTooltip), 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('borderRightWidth', v.borderWidthPx), 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), 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, kd.cssClass('g-column-main-menu'), kd.cssClass('g-column-menu-btn'), @@ -1043,7 +1082,12 @@ GridView.prototype.buildDom = function() { kd.toggleClass('record-zebra', vZebraStripes), // even by 1-indexed rownum, so +1 (makes more sense for user-facing display stuff) 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(() => { const rowType = self.extraRows.getRowType(row.id()); return rowType && `diff-${rowType}` || ''; @@ -1078,6 +1122,10 @@ GridView.prototype.buildDom = function() { dom.autoDispose(isCellSelected), dom.autoDispose(isCellActive), 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), //TODO: Ensure that fields in a row resize when //a cell in that row becomes larger @@ -1428,10 +1476,30 @@ GridView.prototype.maybeSelectRow = function(elem, rowId) { } }; +// End Context Menus + GridView.prototype.revealActiveRecord = function() { 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; diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index 96a7fe92..a8f7583c 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -47,6 +47,7 @@ export const colors = { cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'), selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'), + selectionDarkerOpaque: new CustomProp('color-selection-darker-opaque', '#d6eee5'), inactiveCursor: new CustomProp('color-inactive-cursor', '#A2E1C9'), diff --git a/app/client/widgets/FieldEditor.ts b/app/client/widgets/FieldEditor.ts index 5f22b81f..a2b78bf8 100644 --- a/app/client/widgets/FieldEditor.ts +++ b/app/client/widgets/FieldEditor.ts @@ -429,8 +429,13 @@ export function openFormulaEditor(options: { }); editor.attach(refElem); - // Enter formula-editing mode (highlight formula icons; click on a column inserts its ID). - field.editingFormula(true); + // When formula is empty enter formula-editing mode (highlight formula icons; click on a column inserts its ID). + // 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); return holder; } diff --git a/app/client/widgets/FormulaEditor.ts b/app/client/widgets/FormulaEditor.ts index 119b567f..d962d6f3 100644 --- a/app/client/widgets/FormulaEditor.ts +++ b/app/client/widgets/FormulaEditor.ts @@ -97,12 +97,6 @@ export class FormulaEditor extends NewBaseEditor { this._formulaEditor.getEditor().focus(); }), 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) => { aceObj.setFontSize(11); aceObj.setHighlightActiveLine(false);