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:
parent
734f83470c
commit
6ca41ddf80
@ -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
1
.gitignore
vendored
@ -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
|
||||
|
@ -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);
|
||||
|
115
ElectronClient/gui/NoteEditor/utils/contextMenu.ts
Normal file
115
ElectronClient/gui/NoteEditor/utils/contextMenu.ts
Normal 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;
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user