spell check context menu

This commit is contained in:
zadam 2020-02-29 11:28:30 +01:00
parent 16f42dd4ab
commit 1239293435
3 changed files with 147 additions and 65 deletions

View File

@ -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 "<strong>${params.misspelledWord}</strong>"`,
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 <kbd>${platformModifier}+X`,
uiIcon: "cut",
handler: () => webContents.cut()
});
}
if (params.isEditable || hasText) {
items.push({
enabled: editFlags.canCopy && hasText,
title: `Copy <kbd>${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 <kbd>${platformModifier}+V`,
uiIcon: "paste",
handler: () => webContents.paste()
});
}
if (params.isEditable) {
items.push({
enabled: editFlags.canPaste,
title: `Paste as plain text <kbd>${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);
}
}
});

View File

@ -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($("<div>").addClass("dropdown-divider"));
@ -25,14 +63,20 @@ async function initContextMenu(options) {
const $item = $("<li>")
.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 = $("<ul>").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
}
const contextMenu = new ContextMenu();
export default contextMenu;

View File

@ -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);