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 glob from './services/glob.js';
import contextMenu from './services/tree_context_menu.js';
import link from './services/link.js'; import link from './services/link.js';
import ws from './services/ws.js'; import ws from './services/ws.js';
import noteType from './widgets/note_type.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 RelationMapTypeWidget from "./widgets/type_widgets/relation_map.js";
import ProtectedSessionTypeWidget from "./widgets/type_widgets/protected_session.js"; import ProtectedSessionTypeWidget from "./widgets/type_widgets/protected_session.js";
import BookTypeWidget from "./widgets/type_widgets/book.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()) { if (utils.isElectron()) {
require('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) { require('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) {
@ -87,39 +86,107 @@ noteTooltipService.setupGlobalTooltip();
noteAutocompleteService.init(); noteAutocompleteService.init();
if (utils.isElectron()) { if (utils.isElectron()) {
const {webContents} = require('electron').remote.getCurrentWindow(); const electron = require('electron');
const {webContents} = electron.remote.getCurrentWindow();
webContents.on('context-menu', (event, params) => { webContents.on('context-menu', (event, params) => {
const items = [ const {editFlags} = params;
{title: "Hello", cmd: "openNoteInNewTab", uiIcon: "arrow-up-right"} const hasText = params.selectionText.trim().length > 0;
]; const isMac = process.platform === "darwin";
const platformModifier = isMac ? 'Meta' : 'Ctrl';
const items = [];
if (params.misspelledWord) { if (params.misspelledWord) {
items.push({
title: `Misspelled "<strong>${params.misspelledWord}</strong>"`,
cmd: "openNoteInNewTab",
uiIcon: ""
});
for (const suggestion of params.dictionarySuggestions) { for (const suggestion of params.dictionarySuggestions) {
items.push({ items.push({
title: suggestion, title: suggestion,
command: "replaceMisspelling", command: "replaceMisspelling",
spellingSuggestion: suggestion, 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, x: params.x,
y: params.y, y: params.y,
items, items,
selectContextMenuItem: (e, {command, spellingSuggestion}) => { selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling') { if (command === 'replaceMisspelling') {
console.log("Replacing missspeling", spellingSuggestion); 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'; 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) { $(document).on('click', () => this.hide());
function addItems($parent, items) { }
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) { for (const item of items) {
if (item.title === '----') { if (item.title === '----') {
$parent.append($("<div>").addClass("dropdown-divider")); $parent.append($("<div>").addClass("dropdown-divider"));
@ -25,14 +63,20 @@ async function initContextMenu(options) {
const $item = $("<li>") const $item = $("<li>")
.addClass("dropdown-item") .addClass("dropdown-item")
.append($link) .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(); e.stopPropagation();
hideContextMenu(); this.hide();
e.originalTarget = event.target; 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 // it's important to stop the propagation especially for sub-menus, otherwise the event
// might be handled again by top-level menu // might be handled again by top-level menu
@ -49,7 +93,7 @@ async function initContextMenu(options) {
const $subMenu = $("<ul>").addClass("dropdown-menu"); const $subMenu = $("<ul>").addClass("dropdown-menu");
addItems($subMenu, item.items); this.addItems($subMenu, item.items);
$item.append($subMenu); $item.append($subMenu);
} }
@ -59,45 +103,16 @@ async function initContextMenu(options) {
} }
} }
$contextMenuContainer.empty(); hide() {
// this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468
addItems($contextMenuContainer, options.items); // "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
keyboardActionService.updateDisplayedShortcuts($contextMenuContainer); if (Date.now() - this.dateContextMenuOpenedMs > 300) {
this.$widget.hide();
// 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();
} }
} }
export default { const contextMenu = new ContextMenu();
initContextMenu,
hideContextMenu export default contextMenu;
}

View File

@ -1,7 +1,7 @@
import hoistedNoteService from "../services/hoisted_note.js"; import hoistedNoteService from "../services/hoisted_note.js";
import treeService from "../services/tree.js"; import treeService from "../services/tree.js";
import utils from "../services/utils.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 treeCache from "../services/tree_cache.js";
import treeBuilder from "../services/tree_builder.js"; import treeBuilder from "../services/tree_builder.js";
import TreeContextMenu from "../services/tree_context_menu.js"; import TreeContextMenu from "../services/tree_context_menu.js";
@ -97,7 +97,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}, },
activate: async (event, data) => { activate: async (event, data) => {
// click event won't propagate so let's close context menu manually // click event won't propagate so let's close context menu manually
contextMenuWidget.hideContextMenu(); contextMenu.hide();
const notePath = treeService.getNotePath(data.node); const notePath = treeService.getNotePath(data.node);