mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +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/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
ElectronClient/gui/NoteEditor/NoteEditor.js
|
ElectronClient/gui/NoteEditor/NoteEditor.js
|
||||||
ElectronClient/gui/NoteEditor/styles/index.js
|
ElectronClient/gui/NoteEditor/styles/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/utils/contextMenu.js
|
||||||
ElectronClient/gui/NoteEditor/utils/index.js
|
ElectronClient/gui/NoteEditor/utils/index.js
|
||||||
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
|
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
|
||||||
ElectronClient/gui/NoteEditor/utils/types.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/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
ElectronClient/gui/NoteEditor/NoteEditor.js
|
ElectronClient/gui/NoteEditor/NoteEditor.js
|
||||||
ElectronClient/gui/NoteEditor/styles/index.js
|
ElectronClient/gui/NoteEditor/styles/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/utils/contextMenu.js
|
||||||
ElectronClient/gui/NoteEditor/utils/index.js
|
ElectronClient/gui/NoteEditor/utils/index.js
|
||||||
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
|
ElectronClient/gui/NoteEditor/utils/resourceHandling.js
|
||||||
ElectronClient/gui/NoteEditor/utils/types.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 { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||||
import { resourcesStatus } from '../../utils/resourceHandling';
|
import { resourcesStatus } from '../../utils/resourceHandling';
|
||||||
import useScroll from './utils/useScroll';
|
import useScroll from './utils/useScroll';
|
||||||
|
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu';
|
||||||
const { MarkupToHtml } = require('lib/joplin-renderer');
|
const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||||
const taboverride = require('taboverride');
|
const taboverride = require('taboverride');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { _ } = require('lib/locale');
|
const { _ } = require('lib/locale');
|
||||||
const BaseItem = require('lib/models/BaseItem');
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
|
const Resource = require('lib/models/Resource');
|
||||||
const { themeStyle, buildStyle } = require('../../../../theme.js');
|
const { themeStyle, buildStyle } = require('../../../../theme.js');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
|
|
||||||
@ -147,6 +149,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
const props_onMessage = useRef(null);
|
const props_onMessage = useRef(null);
|
||||||
props_onMessage.current = props.onMessage;
|
props_onMessage.current = props.onMessage;
|
||||||
|
|
||||||
|
const contextMenuActionOptions = useRef<ContextMenuOptions>(null);
|
||||||
|
|
||||||
const markupToHtml = useRef(null);
|
const markupToHtml = useRef(null);
|
||||||
markupToHtml.current = props.markupToHtml;
|
markupToHtml.current = props.markupToHtml;
|
||||||
|
|
||||||
@ -434,7 +438,19 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
|
|
||||||
loadedAssetFiles_ = [];
|
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 loadEditor = async () => {
|
||||||
|
const contextMenuItems = menuItems();
|
||||||
|
const contextMenuItemNames = [];
|
||||||
|
for (const name in contextMenuItems) contextMenuItemNames.push(contextMenuItemNameWithNamespace(name));
|
||||||
|
|
||||||
const editors = await (window as any).tinymce.init({
|
const editors = await (window as any).tinymce.init({
|
||||||
selector: `#${rootIdRef.current}`,
|
selector: `#${rootIdRef.current}`,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -454,6 +470,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
language: props.locale,
|
language: props.locale,
|
||||||
toolbar: 'bold italic | link joplinInlineCode joplinCodeBlock joplinAttach | numlist bullist joplinChecklist | h1 h2 h3 hr blockquote table joplinInsertDateTime',
|
toolbar: 'bold italic | link joplinInlineCode joplinCodeBlock joplinAttach | numlist bullist joplinChecklist | h1 h2 h3 hr blockquote table joplinInsertDateTime',
|
||||||
localization_function: _,
|
localization_function: _,
|
||||||
|
contextmenu: contextMenuItemNames.join(' '),
|
||||||
setup: (editor:any) => {
|
setup: (editor:any) => {
|
||||||
|
|
||||||
function openEditDialog(editable: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?
|
// TODO: remove event on unmount?
|
||||||
editor.on('DblClick', (event:any) => {
|
editor.on('DblClick', (event:any) => {
|
||||||
const editable = findEditableContainer(event.target);
|
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 { useCallback } from 'react';
|
||||||
import { FormNote } from './types';
|
import { FormNote } from './types';
|
||||||
|
import contextMenu from './contextMenu';
|
||||||
const BaseItem = require('lib/models/BaseItem');
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
const { _ } = require('lib/locale');
|
const { _ } = require('lib/locale');
|
||||||
const BaseModel = require('lib/BaseModel.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
@ -8,11 +9,6 @@ const { bridge } = require('electron').remote.require('./bridge');
|
|||||||
const { urlDecode } = require('lib/string-utils');
|
const { urlDecode } = require('lib/string-utils');
|
||||||
const urlUtils = require('lib/urlUtils');
|
const urlUtils = require('lib/urlUtils');
|
||||||
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
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');
|
const { reg } = require('lib/registry.js');
|
||||||
|
|
||||||
export default function useMessageHandler(scrollWhenReady:any, setScrollWhenReady:Function, editorRef:any, setLocalSearchResultCount:Function, dispatch:Function, formNote:FormNote) {
|
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}`);
|
if (s.length < 2) throw new Error(`Invalid message: ${msg}`);
|
||||||
ResourceFetcher.instance().markForDownload(s[1]);
|
ResourceFetcher.instance().markForDownload(s[1]);
|
||||||
} else if (msg === 'contextMenu') {
|
} else if (msg === 'contextMenu') {
|
||||||
const itemType = arg0 && arg0.type;
|
const menu = await contextMenu({
|
||||||
|
itemType: arg0 && arg0.type,
|
||||||
const menu = new Menu();
|
resourceId: arg0.resourceId,
|
||||||
|
textToCopy: arg0.textToCopy,
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.popup(bridge().window());
|
menu.popup(bridge().window());
|
||||||
} else if (msg.indexOf('joplin://') === 0) {
|
} else if (msg.indexOf('joplin://') === 0) {
|
||||||
|
Loading…
Reference in New Issue
Block a user