(core) Context menu for cards.

Summary:
User was not able to delete cards. This patch introduces a context menu for cards, analogous to the one available for rows on a GridView.

Changes:
- Row numbers on a GridView have the same icon as on columns to make context menu more discoverable.
- Context menu for rows and columns, when activated, didn't switch section in rare conditions (i.e. when the section had 2 or more columns selected, one of which had the same rowId as a column in the section that the user switched from).
- Card list layout and a single card layout has the same context menu as in a GridView, available by pressing the context menu button.

Test Plan: Browser tests

Reviewers: dsagal, paulfitz

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2870
This commit is contained in:
Jarosław Sadziński
2021-06-28 20:02:45 +02:00
parent 01cef034ed
commit e180641c7d
11 changed files with 204 additions and 83 deletions

View File

@@ -6,7 +6,8 @@
/* Make visible if open or in column header hover */
.g-column-menu-btn.open,
.g-column-menu-btn.active,
.column_name:hover .g-column-menu-btn {
.column_name:hover .g-column-menu-btn,
.column_name .g-column-menu-btn.weasel-popup-open {
visibility: visible;
}
@@ -14,35 +15,6 @@
visibility: hidden;
}
.g-column-menu-btn > span.glyphicon {
padding: 1px;
margin-left: 2px;
margin-right: 2px;
background-color: #fff;
color: #999;
border: 1px solid #999;
border-radius: 3px;
font-size: 1rem;
}
.g-column-menu-btn.left-btn > span.glyphicon {
margin: 0 0 0 2px;
}
.g-column-menu-btn.right-btn > span.glyphicon {
margin: 0 2px 0 0;
}
.g-column-menu-btn:hover > span.glyphicon {
color: #333;
border: 1px solid #333;
}
.g-column-menu-btn.active > span.glyphicon {
color: #33f;
border-color: #33f;
}
.g-column-menu {
position: absolute;
min-width: 180px;

View File

@@ -52,15 +52,32 @@
}
.detail_row_num {
text-align: right;
font-size: var(--grist-x-small-font-size);
font-weight: normal;
color: var(--grist-color-slate);
padding: 8px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.detail_row_num .menu_toggle {
margin-left: 0.5rem;
}
.detail_row_num:hover .menu_toggle,
.detail_row_num .menu_toggle.weasel-popup-open {
color: var(--color-link-default);
}
/* hide menu on layout editor */
.detailview_layout_editor .menu_toggle {
visibility: hidden !important;
}
.detail_row_num::before {
content: "ROW ";
margin-right: 2px;
}
.detail-left.disabled, .detail-right.disabled, .detail-add-btn.disabled {
@@ -200,6 +217,10 @@
margin-right: -1px; /* allow labels to overflow into the padding */
}
.detail_theme_record_compact .menu_toggle {
transform: translateY(-1px);
}
/*** form theme ***/
.detail_theme_field_form {

View File

@@ -13,6 +13,7 @@ var BaseView = require('./BaseView');
var CopySelection = require('./CopySelection');
var RecordLayout = require('./RecordLayout');
var commands = require('./commands');
const {RowContextMenu} = require('../ui/RowContextMenu');
/**
* DetailView component implements a list of record layouts.
@@ -28,6 +29,7 @@ function DetailView(gristDoc, viewSectionModel) {
this.recordLayout = this.autoDispose(RecordLayout.create({
viewSection: this.viewSection,
buildFieldDom: this.buildFieldDom.bind(this),
buildContextMenu : this.buildContextMenu.bind(this),
resizeCallback: () => {
if (!this._isSingle) {
this.scrolly().updateSize();
@@ -205,6 +207,15 @@ DetailView.prototype.getSelection = function() {
);
};
DetailView.prototype.buildContextMenu = function(row, options) {
const defaults = {
disableInsert: Boolean(this.gristDoc.isReadonly.get() || this.viewSection.disableAddRemoveRows() || this.tableModel.tableMetaRow.onDemand()),
disableDelete: Boolean(this.gristDoc.isReadonly.get() || this.viewSection.disableAddRemoveRows() || row._isAddRow()),
isViewSorted: this.viewSection.activeSortSpec.peek().length > 0,
};
return RowContextMenu(options ? Object.assign(defaults, options) : defaults);
}
/**
* Builds the DOM for the given field of the given row.
* @param {MetaRowModel|String} field: Model for the field to render. For a new field being added,
@@ -262,6 +273,8 @@ DetailView.prototype.buildDom = function() {
// Add .detailview_single when showing a single card or while editing layout.
kd.toggleClass('detailview_single',
() => this._isSingle || this.recordLayout.isEditingLayout()),
// Add a marker class that editor is active - used for hiding context menu toggle.
kd.toggleClass('detailview_layout_editor', this.recordLayout.isEditingLayout),
kd.maybe(this.recordLayout.isEditingLayout, () => {
const rowId = this.viewData.getRowId(this.recordLayout.editIndex.peek());
const record = this.getRenderedRowModel(rowId);

View File

@@ -83,6 +83,21 @@
cursor: pointer;
}
/* Menu toggle on a row */
.gridview_data_row_num .menu_toggle {
visibility: hidden;
position: absolute;
top: 2px;
right: 0px;
}
/* Show on hover or when menu is opened */
.gridview_data_row_num:hover .menu_toggle,
.gridview_data_row_num .menu_toggle.weasel-popup-open {
visibility: visible;
}
@media print {
/* For printing, !important tag is needed for background colors to be respected; but normally,
* do not want !important, as it interferes with row selection.
@@ -339,8 +354,8 @@
.g-column-main-menu {
position: absolute;
top: 0;
right: 0;
top: 3px;
right: 2px;
}

View File

@@ -29,9 +29,13 @@ const {onDblClickMatchElem} = require('app/client/lib/dblclick');
const {Holder} = require('grainjs');
const {menu} = require('../ui2018/menus');
const {calcFieldsCondition} = require('../ui/GridViewMenus');
const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, RowContextMenu, freezeAction} = require('../ui/GridViewMenus');
const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, freezeAction} = require('../ui/GridViewMenus');
const {RowContextMenu} = require('../ui/RowContextMenu');
const {setPopupToCreateDom} = require('popweasel');
const {testId} = require('app/client/ui2018/cssVars');
const {menuToggle} = require('app/client/ui/MenuToggle');
// A threshold for interpreting a motionless click as a click rather than a drag.
// Anything longer than this time (in milliseconds) should be interpreted as a drag
@@ -901,8 +905,9 @@ GridView.prototype.buildDom = function() {
kf.editableLabel(field.displayLabel, isEditingLabel, renameCommands),
dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true)
),
this.isPreview ? null : dom('div.g-column-main-menu.g-column-menu-btn.right-btn',
dom('span.glyphicon.glyphicon-triangle-bottom'),
this.isPreview ? null : menuToggle(null,
kd.cssClass('g-column-main-menu'),
kd.cssClass('g-column-menu-btn'),
// Prevent mousedown on the dropdown triangle from initiating column drag.
dom.on('mousedown', () => false),
// Select the column if it's not part of a multiselect.
@@ -993,17 +998,26 @@ GridView.prototype.buildDom = function() {
);
}
}),
dom.on('contextmenu', ev => {
// This is a little hack to position the menu the same way as with a click,
// the same hack as on a column menu.
ev.preventDefault();
ev.currentTarget.querySelector('.menu_toggle').click();
}),
menuToggle(null,
dom.on('click', ev => self.maybeSelectRow(ev.currentTarget.parentNode, row.getRowId())),
menu(() => RowContextMenu({
disableInsert: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.tableModel.tableMetaRow.onDemand()),
disableDelete: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.getSelection().onlyAddRowSelected()),
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
}), { trigger: ['click'] }),
// Prevent mousedown on the dropdown triangle from initiating row drag.
dom.on('mousedown', () => false),
testId('row-menu-trigger'),
),
kd.toggleClass('selected', () =>
!row._isAddRow() && self.cellSelector.isRowSelected(row._index())),
dom.on('contextmenu', ev => self.maybeSelectRow(ev.currentTarget, row.getRowId())),
menu(ctl => RowContextMenu({
disableInsert: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.tableModel.tableMetaRow.onDemand()),
disableDelete: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.getSelection().onlyAddRowSelected()),
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
}), { trigger: ['contextmenu'] }),
),
dom('div.record',
kd.toggleClass('record-add', row._isAddRow),
kd.style('borderLeftWidth', v.borderWidthPx),
@@ -1379,6 +1393,8 @@ GridView.prototype._columnFilterMenu = function(ctl, field) {
};
GridView.prototype.maybeSelectColumn = function (elem, field) {
// Change focus before running command so that the correct viewsection's cursor is moved.
this.viewSection.hasFocus(true);
const selectedColIds = this.getSelection().colIds;
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
return; // No need to select the column because it's included in the multi-selection
@@ -1387,6 +1403,8 @@ GridView.prototype.maybeSelectColumn = function (elem, field) {
};
GridView.prototype.maybeSelectRow = function(elem, rowId) {
// Change focus before running command so that the correct viewsection's cursor is moved.
this.viewSection.hasFocus(true);
// If the clicked row was not already in the selection, move the selection to the row.
if (!this.getSelection().rowIds.includes(rowId)) {
this.assignCursor(elem, selector.ROW);

View File

@@ -32,9 +32,12 @@ var dispose = require('../lib/dispose');
var dom = require('../lib/dom');
var {Delay} = require('../lib/Delay');
var kd = require('../lib/koDom');
var Layout = require('./Layout');
var RecordLayoutEditor = require('./RecordLayoutEditor');
var commands = require('./commands');
var {menuToggle} = require('app/client/ui/MenuToggle');
var {menu} = require('../ui2018/menus');
var {testId} = require('app/client/ui2018/cssVars');
/**
* Construct a RecordLayout.
@@ -47,6 +50,7 @@ var RecordLayoutEditor = require('./RecordLayoutEditor');
function RecordLayout(options) {
this.viewSection = options.viewSection;
this.buildFieldDom = options.buildFieldDom;
this.buildContextMenu = options.buildContextMenu;
this.isEditingLayout = ko.observable(false);
this.editIndex = ko.observable(0);
this.layoutEditor = ko.observable(null); // RecordLayoutEditor when one is active.
@@ -328,7 +332,23 @@ RecordLayout.prototype.buildLayoutDom = function(row, optCreateEditor) {
this.layoutEditor.peek().dispose();
this.layoutEditor(null);
}) : null,
dom('div.detail_row_num', kd.text(() => (row._index() + 1))),
dom('div.detail_row_num',
kd.text(() => (row._index() + 1)),
dom.on('contextmenu', ev => {
// This is a little hack to position the menu the same way as with a click,
// the same hack as on a column menu.
ev.preventDefault();
ev.currentTarget.querySelector('.menu_toggle').click();
}),
menuToggle(null,
dom.on('click', () => {
this.viewSection.hasFocus(true);
commands.allCommands.setCursor.run(row);
}),
menu(() => this.buildContextMenu(row)),
testId('card-menu-trigger')
)
),
dom('div.g_record_detail_inner', layout.rootElem)
);
};

View File

@@ -3,6 +3,10 @@
flex: 1 1 0px;
}
.viewsection_buttons {
margin-left: 4px;
}
.viewsection_title {
flex-shrink: 0;
align-items: baseline;