1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: WYSIWYG: Enable context menu on resources, links and text

This commit is contained in:
Laurent Cozic 2020-05-09 19:18:41 +01:00
parent 734f83470c
commit 6ca41ddf80
5 changed files with 177 additions and 75 deletions

View File

@ -72,6 +72,7 @@ ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
ElectronClient/gui/NoteEditor/NoteEditor.js
ElectronClient/gui/NoteEditor/styles/index.js
ElectronClient/gui/NoteEditor/utils/contextMenu.js
ElectronClient/gui/NoteEditor/utils/index.js
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
ElectronClient/gui/NoteEditor/utils/types.js

1
.gitignore vendored
View File

@ -62,6 +62,7 @@ ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
ElectronClient/gui/NoteEditor/NoteEditor.js
ElectronClient/gui/NoteEditor/styles/index.js
ElectronClient/gui/NoteEditor/utils/contextMenu.js
ElectronClient/gui/NoteEditor/utils/index.js
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
ElectronClient/gui/NoteEditor/utils/types.js

View File

@ -3,11 +3,13 @@ import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHand
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { resourcesStatus } from '../../utils/resourceHandling';
import useScroll from './utils/useScroll';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu';
const { MarkupToHtml } = require('lib/joplin-renderer');
const taboverride = require('taboverride');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale');
const BaseItem = require('lib/models/BaseItem');
const Resource = require('lib/models/Resource');
const { themeStyle, buildStyle } = require('../../../../theme.js');
const { clipboard } = require('electron');
@ -147,6 +149,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
const props_onMessage = useRef(null);
props_onMessage.current = props.onMessage;
const contextMenuActionOptions = useRef<ContextMenuOptions>(null);
const markupToHtml = useRef(null);
markupToHtml.current = props.markupToHtml;
@ -434,7 +438,19 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
loadedAssetFiles_ = [];
function contextMenuItemNameWithNamespace(name:string) {
// For unknown reasons, TinyMCE converts all context menu names to
// lowercase when setting them in the init method, so we need to
// make them lowercase too, to make sure that the update() method
// addContextMenu is triggered.
return (`joplin${name}`).toLowerCase();
}
const loadEditor = async () => {
const contextMenuItems = menuItems();
const contextMenuItemNames = [];
for (const name in contextMenuItems) contextMenuItemNames.push(contextMenuItemNameWithNamespace(name));
const editors = await (window as any).tinymce.init({
selector: `#${rootIdRef.current}`,
width: '100%',
@ -454,6 +470,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
language: props.locale,
toolbar: 'bold italic | link joplinInlineCode joplinCodeBlock joplinAttach | numlist bullist joplinChecklist | h1 h2 h3 hr blockquote table joplinInsertDateTime',
localization_function: _,
contextmenu: contextMenuItemNames.join(' '),
setup: (editor:any) => {
function openEditDialog(editable:any) {
@ -573,6 +590,43 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
},
});
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];
const itemNameNS = contextMenuItemNameWithNamespace(itemName);
editor.ui.registry.addMenuItem(itemNameNS, {
text: item.label,
onAction: () => {
item.onAction(contextMenuActionOptions.current);
},
});
editor.ui.registry.addContextMenu(itemNameNS, {
update: function(element:any) {
let itemType:ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let textToCopy = '';
if (element.nodeName === 'IMG') {
itemType = ContextMenuItemType.Image;
resourceId = Resource.pathToId(element.src);
} else if (element.nodeName === 'A') {
resourceId = Resource.pathToId(element.href);
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
} else {
itemType = ContextMenuItemType.Text;
textToCopy = editor.selection.getContent({ format: 'text' });
}
contextMenuActionOptions.current = { itemType, resourceId, textToCopy };
return item.isActive(itemType) ? itemNameNS : '';
},
});
}
// TODO: remove event on unmount?
editor.on('DblClick', (event:any) => {
const editable = findEditableContainer(event.target);

View File

@ -0,0 +1,115 @@
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const Resource = require('lib/models/Resource.js');
const fs = require('fs-extra');
const { clipboard } = require('electron');
const { toSystemSlashes } = require('lib/path-utils');
const { _ } = require('lib/locale');
export enum ContextMenuItemType {
None = '',
Image = 'image',
Resource = 'resource',
Text = 'text',
Link = 'link',
}
export interface ContextMenuOptions {
itemType: ContextMenuItemType,
resourceId: string,
textToCopy: string,
}
interface ContextMenuItem {
label: string,
onAction: Function,
isActive: Function,
}
interface ContextMenuItems {
[key:string]: ContextMenuItem;
}
async function resourceInfo(options:ContextMenuOptions):Promise<any> {
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
const resourcePath = resource ? Resource.fullPath(resource) : '';
return { resource, resourcePath };
}
export function menuItems():ContextMenuItems {
return {
open: {
label: _('Open...'),
onAction: async (options:ContextMenuOptions) => {
const { resourcePath } = await resourceInfo(options);
const ok = bridge().openExternal(`file://${resourcePath}`);
if (!ok) bridge().showErrorMessageBox(_('This file could not be opened: %s', resourcePath));
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
saveAs: {
label: _('Save as...'),
onAction: async (options:ContextMenuOptions) => {
const { resourcePath, resource } = await resourceInfo(options);
const filePath = bridge().showSaveDialog({
defaultPath: resource.filename ? resource.filename : resource.title,
});
if (!filePath) return;
await fs.copy(resourcePath, filePath);
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
revealInFolder: {
label: _('Reveal file in folder'),
onAction: async (options:ContextMenuOptions) => {
const { resourcePath } = await resourceInfo(options);
bridge().showItemInFolder(resourcePath);
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
copyPathToClipboard: {
label: _('Copy path to clipboard'),
onAction: async (options:ContextMenuOptions) => {
const { resourcePath } = await resourceInfo(options);
clipboard.writeText(toSystemSlashes(resourcePath));
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
copy: {
label: _('Copy'),
onAction: async (options:ContextMenuOptions) => {
clipboard.writeText(options.textToCopy);
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Text,
},
copyLinkUrl: {
label: _('Copy Link Address'),
onAction: async (options:ContextMenuOptions) => {
clipboard.writeText(options.textToCopy);
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Link,
},
};
}
export default async function contextMenu(options:ContextMenuOptions) {
const menu = new Menu();
const items = menuItems();
for (const itemKey in items) {
const item = items[itemKey];
if (!item.isActive(options.itemType)) continue;
menu.append(new MenuItem({
label: item.label,
click: () => {
item.onAction(options);
},
}));
}
return menu;
}

View File

@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { FormNote } from './types';
import contextMenu from './contextMenu';
const BaseItem = require('lib/models/BaseItem');
const { _ } = require('lib/locale');
const BaseModel = require('lib/BaseModel.js');
@ -8,11 +9,6 @@ const { bridge } = require('electron').remote.require('./bridge');
const { urlDecode } = require('lib/string-utils');
const urlUtils = require('lib/urlUtils');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const fs = require('fs-extra');
const { clipboard } = require('electron');
const { toSystemSlashes } = require('lib/path-utils');
const { reg } = require('lib/registry.js');
export default function useMessageHandler(scrollWhenReady:any, setScrollWhenReady:Function, editorRef:any, setLocalSearchResultCount:Function, dispatch:Function, formNote:FormNote) {
@ -40,76 +36,11 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
if (s.length < 2) throw new Error(`Invalid message: ${msg}`);
ResourceFetcher.instance().markForDownload(s[1]);
} else if (msg === 'contextMenu') {
const itemType = arg0 && arg0.type;
const menu = new Menu();
if (itemType === 'image' || itemType === 'resource') {
const resource = await Resource.load(arg0.resourceId);
const resourcePath = Resource.fullPath(resource);
menu.append(
new MenuItem({
label: _('Open...'),
click: async () => {
const ok = bridge().openExternal(`file://${resourcePath}`);
if (!ok) bridge().showErrorMessageBox(_('This file could not be opened: %s', resourcePath));
},
})
);
menu.append(
new MenuItem({
label: _('Save as...'),
click: async () => {
const filePath = bridge().showSaveDialog({
defaultPath: resource.filename ? resource.filename : resource.title,
});
if (!filePath) return;
await fs.copy(resourcePath, filePath);
},
})
);
menu.append(
new MenuItem({
label: _('Reveal file in folder'),
click: async () => {
bridge().showItemInFolder(resourcePath);
},
})
);
menu.append(
new MenuItem({
label: _('Copy path to clipboard'),
click: async () => {
clipboard.writeText(toSystemSlashes(resourcePath));
},
})
);
} else if (itemType === 'text') {
menu.append(
new MenuItem({
label: _('Copy'),
click: async () => {
clipboard.writeText(arg0.textToCopy);
},
})
);
} else if (itemType === 'link') {
menu.append(
new MenuItem({
label: _('Copy Link Address'),
click: async () => {
clipboard.writeText(arg0.textToCopy);
},
})
);
} else {
reg.logger().error(`Unhandled item type: ${itemType}`);
return;
}
const menu = await contextMenu({
itemType: arg0 && arg0.type,
resourceId: arg0.resourceId,
textToCopy: arg0.textToCopy,
});
menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) {