Skip to content

Commit

Permalink
Desktop: WYSIWYG: Enable context menu on resources, links and text
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed May 9, 2020
1 parent 734f834 commit 6ca41dd
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 75 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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%',
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
115 changes: 115 additions & 0 deletions ElectronClient/gui/NoteEditor/utils/contextMenu.ts
Original file line number Diff line number Diff line change
@@ -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;
}
81 changes: 6 additions & 75 deletions ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 6ca41dd

Please sign in to comment.