From afa90cc3655bc5e30fc802febf771aa8be7e14f0 Mon Sep 17 00:00:00 2001 From: Cyprien P Date: Wed, 16 Feb 2022 15:15:27 +0100 Subject: [PATCH] (core) Show default context menu on link Summary: also: - closes opened menu if any when click on a custom widget - closes opened menu if any when F2 Test Plan: Include test case Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3269 --- app/client/components/BaseView.js | 3 ++- app/client/components/CustomView.ts | 3 +++ app/client/components/GridView.js | 4 ++-- app/client/ui/contextMenu.ts | 4 +++- app/client/ui2018/links.ts | 3 +++ app/client/ui2018/menus.ts | 19 ++++++++++++++++++- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index ce54761d..558dcff2 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -22,6 +22,7 @@ const {copyToClipboard} = require('app/client/lib/copyToClipboard'); const {setTestState} = require('app/client/lib/testState'); const {ExtraRows} = require('app/client/models/DataTableModelWithDiff'); const {createFilterMenu} = require('app/client/ui/ColumnFilterMenu'); +const {closeRegisteredMenu} = require('app/client/ui2018/menus'); /** * BaseView forms the basis for ViewSection classes. @@ -219,7 +220,7 @@ BaseView.commonCommands = { this.scrollToCursor(true).catch(reportError); this.activateEditorAtCursor({init}); }, - editField: function() { this.scrollToCursor(true); this.activateEditorAtCursor(); }, + editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); }, insertRecordBefore: function() { this.insertRow(this.cursor.rowIndex()); }, insertRecordAfter: function() { this.insertRow(this.cursor.rowIndex() + 1); }, diff --git a/app/client/components/CustomView.ts b/app/client/components/CustomView.ts index 2de3b150..bf3e0a36 100644 --- a/app/client/components/CustomView.ts +++ b/app/client/components/CustomView.ts @@ -20,6 +20,7 @@ import {dom as grains} from 'grainjs'; import * as ko from 'knockout'; import defaults = require('lodash/defaults'); import {AccessLevel} from 'app/common/CustomWidget'; +import {closeRegisteredMenu} from 'app/client/ui2018/menus'; /** * CustomView components displays arbitrary html. There are two modes available, in the "url" mode @@ -218,6 +219,8 @@ export class CustomView extends Disposable { if (!this.viewSection.isDisposed() && !this.viewSection.hasFocus()) { this.viewSection.hasFocus(true); } + // allow menus to close if any + closeRegisteredMenu(); }) }); diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 11c6babc..8fe353cb 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -29,7 +29,7 @@ const {onDblClickMatchElem} = require('app/client/lib/dblclick'); // Grist UI Components const {dom: grainjsDom, Holder, Computed} = require('grainjs'); -const {menu} = require('../ui2018/menus'); +const {closeRegisteredMenu, menu} = require('../ui2018/menus'); const {calcFieldsCondition} = require('../ui/GridViewMenus'); const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, freezeAction} = require('../ui/GridViewMenus'); const {RowContextMenu} = require('../ui/RowContextMenu'); @@ -273,7 +273,7 @@ GridView.gridCommands = { fieldEditSave: function() { this.cursor.rowIndex(this.cursor.rowIndex() + 1); }, // Re-define editField after fieldEditSave to make it take precedence for the Enter key. - editField: function() { this.scrollToCursor(true); this.activateEditorAtCursor(); }, + editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); }, deleteRecords: function() { const saved = this.cursor.getCursorPos(); diff --git a/app/client/ui/contextMenu.ts b/app/client/ui/contextMenu.ts index cf27403b..e629cfcf 100644 --- a/app/client/ui/contextMenu.ts +++ b/app/client/ui/contextMenu.ts @@ -8,7 +8,7 @@ * `dom.on('contextmenu', ev => ev.preventDefault())` */ import { Disposable, dom, DomArg, DomContents, Holder } from "grainjs"; -import { cssMenuElem } from 'app/client/ui2018/menus'; +import { cssMenuElem, registerMenuOpen } from 'app/client/ui2018/menus'; import { IOpenController, Menu } from 'popweasel'; export type IContextMenuContentFunc = (ctx: ContextMenuController) => DomContents; @@ -50,6 +50,8 @@ class ContextMenuController extends Disposable implements IOpenController { dom.domDispose(content); content.remove(); }); + + registerMenuOpen(this); } public close() { diff --git a/app/client/ui2018/links.ts b/app/client/ui2018/links.ts index e0b8f57c..0045db8f 100644 --- a/app/client/ui2018/links.ts +++ b/app/client/ui2018/links.ts @@ -24,6 +24,9 @@ export function gristLink(href: string|Observable, ...args: IDomArgs onClickHyperLink(ev, typeof href === 'string' ? href : href.get())), + // stop propagation to prevent the grist custom context menu to show up and let the default one + // to show up instead. + dom.on("contextmenu", ev => ev.stopPropagation()), // As per Google and Mozilla recommendations to prevent opened links // from running on the same process as Grist: // https://developers.google.com/web/tools/lighthouse/audits/noopener diff --git a/app/client/ui2018/menus.ts b/app/client/ui2018/menus.ts index c5586f90..5e262e29 100644 --- a/app/client/ui2018/menus.ts +++ b/app/client/ui2018/menus.ts @@ -17,11 +17,28 @@ export interface IOptionFull { icon?: IconName; } +let _lastOpenedController: weasel.IOpenController|null = null; + +// Close opened menu if any, otherwise do nothing. +export function closeRegisteredMenu() { + if (_lastOpenedController) { _lastOpenedController.close(); } +} + +// Register `ctl` to make sure it is closed when `closeMenu()` is called. +export function registerMenuOpen(ctl: weasel.IOpenController) { + _lastOpenedController = ctl; + ctl.onDispose(() => _lastOpenedController = null); +} + // For string options, we can use a string for label and value without wrapping into an object. export type IOption = (T & string) | IOptionFull; export function menu(createFunc: weasel.MenuCreateFunc, options?: weasel.IMenuOptions): DomElementMethod { - return weasel.menu(createFunc, {...defaults, ...options}); + const wrappedCreateFunc = (ctl: weasel.IOpenController) => { + registerMenuOpen(ctl); + return createFunc(ctl); + }; + return weasel.menu(wrappedCreateFunc, {...defaults, ...options}); } // TODO Weasel doesn't allow other options for submenus, but probably should.