From 12392934359d22808fccfb62f461a0a0e08f34ce Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 29 Feb 2020 11:28:30 +0100 Subject: [PATCH] spell check context menu --- src/public/javascripts/desktop.js | 99 +++++++++++++--- .../javascripts/services/context_menu.js | 109 ++++++++++-------- src/public/javascripts/widgets/note_tree.js | 4 +- 3 files changed, 147 insertions(+), 65 deletions(-) diff --git a/src/public/javascripts/desktop.js b/src/public/javascripts/desktop.js index 0f2df5ba1..dcd28744d 100644 --- a/src/public/javascripts/desktop.js +++ b/src/public/javascripts/desktop.js @@ -1,5 +1,4 @@ import glob from './services/glob.js'; -import contextMenu from './services/tree_context_menu.js'; import link from './services/link.js'; import ws from './services/ws.js'; import noteType from './widgets/note_type.js'; @@ -66,7 +65,7 @@ import RenderTypeWidget from "./widgets/type_widgets/render.js"; import RelationMapTypeWidget from "./widgets/type_widgets/relation_map.js"; import ProtectedSessionTypeWidget from "./widgets/type_widgets/protected_session.js"; import BookTypeWidget from "./widgets/type_widgets/book.js"; -import contextMenuService from "./services/context_menu.js"; +import contextMenu from "./services/context_menu.js"; if (utils.isElectron()) { require('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) { @@ -87,39 +86,107 @@ noteTooltipService.setupGlobalTooltip(); noteAutocompleteService.init(); if (utils.isElectron()) { - const {webContents} = require('electron').remote.getCurrentWindow(); + const electron = require('electron'); + const {webContents} = electron.remote.getCurrentWindow(); webContents.on('context-menu', (event, params) => { - const items = [ - {title: "Hello", cmd: "openNoteInNewTab", uiIcon: "arrow-up-right"} - ]; + const {editFlags} = params; + const hasText = params.selectionText.trim().length > 0; + const isMac = process.platform === "darwin"; + const platformModifier = isMac ? 'Meta' : 'Ctrl'; + + const items = []; if (params.misspelledWord) { - items.push({ - title: `Misspelled "${params.misspelledWord}"`, - cmd: "openNoteInNewTab", - uiIcon: "" - }); - for (const suggestion of params.dictionarySuggestions) { items.push({ title: suggestion, command: "replaceMisspelling", spellingSuggestion: suggestion, - uiIcon: "" + uiIcon: "empty" }); } + + items.push({ + title: `Add "${params.misspelledWord}" to dictionary`, + uiIcon: "plus", + handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) + }); + + items.push({ title: `----` }); } - contextMenuService.initContextMenu({ + if (params.isEditable) { + items.push({ + enabled: editFlags.canCut && hasText, + title: `Cut ${platformModifier}+X`, + uiIcon: "cut", + handler: () => webContents.cut() + }); + } + + if (params.isEditable || hasText) { + items.push({ + enabled: editFlags.canCopy && hasText, + title: `Copy ${platformModifier}+C`, + uiIcon: "copy", + handler: () => webContents.copy() + }); + } + + if (params.linkURL.length !== 0 && params.mediaType === 'none') { + items.push({ + title: `Copy link`, + uiIcon: "copy", + handler: () => { + electron.clipboard.write({ + bookmark: params.linkText, + text: params.linkURL + }); + } + }); + } + + if (params.isEditable) { + items.push({ + enabled: editFlags.canPaste, + title: `Paste ${platformModifier}+V`, + uiIcon: "paste", + handler: () => webContents.paste() + }); + } + + if (params.isEditable) { + items.push({ + enabled: editFlags.canPaste, + title: `Paste as plain text ${platformModifier}+Shift+V`, + uiIcon: "paste", + handler: () => webContents.pasteAndMatchStyle() + }); + } + + if (hasText) { + const shortenedSelection = params.selectionText.length > 15 + ? (params.selectionText.substr(0, 13) + "…") + : params.selectionText; + + items.push({ + enabled: editFlags.canPaste, + title: `Search for "${shortenedSelection}" with DuckDuckGo`, + uiIcon: "search-alt", + handler: () => electron.shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(params.selectionText)}`) + }); + } + + contextMenu.show({ x: params.x, y: params.y, items, - selectContextMenuItem: (e, {command, spellingSuggestion}) => { + selectMenuItemHandler: ({command, spellingSuggestion}) => { if (command === 'replaceMisspelling') { console.log("Replacing missspeling", spellingSuggestion); - require('electron').remote.getCurrentWindow().webContents.insertText(spellingSuggestion); + webContents.insertText(spellingSuggestion); } } }); diff --git a/src/public/javascripts/services/context_menu.js b/src/public/javascripts/services/context_menu.js index 5d879daa0..13b626c17 100644 --- a/src/public/javascripts/services/context_menu.js +++ b/src/public/javascripts/services/context_menu.js @@ -1,10 +1,48 @@ import keyboardActionService from './keyboard_actions.js'; -const $contextMenuContainer = $("#context-menu-container"); -let dateContextMenuOpenedMs = 0; +class ContextMenu { + constructor() { + this.$widget = $("#context-menu-container"); + this.dateContextMenuOpenedMs = 0; -async function initContextMenu(options) { - function addItems($parent, items) { + $(document).on('click', () => this.hide()); + } + + async show(options) { + this.options = options; + + this.$widget.empty(); + + this.addItems(this.$widget, options.items); + + keyboardActionService.updateDisplayedShortcuts(this.$widget); + + this.positionMenu(); + + this.dateContextMenuOpenedMs = Date.now(); + } + + positionMenu() { + // code below tries to detect when dropdown would overflow from page + // in such case we'll position it above click coordinates so it will fit into client + const clientHeight = document.documentElement.clientHeight; + const contextMenuHeight = this.$widget.outerHeight() + 30; + let top; + + if (this.options.y + contextMenuHeight > clientHeight) { + top = clientHeight - contextMenuHeight - 10; + } else { + top = this.options.y - 10; + } + + this.$widget.css({ + display: "block", + top: top, + left: this.options.x - 20 + }).addClass("show"); + } + + addItems($parent, items) { for (const item of items) { if (item.title === '----') { $parent.append($("
").addClass("dropdown-divider")); @@ -25,14 +63,20 @@ async function initContextMenu(options) { const $item = $("
  • ") .addClass("dropdown-item") .append($link) - .on('mousedown', function (e) { + // important to use mousedown instead of click since the former does not change focus + // (especially important for focused text for spell check) + .on('mousedown', (e) => { e.stopPropagation(); - hideContextMenu(); + this.hide(); e.originalTarget = event.target; - options.selectContextMenuItem(e, item); + if (item.handler) { + item.handler(item, e); + } + + this.options.selectMenuItemHandler(item, e); // it's important to stop the propagation especially for sub-menus, otherwise the event // might be handled again by top-level menu @@ -49,7 +93,7 @@ async function initContextMenu(options) { const $subMenu = $("
      ").addClass("dropdown-menu"); - addItems($subMenu, item.items); + this.addItems($subMenu, item.items); $item.append($subMenu); } @@ -59,45 +103,16 @@ async function initContextMenu(options) { } } - $contextMenuContainer.empty(); - - addItems($contextMenuContainer, options.items); - - keyboardActionService.updateDisplayedShortcuts($contextMenuContainer); - - // code below tries to detect when dropdown would overflow from page - // in such case we'll position it above click coordinates so it will fit into client - const clientHeight = document.documentElement.clientHeight; - const contextMenuHeight = $contextMenuContainer.outerHeight() + 30; - let top; - - if (options.y + contextMenuHeight > clientHeight) { - top = clientHeight - contextMenuHeight - 10; - } else { - top = options.y - 10; - } - - dateContextMenuOpenedMs = Date.now(); - - $contextMenuContainer.css({ - display: "block", - top: top, - left: options.x - 20 - }).addClass("show"); -} - -$(document).on('click', () => hideContextMenu()); - -function hideContextMenu() { - // this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468 - // "contextmenu" event also triggers "click" event which depending on the timing can close just opened context menu - // we might filter out right clicks, but then it's better if even right clicks close the context menu - if (Date.now() - dateContextMenuOpenedMs > 300) { - $contextMenuContainer.hide(); + hide() { + // this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468 + // "contextmenu" event also triggers "click" event which depending on the timing can close just opened context menu + // we might filter out right clicks, but then it's better if even right clicks close the context menu + if (Date.now() - this.dateContextMenuOpenedMs > 300) { + this.$widget.hide(); + } } } -export default { - initContextMenu, - hideContextMenu -} \ No newline at end of file +const contextMenu = new ContextMenu(); + +export default contextMenu; \ No newline at end of file diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js index ae3f06cb0..784217a74 100644 --- a/src/public/javascripts/widgets/note_tree.js +++ b/src/public/javascripts/widgets/note_tree.js @@ -1,7 +1,7 @@ import hoistedNoteService from "../services/hoisted_note.js"; import treeService from "../services/tree.js"; import utils from "../services/utils.js"; -import contextMenuWidget from "../services/context_menu.js"; +import contextMenu from "../services/context_menu.js"; import treeCache from "../services/tree_cache.js"; import treeBuilder from "../services/tree_builder.js"; import TreeContextMenu from "../services/tree_context_menu.js"; @@ -97,7 +97,7 @@ export default class NoteTreeWidget extends TabAwareWidget { }, activate: async (event, data) => { // click event won't propagate so let's close context menu manually - contextMenuWidget.hideContextMenu(); + contextMenu.hide(); const notePath = treeService.getNotePath(data.node);