mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
01cef034ed
commit
e180641c7d
@ -6,7 +6,8 @@
|
|||||||
/* Make visible if open or in column header hover */
|
/* Make visible if open or in column header hover */
|
||||||
.g-column-menu-btn.open,
|
.g-column-menu-btn.open,
|
||||||
.g-column-menu-btn.active,
|
.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;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,35 +15,6 @@
|
|||||||
visibility: hidden;
|
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 {
|
.g-column-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
|
@ -52,15 +52,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail_row_num {
|
.detail_row_num {
|
||||||
text-align: right;
|
|
||||||
font-size: var(--grist-x-small-font-size);
|
font-size: var(--grist-x-small-font-size);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--grist-color-slate);
|
color: var(--grist-color-slate);
|
||||||
padding: 8px;
|
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 {
|
.detail_row_num::before {
|
||||||
content: "ROW ";
|
content: "ROW ";
|
||||||
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-left.disabled, .detail-right.disabled, .detail-add-btn.disabled {
|
.detail-left.disabled, .detail-right.disabled, .detail-add-btn.disabled {
|
||||||
@ -200,6 +217,10 @@
|
|||||||
margin-right: -1px; /* allow labels to overflow into the padding */
|
margin-right: -1px; /* allow labels to overflow into the padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail_theme_record_compact .menu_toggle {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
/*** form theme ***/
|
/*** form theme ***/
|
||||||
|
|
||||||
.detail_theme_field_form {
|
.detail_theme_field_form {
|
||||||
|
@ -13,6 +13,7 @@ var BaseView = require('./BaseView');
|
|||||||
var CopySelection = require('./CopySelection');
|
var CopySelection = require('./CopySelection');
|
||||||
var RecordLayout = require('./RecordLayout');
|
var RecordLayout = require('./RecordLayout');
|
||||||
var commands = require('./commands');
|
var commands = require('./commands');
|
||||||
|
const {RowContextMenu} = require('../ui/RowContextMenu');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DetailView component implements a list of record layouts.
|
* DetailView component implements a list of record layouts.
|
||||||
@ -28,6 +29,7 @@ function DetailView(gristDoc, viewSectionModel) {
|
|||||||
this.recordLayout = this.autoDispose(RecordLayout.create({
|
this.recordLayout = this.autoDispose(RecordLayout.create({
|
||||||
viewSection: this.viewSection,
|
viewSection: this.viewSection,
|
||||||
buildFieldDom: this.buildFieldDom.bind(this),
|
buildFieldDom: this.buildFieldDom.bind(this),
|
||||||
|
buildContextMenu : this.buildContextMenu.bind(this),
|
||||||
resizeCallback: () => {
|
resizeCallback: () => {
|
||||||
if (!this._isSingle) {
|
if (!this._isSingle) {
|
||||||
this.scrolly().updateSize();
|
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.
|
* 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,
|
* @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.
|
// Add .detailview_single when showing a single card or while editing layout.
|
||||||
kd.toggleClass('detailview_single',
|
kd.toggleClass('detailview_single',
|
||||||
() => this._isSingle || this.recordLayout.isEditingLayout()),
|
() => 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, () => {
|
kd.maybe(this.recordLayout.isEditingLayout, () => {
|
||||||
const rowId = this.viewData.getRowId(this.recordLayout.editIndex.peek());
|
const rowId = this.viewData.getRowId(this.recordLayout.editIndex.peek());
|
||||||
const record = this.getRenderedRowModel(rowId);
|
const record = this.getRenderedRowModel(rowId);
|
||||||
|
@ -83,6 +83,21 @@
|
|||||||
cursor: pointer;
|
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 {
|
@media print {
|
||||||
/* For printing, !important tag is needed for background colors to be respected; but normally,
|
/* For printing, !important tag is needed for background colors to be respected; but normally,
|
||||||
* do not want !important, as it interferes with row selection.
|
* do not want !important, as it interferes with row selection.
|
||||||
@ -339,8 +354,8 @@
|
|||||||
|
|
||||||
.g-column-main-menu {
|
.g-column-main-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 3px;
|
||||||
right: 0;
|
right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,9 +29,13 @@ const {onDblClickMatchElem} = require('app/client/lib/dblclick');
|
|||||||
const {Holder} = require('grainjs');
|
const {Holder} = require('grainjs');
|
||||||
const {menu} = require('../ui2018/menus');
|
const {menu} = require('../ui2018/menus');
|
||||||
const {calcFieldsCondition} = require('../ui/GridViewMenus');
|
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 {setPopupToCreateDom} = require('popweasel');
|
||||||
const {testId} = require('app/client/ui2018/cssVars');
|
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.
|
// 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
|
// 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),
|
kf.editableLabel(field.displayLabel, isEditingLabel, renameCommands),
|
||||||
dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true)
|
dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true)
|
||||||
),
|
),
|
||||||
this.isPreview ? null : dom('div.g-column-main-menu.g-column-menu-btn.right-btn',
|
this.isPreview ? null : menuToggle(null,
|
||||||
dom('span.glyphicon.glyphicon-triangle-bottom'),
|
kd.cssClass('g-column-main-menu'),
|
||||||
|
kd.cssClass('g-column-menu-btn'),
|
||||||
// Prevent mousedown on the dropdown triangle from initiating column drag.
|
// Prevent mousedown on the dropdown triangle from initiating column drag.
|
||||||
dom.on('mousedown', () => false),
|
dom.on('mousedown', () => false),
|
||||||
// Select the column if it's not part of a multiselect.
|
// Select the column if it's not part of a multiselect.
|
||||||
@ -993,17 +998,26 @@ GridView.prototype.buildDom = function() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
kd.toggleClass('selected', () =>
|
dom.on('contextmenu', ev => {
|
||||||
!row._isAddRow() && self.cellSelector.isRowSelected(row._index())),
|
// This is a little hack to position the menu the same way as with a click,
|
||||||
dom.on('contextmenu', ev => self.maybeSelectRow(ev.currentTarget, row.getRowId())),
|
// the same hack as on a column menu.
|
||||||
menu(ctl => RowContextMenu({
|
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()),
|
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()),
|
disableDelete: Boolean(self.gristDoc.isReadonly.get() || self.viewSection.disableAddRemoveRows() || self.getSelection().onlyAddRowSelected()),
|
||||||
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
|
isViewSorted: self.viewSection.activeSortSpec.peek().length > 0,
|
||||||
}), { trigger: ['contextmenu'] }),
|
}), { 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('div.record',
|
dom('div.record',
|
||||||
kd.toggleClass('record-add', row._isAddRow),
|
kd.toggleClass('record-add', row._isAddRow),
|
||||||
kd.style('borderLeftWidth', v.borderWidthPx),
|
kd.style('borderLeftWidth', v.borderWidthPx),
|
||||||
@ -1379,6 +1393,8 @@ GridView.prototype._columnFilterMenu = function(ctl, field) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
GridView.prototype.maybeSelectColumn = function (elem, 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;
|
const selectedColIds = this.getSelection().colIds;
|
||||||
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
|
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
|
||||||
return; // No need to select the column because it's included in the multi-selection
|
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) {
|
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 the clicked row was not already in the selection, move the selection to the row.
|
||||||
if (!this.getSelection().rowIds.includes(rowId)) {
|
if (!this.getSelection().rowIds.includes(rowId)) {
|
||||||
this.assignCursor(elem, selector.ROW);
|
this.assignCursor(elem, selector.ROW);
|
||||||
|
@ -32,9 +32,12 @@ var dispose = require('../lib/dispose');
|
|||||||
var dom = require('../lib/dom');
|
var dom = require('../lib/dom');
|
||||||
var {Delay} = require('../lib/Delay');
|
var {Delay} = require('../lib/Delay');
|
||||||
var kd = require('../lib/koDom');
|
var kd = require('../lib/koDom');
|
||||||
|
|
||||||
var Layout = require('./Layout');
|
var Layout = require('./Layout');
|
||||||
var RecordLayoutEditor = require('./RecordLayoutEditor');
|
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.
|
* Construct a RecordLayout.
|
||||||
@ -47,6 +50,7 @@ var RecordLayoutEditor = require('./RecordLayoutEditor');
|
|||||||
function RecordLayout(options) {
|
function RecordLayout(options) {
|
||||||
this.viewSection = options.viewSection;
|
this.viewSection = options.viewSection;
|
||||||
this.buildFieldDom = options.buildFieldDom;
|
this.buildFieldDom = options.buildFieldDom;
|
||||||
|
this.buildContextMenu = options.buildContextMenu;
|
||||||
this.isEditingLayout = ko.observable(false);
|
this.isEditingLayout = ko.observable(false);
|
||||||
this.editIndex = ko.observable(0);
|
this.editIndex = ko.observable(0);
|
||||||
this.layoutEditor = ko.observable(null); // RecordLayoutEditor when one is active.
|
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.peek().dispose();
|
||||||
this.layoutEditor(null);
|
this.layoutEditor(null);
|
||||||
}) : 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)
|
dom('div.g_record_detail_inner', layout.rootElem)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
flex: 1 1 0px;
|
flex: 1 1 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewsection_buttons {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.viewsection_title {
|
.viewsection_title {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
@ -31,42 +31,6 @@ export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
|||||||
}, `Show column ${col.label()}`))
|
}, `Show column ${col.label()}`))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRowContextMenu {
|
|
||||||
disableInsert: boolean;
|
|
||||||
disableDelete: boolean;
|
|
||||||
isViewSorted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RowContextMenu({ disableInsert, disableDelete, isViewSorted }: IRowContextMenu) {
|
|
||||||
const result: Element[] = [];
|
|
||||||
if (isViewSorted) {
|
|
||||||
// When the view is sorted, any newly added records get shifts instantly at the top or
|
|
||||||
// bottom. It could be very confusing for users who might expect the record to stay above or
|
|
||||||
// below the active row. Thus in this case we show a single `insert row` command.
|
|
||||||
result.push(
|
|
||||||
menuItemCmd(allCommands.insertRecordAfter, 'Insert row',
|
|
||||||
dom.cls('disabled', disableInsert)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
result.push(
|
|
||||||
menuItemCmd(allCommands.insertRecordBefore, 'Insert row above',
|
|
||||||
dom.cls('disabled', disableInsert)),
|
|
||||||
menuItemCmd(allCommands.insertRecordAfter, 'Insert row below',
|
|
||||||
dom.cls('disabled', disableInsert)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result.push(
|
|
||||||
menuDivider(),
|
|
||||||
menuItemCmd(allCommands.deleteRecords, 'Delete',
|
|
||||||
dom.cls('disabled', disableDelete)),
|
|
||||||
);
|
|
||||||
result.push(
|
|
||||||
menuDivider(),
|
|
||||||
menuItemCmd(allCommands.copyLink, 'Copy anchor link'));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMultiColumnContextMenu {
|
interface IMultiColumnContextMenu {
|
||||||
// For multiple selection, true/false means the value applies to all columns, 'mixed' means it's
|
// For multiple selection, true/false means the value applies to all columns, 'mixed' means it's
|
||||||
// true for some columns, but not all.
|
// true for some columns, but not all.
|
||||||
|
34
app/client/ui/MenuToggle.ts
Normal file
34
app/client/ui/MenuToggle.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { dom, DomArg, IDisposableOwner, styled } from "grainjs";
|
||||||
|
import { icon } from "app/client/ui2018/icons";
|
||||||
|
import { colors } from "app/client/ui2018/cssVars";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a toggle button - little square button with a dropdown icon inside, used
|
||||||
|
* by a context menu for a row inside a grid, a card inside a cardlist and column name.
|
||||||
|
*/
|
||||||
|
export function menuToggle(obs: IDisposableOwner, ...args: DomArg[]) {
|
||||||
|
const contextMenu = cssMenuToggle(
|
||||||
|
icon('Dropdown', dom.cls('menu_toggle_icon')),
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
return contextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssMenuToggle = styled('div.menu_toggle', `
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
--icon-color: ${colors.slate};
|
||||||
|
border: 1px solid ${colors.slate};
|
||||||
|
border-radius: 4px;
|
||||||
|
&:hover {
|
||||||
|
--icon-color: ${colors.darkGreen};
|
||||||
|
border-color: ${colors.darkGreen};
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
--icon-color: ${colors.darkerGreen};
|
||||||
|
border-color: ${colors.darkerGreen};
|
||||||
|
}
|
||||||
|
& > .menu_toggle_icon {
|
||||||
|
display: block; /* don't create a line */
|
||||||
|
}
|
||||||
|
`);
|
38
app/client/ui/RowContextMenu.ts
Normal file
38
app/client/ui/RowContextMenu.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { allCommands } from 'app/client/components/commands';
|
||||||
|
import { menuDivider, menuItemCmd } from 'app/client/ui2018/menus';
|
||||||
|
import { dom } from 'grainjs';
|
||||||
|
|
||||||
|
interface IRowContextMenu {
|
||||||
|
disableInsert: boolean;
|
||||||
|
disableDelete: boolean;
|
||||||
|
isViewSorted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RowContextMenu({ disableInsert, disableDelete, isViewSorted }: IRowContextMenu) {
|
||||||
|
const result: Element[] = [];
|
||||||
|
if (isViewSorted) {
|
||||||
|
// When the view is sorted, any newly added records get shifts instantly at the top or
|
||||||
|
// bottom. It could be very confusing for users who might expect the record to stay above or
|
||||||
|
// below the active row. Thus in this case we show a single `insert row` command.
|
||||||
|
result.push(
|
||||||
|
menuItemCmd(allCommands.insertRecordAfter, 'Insert row',
|
||||||
|
dom.cls('disabled', disableInsert)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result.push(
|
||||||
|
menuItemCmd(allCommands.insertRecordBefore, 'Insert row above',
|
||||||
|
dom.cls('disabled', disableInsert)),
|
||||||
|
menuItemCmd(allCommands.insertRecordAfter, 'Insert row below',
|
||||||
|
dom.cls('disabled', disableInsert)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.push(
|
||||||
|
menuDivider(),
|
||||||
|
menuItemCmd(allCommands.deleteRecords, 'Delete',
|
||||||
|
dom.cls('disabled', disableDelete)),
|
||||||
|
);
|
||||||
|
result.push(
|
||||||
|
menuDivider(),
|
||||||
|
menuItemCmd(allCommands.copyLink, 'Copy anchor link'));
|
||||||
|
return result;
|
||||||
|
}
|
@ -8,8 +8,30 @@ import {dom} from 'grainjs';
|
|||||||
* Returns a list of menu items for a view section.
|
* Returns a list of menu items for a view section.
|
||||||
*/
|
*/
|
||||||
export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionRec, isReadonly: boolean) {
|
export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionRec, isReadonly: boolean) {
|
||||||
const gristDoc = viewSection.viewInstance.peek()!.gristDoc;
|
const viewInstance = viewSection.viewInstance.peek()!;
|
||||||
|
const gristDoc = viewInstance.gristDoc;
|
||||||
|
|
||||||
|
// get current row index from cursor
|
||||||
|
const cursorRow = viewInstance.cursor.rowIndex.peek();
|
||||||
|
// get row id from current data
|
||||||
|
// rowId can be string - it is wrongly typed in cursor and in viewData
|
||||||
|
const rowId = (cursorRow !== null ? viewInstance.viewData.getRowId(cursorRow) : null) as string|null|number;
|
||||||
|
const isAddRow = rowId === 'new';
|
||||||
|
|
||||||
|
const contextMenu = [
|
||||||
|
menuItemCmd(allCommands.deleteRecords,
|
||||||
|
'Delete record',
|
||||||
|
testId('section-delete-card'),
|
||||||
|
dom.cls('disabled', isReadonly || isAddRow)),
|
||||||
|
menuItemCmd(allCommands.copyLink,
|
||||||
|
'Copy anchor link',
|
||||||
|
testId('section-card-link'),
|
||||||
|
),
|
||||||
|
menuDivider(),
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
dom.maybe((use) => ['single'].includes(use(viewSection.parentKey)), () => contextMenu),
|
||||||
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
||||||
menuItemLink({ href: gristDoc.getCsvLink(), target: '_blank', download: ''},
|
menuItemLink({ href: gristDoc.getCsvLink(), target: '_blank', download: ''},
|
||||||
'Download as CSV', testId('download-section')),
|
'Download as CSV', testId('download-section')),
|
||||||
|
Loading…
Reference in New Issue
Block a user