mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
This commit is contained in:
parent
37d51c3b58
commit
a62e1fba96
@ -490,6 +490,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
||||||
@ -724,6 +730,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
|
|||||||
packages/app-desktop/gui/utils/loadScript.d.ts
|
packages/app-desktop/gui/utils/loadScript.d.ts
|
||||||
packages/app-desktop/gui/utils/loadScript.js
|
packages/app-desktop/gui/utils/loadScript.js
|
||||||
packages/app-desktop/gui/utils/loadScript.js.map
|
packages/app-desktop/gui/utils/loadScript.js.map
|
||||||
|
packages/app-desktop/loadResources.testEnv.d.ts
|
||||||
|
packages/app-desktop/loadResources.testEnv.js
|
||||||
|
packages/app-desktop/loadResources.testEnv.js.map
|
||||||
packages/app-desktop/plugins/GotoAnything.d.ts
|
packages/app-desktop/plugins/GotoAnything.d.ts
|
||||||
packages/app-desktop/plugins/GotoAnything.js
|
packages/app-desktop/plugins/GotoAnything.js
|
||||||
packages/app-desktop/plugins/GotoAnything.js.map
|
packages/app-desktop/plugins/GotoAnything.js.map
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -480,6 +480,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
||||||
@ -714,6 +720,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
|
|||||||
packages/app-desktop/gui/utils/loadScript.d.ts
|
packages/app-desktop/gui/utils/loadScript.d.ts
|
||||||
packages/app-desktop/gui/utils/loadScript.js
|
packages/app-desktop/gui/utils/loadScript.js
|
||||||
packages/app-desktop/gui/utils/loadScript.js.map
|
packages/app-desktop/gui/utils/loadScript.js.map
|
||||||
|
packages/app-desktop/loadResources.testEnv.d.ts
|
||||||
|
packages/app-desktop/loadResources.testEnv.js
|
||||||
|
packages/app-desktop/loadResources.testEnv.js.map
|
||||||
packages/app-desktop/plugins/GotoAnything.d.ts
|
packages/app-desktop/plugins/GotoAnything.d.ts
|
||||||
packages/app-desktop/plugins/GotoAnything.js
|
packages/app-desktop/plugins/GotoAnything.js
|
||||||
packages/app-desktop/plugins/GotoAnything.js.map
|
packages/app-desktop/plugins/GotoAnything.js.map
|
||||||
|
@ -3,7 +3,8 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
|||||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import bridge from '../../../../../services/bridge';
|
import bridge from '../../../../../services/bridge';
|
||||||
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenu';
|
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
|
||||||
|
import { menuItems } from '../../../utils/contextMenu';
|
||||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
|
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
|
||||||
@ -67,6 +68,8 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function)
|
|||||||
contextMenuActionOptions.current = {
|
contextMenuActionOptions.current = {
|
||||||
itemType,
|
itemType,
|
||||||
resourceId,
|
resourceId,
|
||||||
|
filename: null,
|
||||||
|
mime: null,
|
||||||
linkToCopy,
|
linkToCopy,
|
||||||
textToCopy: null,
|
textToCopy: null,
|
||||||
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/** @jest-environment ./loadResources.testEnv */
|
||||||
|
// eslint-disable-next-line strict, lines-around-directive
|
||||||
|
'use strict';
|
||||||
|
// use strict is necessary here so that typescript doesn't place "use strict" above the jest docblock
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/15819#issuecomment-782235619
|
||||||
|
|
||||||
|
import { textToDataUri, svgUriToPng } from './contextMenuUtils';
|
||||||
|
|
||||||
|
jest.mock('@joplin/lib/models/Resource');
|
||||||
|
|
||||||
|
describe('contextMenu', () => {
|
||||||
|
it('should provide proper copy path', async () => {
|
||||||
|
const testCase = [
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">test</svg>',
|
||||||
|
'image/svg+xml',
|
||||||
|
];
|
||||||
|
const expectedText = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIj50ZXN0PC9zdmc+';
|
||||||
|
expect(textToDataUri(testCase[0], testCase[1])).toBe(expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert to png binary', async () => {
|
||||||
|
const testCase = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiIGhlaWdodD0iMTAwcHgiIHdpZHRoPSIxMDBweCI+CjxnPgoJPHBhdGggZD0iTTI4LjEsMzYuNmM0LjYsMS45LDEyLjIsMS42LDIwLjksMS4xYzguOS0wLjQsMTktMC45LDI4LjksMC45YzYuMywxLjIsMTEuOSwzLjEsMTYuOCw2Yy0xLjUtMTIuMi03LjktMjMuNy0xOC42LTMxLjMgICBjLTQuOS0wLjItOS45LDAuMy0xNC44LDEuNEM0Ny44LDE3LjksMzYuMiwyNS42LDI4LjEsMzYuNnoiLz4KCTxwYXRoIGQ9Ik03MC4zLDkuOEM1Ny41LDMuNCw0Mi44LDMuNiwzMC41LDkuNWMtMyw2LTguNCwxOS42LTUuMywyNC45YzguNi0xMS43LDIwLjktMTkuOCwzNS4yLTIzLjFDNjMuNywxMC41LDY3LDEwLDcwLjMsOS44eiIvPgoJPHBhdGggZD0iTTE2LjUsNTEuM2MwLjYtMS43LDEuMi0zLjQsMi01LjFjLTMuOC0zLjQtNy41LTctMTEtMTAuOGMtMi4xLDYuMS0yLjgsMTIuNS0yLjMsMTguN0M5LjYsNTEuMSwxMy40LDUwLjIsMTYuNSw1MS4zeiIvPgoJPHBhdGggZD0iTTksMzEuNmMzLjUsMy45LDcuMiw3LjYsMTEuMSwxMS4xYzAuOC0xLjYsMS43LTMuMSwyLjYtNC42YzAuMS0wLjIsMC4zLTAuNCwwLjQtMC42Yy0yLjktMy4zLTMuMS05LjItMC42LTE3LjYgICBjMC44LTIuNywxLjgtNS4zLDIuNy03LjRjLTUuMiwzLjQtOS44LDgtMTMuMywxMy43QzEwLjgsMjcuOSw5LjgsMjkuNyw5LDMxLjZ6Ii8+Cgk8cGF0aCBkPSJNMTUuNCw1NC43Yy0yLjYtMS02LjEsMC43LTkuNywzLjRjMS4yLDYuNiwzLjksMTMsOCwxOC41QzEzLDY5LjMsMTMuNSw2MS44LDE1LjQsNTQuN3oiLz4KCTxwYXRoIGQ9Ik0zOS44LDU3LjZDNTQuMyw2Ni43LDcwLDczLDg2LjUsNzYuNGMwLjYtMC44LDEuMS0xLjYsMS43LTIuNWM0LjgtNy43LDctMTYuMyw2LjgtMjQuOGMtMTMuOC05LjMtMzEuMy04LjQtNDUuOC03LjcgICBjLTkuNSwwLjUtMTcuOCwwLjktMjMuMi0xLjdjLTAuMSwwLjEtMC4yLDAuMy0wLjMsMC40Yy0xLDEuNy0yLDMuNC0yLjksNS4xQzI4LjIsNDkuNywzMy44LDUzLjksMzkuOCw1Ny42eiIvPgoJPHBhdGggZD0iTTI2LjIsODguMmMzLjMsMiw2LjcsMy42LDEwLjIsNC43Yy0zLjUtNi4yLTYuMy0xMi42LTguOC0xOC41Yy0zLjEtNy4yLTUuOC0xMy41LTktMTcuMmMtMS45LDgtMiwxNi40LTAuMywyNC43ICAgQzIwLjYsODQuMiwyMy4yLDg2LjMsMjYuMiw4OC4yeiIvPgoJPHBhdGggZD0iTTMwLjksNzNjMi45LDYuOCw2LjEsMTQuNCwxMC41LDIxLjJjMTUuNiwzLDMyLTIuMyw0Mi42LTE0LjZDNjcuNyw3Niw1Mi4yLDY5LjYsMzcuOSw2MC43QzMyLDU3LDI2LjUsNTMsMjEuMyw0OC42ICAgYy0wLjYsMS41LTEuMiwzLTEuNyw0LjZDMjQuMSw1Ny4xLDI3LjMsNjQuNSwzMC45LDczeiIvPgo8L2c+Cjwvc3ZnPg==';
|
||||||
|
const png = await svgUriToPng(document, testCase);
|
||||||
|
expect(png).toBeInstanceOf(Uint8Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid svg uri', async () => {
|
||||||
|
// We are mocking console.error since jsdom throws errors to console when we try to load an invalid img
|
||||||
|
// https://github.com/facebook/jest/pull/5267#issuecomment-356605468
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn();
|
||||||
|
const testCases: Array<string> = [
|
||||||
|
'data:image/svg+xml;base64,error',
|
||||||
|
'invalid',
|
||||||
|
];
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
await expect(svgUriToPng(document, testCase)).rejects.toBeInstanceOf(Error);
|
||||||
|
}
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
});
|
@ -2,6 +2,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { copyHtmlToClipboard } from './clipboardUtils';
|
import { copyHtmlToClipboard } from './clipboardUtils';
|
||||||
import bridge from '../../../services/bridge';
|
import bridge from '../../../services/bridge';
|
||||||
|
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng } from './contextMenuUtils';
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem = bridge().MenuItem;
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
@ -10,43 +11,10 @@ import BaseModel from '@joplin/lib/BaseModel';
|
|||||||
import { processPastedHtml } from './resourceHandling';
|
import { processPastedHtml } from './resourceHandling';
|
||||||
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const { writeFile } = require('fs-extra');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const { toSystemSlashes } = require('@joplin/lib/path-utils');
|
const { toSystemSlashes } = require('@joplin/lib/path-utils');
|
||||||
|
|
||||||
export enum ContextMenuItemType {
|
|
||||||
None = '',
|
|
||||||
Image = 'image',
|
|
||||||
Resource = 'resource',
|
|
||||||
Text = 'text',
|
|
||||||
Link = 'link',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContextMenuOptions {
|
|
||||||
itemType: ContextMenuItemType;
|
|
||||||
resourceId: string;
|
|
||||||
linkToCopy: string;
|
|
||||||
textToCopy: string;
|
|
||||||
htmlToCopy: string;
|
|
||||||
insertContent: Function;
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCopyToClipboard(options: ContextMenuOptions) {
|
function handleCopyToClipboard(options: ContextMenuOptions) {
|
||||||
if (options.textToCopy) {
|
if (options.textToCopy) {
|
||||||
clipboard.writeText(options.textToCopy);
|
clipboard.writeText(options.textToCopy);
|
||||||
@ -55,6 +23,12 @@ function handleCopyToClipboard(options: ContextMenuOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveFileData(data: any, filename: string) {
|
||||||
|
const newFilePath = await bridge().showSaveDialog({ defaultPath: filename });
|
||||||
|
if (!newFilePath) return;
|
||||||
|
await writeFile(newFilePath, data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
|
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
|
||||||
|
|
||||||
const item = await BaseItem.loadItemById(itemId);
|
const item = await BaseItem.loadItemById(itemId);
|
||||||
@ -100,7 +74,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
onAction: async (options: ContextMenuOptions) => {
|
onAction: async (options: ContextMenuOptions) => {
|
||||||
await openItemById(options.resourceId, dispatch);
|
await openItemById(options.resourceId, dispatch);
|
||||||
},
|
},
|
||||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||||
},
|
},
|
||||||
saveAs: {
|
saveAs: {
|
||||||
label: _('Save as...'),
|
label: _('Save as...'),
|
||||||
@ -112,7 +86,32 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
await fs.copy(resourcePath, filePath);
|
await fs.copy(resourcePath, filePath);
|
||||||
},
|
},
|
||||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
// We handle images received as text seperately as it can be saved in multiple formats
|
||||||
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||||
|
},
|
||||||
|
saveAsSvg: {
|
||||||
|
label: _('Save as SVG'),
|
||||||
|
onAction: async (options: ContextMenuOptions) => {
|
||||||
|
await saveFileData(options.textToCopy, options.filename);
|
||||||
|
},
|
||||||
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||||
|
},
|
||||||
|
saveAsPng: {
|
||||||
|
label: _('Save as PNG'),
|
||||||
|
onAction: async (options: ContextMenuOptions) => {
|
||||||
|
// First convert it to png then save
|
||||||
|
if (options.mime != 'image/svg+xml') {
|
||||||
|
throw new Error(`Unsupported image type: ${options.mime}`);
|
||||||
|
}
|
||||||
|
if (!options.filename) {
|
||||||
|
throw new Error('Filename is needed to save as png');
|
||||||
|
}
|
||||||
|
const dataUri = textToDataUri(options.textToCopy, options.mime);
|
||||||
|
const png = await svgUriToPng(document, dataUri);
|
||||||
|
const filename = options.filename.replace('.svg', '.png');
|
||||||
|
await saveFileData(png, filename);
|
||||||
|
},
|
||||||
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||||
},
|
},
|
||||||
revealInFolder: {
|
revealInFolder: {
|
||||||
label: _('Reveal file in folder'),
|
label: _('Reveal file in folder'),
|
||||||
@ -120,13 +119,20 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
const { resourcePath } = await resourceInfo(options);
|
const { resourcePath } = await resourceInfo(options);
|
||||||
bridge().showItemInFolder(resourcePath);
|
bridge().showItemInFolder(resourcePath);
|
||||||
},
|
},
|
||||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||||
},
|
},
|
||||||
copyPathToClipboard: {
|
copyPathToClipboard: {
|
||||||
label: _('Copy path to clipboard'),
|
label: _('Copy path to clipboard'),
|
||||||
onAction: async (options: ContextMenuOptions) => {
|
onAction: async (options: ContextMenuOptions) => {
|
||||||
|
let path = '';
|
||||||
|
if (options.textToCopy && options.mime) {
|
||||||
|
path = textToDataUri(options.textToCopy, options.mime);
|
||||||
|
} else {
|
||||||
const { resourcePath } = await resourceInfo(options);
|
const { resourcePath } = await resourceInfo(options);
|
||||||
clipboard.writeText(toSystemSlashes(resourcePath));
|
if (resourcePath) path = toSystemSlashes(resourcePath);
|
||||||
|
}
|
||||||
|
if (!path) return;
|
||||||
|
clipboard.writeText(path);
|
||||||
},
|
},
|
||||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||||
},
|
},
|
||||||
@ -137,7 +143,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
const image = bridge().createImageFromPath(resourcePath);
|
const image = bridge().createImageFromPath(resourcePath);
|
||||||
clipboard.writeImage(image);
|
clipboard.writeImage(image);
|
||||||
},
|
},
|
||||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image,
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image,
|
||||||
},
|
},
|
||||||
cut: {
|
cut: {
|
||||||
label: _('Cut'),
|
label: _('Cut'),
|
||||||
@ -145,14 +151,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
handleCopyToClipboard(options);
|
handleCopyToClipboard(options);
|
||||||
options.insertContent('');
|
options.insertContent('');
|
||||||
},
|
},
|
||||||
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy),
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
label: _('Copy'),
|
label: _('Copy'),
|
||||||
onAction: async (options: ContextMenuOptions) => {
|
onAction: async (options: ContextMenuOptions) => {
|
||||||
handleCopyToClipboard(options);
|
handleCopyToClipboard(options);
|
||||||
},
|
},
|
||||||
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy || !!options.htmlToCopy,
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
|
||||||
},
|
},
|
||||||
paste: {
|
paste: {
|
||||||
label: _('Paste'),
|
label: _('Paste'),
|
||||||
@ -184,7 +190,6 @@ export default async function contextMenu(options: ContextMenuOptions, dispatch:
|
|||||||
const items = menuItems(dispatch);
|
const items = menuItems(dispatch);
|
||||||
|
|
||||||
if (!('readyOnly' in options)) options.isReadOnly = true;
|
if (!('readyOnly' in options)) options.isReadOnly = true;
|
||||||
|
|
||||||
for (const itemKey in items) {
|
for (const itemKey in items) {
|
||||||
const item = items[itemKey];
|
const item = items[itemKey];
|
||||||
|
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
|
|
||||||
|
export enum ContextMenuItemType {
|
||||||
|
None = '',
|
||||||
|
Image = 'image',
|
||||||
|
Resource = 'resource',
|
||||||
|
Text = 'text',
|
||||||
|
Link = 'link',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuOptions {
|
||||||
|
itemType: ContextMenuItemType;
|
||||||
|
resourceId: string;
|
||||||
|
mime: string;
|
||||||
|
filename: string;
|
||||||
|
linkToCopy: string;
|
||||||
|
textToCopy: string;
|
||||||
|
htmlToCopy: string;
|
||||||
|
insertContent: Function;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
label: string;
|
||||||
|
onAction: Function;
|
||||||
|
isActive: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuItems {
|
||||||
|
[key: string]: ContextMenuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resourceInfo(options: ContextMenuOptions): Promise<any> {
|
||||||
|
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
|
||||||
|
const filePath = resource ? Resource.fullPath(resource) : null;
|
||||||
|
const filename = resource ? (resource.filename ? resource.filename : resource.title) : options.filename ? options.filename : '';
|
||||||
|
return { resource, filePath, filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textToDataUri(text: string, mime: string): string {
|
||||||
|
return `data:${mime};base64,${Buffer.from(text).toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const svgUriToPng = (document: Document, svg: string) => {
|
||||||
|
return new Promise<Uint8Array>((resolve, reject) => {
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let img: HTMLImageElement;
|
||||||
|
|
||||||
|
const cleanUpAndReject = (e: Error) => {
|
||||||
|
if (canvas) canvas.remove();
|
||||||
|
if (img) img.remove();
|
||||||
|
return reject(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
img = document.createElement('img');
|
||||||
|
if (!img) throw new Error('Failed to create img element');
|
||||||
|
} catch (e) {
|
||||||
|
return cleanUpAndReject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
try {
|
||||||
|
canvas = document.createElement('canvas');
|
||||||
|
if (!canvas) throw new Error('Failed to create canvas element');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Failed to get context');
|
||||||
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
|
||||||
|
const pngUri = canvas.toDataURL('image/png');
|
||||||
|
if (!pngUri) throw new Error('Failed to generate png uri');
|
||||||
|
const pngBase64 = pngUri.split(',')[1];
|
||||||
|
const byteString = atob(pngBase64);
|
||||||
|
// write the bytes of the string to a typed array
|
||||||
|
const buff = new Uint8Array(byteString.length);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
buff[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
canvas.remove();
|
||||||
|
img.remove();
|
||||||
|
resolve(buff);
|
||||||
|
} catch (err) {
|
||||||
|
cleanUpAndReject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.onerror = function(e) {
|
||||||
|
cleanUpAndReject(new Error(e.toString()));
|
||||||
|
};
|
||||||
|
img.src = svg;
|
||||||
|
});
|
||||||
|
};
|
@ -35,6 +35,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
|||||||
const menu = await contextMenu({
|
const menu = await contextMenu({
|
||||||
itemType: arg0 && arg0.type,
|
itemType: arg0 && arg0.type,
|
||||||
resourceId: arg0.resourceId,
|
resourceId: arg0.resourceId,
|
||||||
|
filename: arg0.filename,
|
||||||
|
mime: arg0.mime,
|
||||||
textToCopy: arg0.textToCopy,
|
textToCopy: arg0.textToCopy,
|
||||||
linkToCopy: arg0.linkToCopy || null,
|
linkToCopy: arg0.linkToCopy || null,
|
||||||
htmlToCopy: '',
|
htmlToCopy: '',
|
||||||
|
@ -586,9 +586,24 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
|
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
|
||||||
|
// To handle right clicks on resource icons
|
||||||
let element = event.target;
|
let element = event.target;
|
||||||
|
|
||||||
// To handle right clicks on resource icons
|
// Mermaid svgs are wrapped inside a <pre> with class "mermaid"
|
||||||
|
let mermaidElement = element.closest(".mermaid")?.children[0];
|
||||||
|
if (mermaidElement) {
|
||||||
|
const svgString = new XMLSerializer().serializeToString(mermaidElement);
|
||||||
|
if (!!svgString) {
|
||||||
|
ipcProxySendToHost('contextMenu', {
|
||||||
|
type: 'image',
|
||||||
|
textToCopy: svgString,
|
||||||
|
mime: 'image/svg+xml',
|
||||||
|
filename: mermaidElement.id + '.svg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
|
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
|
||||||
|
|
||||||
if (element && element.getAttribute('data-resource-id')) {
|
if (element && element.getAttribute('data-resource-id')) {
|
||||||
|
20
packages/app-desktop/loadResources.testEnv.ts
Normal file
20
packages/app-desktop/loadResources.testEnv.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* A Jest custom test Environment to load the resources for the tests.
|
||||||
|
* Use this test envirenment when you work with resources like images, files.
|
||||||
|
* See gui/NoteEditor/utils/contextMenu.test.ts for an example.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const JSDOMEnvironment = require('jest-environment-jsdom');
|
||||||
|
import type { EnvironmentContext } from '@jest/environment';
|
||||||
|
import type { Config } from '@jest/types';
|
||||||
|
|
||||||
|
|
||||||
|
export default class CustomEnvironment extends JSDOMEnvironment {
|
||||||
|
constructor(config: Config.ProjectConfig, context?: EnvironmentContext) {
|
||||||
|
// Resources is set to 'usable' to enable fetching of resources like images and fonts while testing
|
||||||
|
// Which does not happen by default in jest
|
||||||
|
// https://stackoverflow.com/a/49482563
|
||||||
|
config.testEnvironmentOptions.resources = 'usable';
|
||||||
|
super(config, context);
|
||||||
|
}
|
||||||
|
}
|
@ -116,6 +116,7 @@
|
|||||||
"app-builder-bin": "^1.9.11",
|
"app-builder-bin": "^1.9.11",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"canvas": "^2.9.0",
|
||||||
"electron": "14.1.0",
|
"electron": "14.1.0",
|
||||||
"electron-builder": "^22.11.7",
|
"electron-builder": "^22.11.7",
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
|
81
yarn.lock
81
yarn.lock
@ -2910,6 +2910,7 @@ __metadata:
|
|||||||
async-mutex: ^0.1.3
|
async-mutex: ^0.1.3
|
||||||
babel-cli: ^6.26.0
|
babel-cli: ^6.26.0
|
||||||
babel-preset-react: ^6.24.1
|
babel-preset-react: ^6.24.1
|
||||||
|
canvas: ^2.9.0
|
||||||
codemirror: ^5.56.0
|
codemirror: ^5.56.0
|
||||||
color: ^3.1.2
|
color: ^3.1.2
|
||||||
compare-versions: ^3.2.1
|
compare-versions: ^3.2.1
|
||||||
@ -4339,6 +4340,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@mapbox/node-pre-gyp@npm:^1.0.0":
|
||||||
|
version: 1.0.8
|
||||||
|
resolution: "@mapbox/node-pre-gyp@npm:1.0.8"
|
||||||
|
dependencies:
|
||||||
|
detect-libc: ^1.0.3
|
||||||
|
https-proxy-agent: ^5.0.0
|
||||||
|
make-dir: ^3.1.0
|
||||||
|
node-fetch: ^2.6.5
|
||||||
|
nopt: ^5.0.0
|
||||||
|
npmlog: ^5.0.1
|
||||||
|
rimraf: ^3.0.2
|
||||||
|
semver: ^7.3.5
|
||||||
|
tar: ^6.1.11
|
||||||
|
bin:
|
||||||
|
node-pre-gyp: bin/node-pre-gyp
|
||||||
|
checksum: 29a38f39575107fa1665edf14defcfdf62e12bb38e9c27f7457ba42be84060125015171d12b8de3065155a465992f1854a363e2985f071fcbea9ff0701362b05
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@mrmlnc/readdir-enhanced@npm:^2.2.1":
|
"@mrmlnc/readdir-enhanced@npm:^2.2.1":
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1"
|
resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1"
|
||||||
@ -8915,6 +8935,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"canvas@npm:^2.9.0":
|
||||||
|
version: 2.9.0
|
||||||
|
resolution: "canvas@npm:2.9.0"
|
||||||
|
dependencies:
|
||||||
|
"@mapbox/node-pre-gyp": ^1.0.0
|
||||||
|
nan: ^2.15.0
|
||||||
|
node-gyp: latest
|
||||||
|
simple-get: ^3.0.3
|
||||||
|
checksum: 376ccd47340a46c04d5cabafd6feb1b7ae82c92dc3ae6db68c9cbac17ec1c43d2bf6aab60019690e9d49bf40b41bee3e4e0a8901f39b53e993789698e77e2699
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"capital-case@npm:^1.0.4":
|
"capital-case@npm:^1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "capital-case@npm:1.0.4"
|
resolution: "capital-case@npm:1.0.4"
|
||||||
@ -14886,6 +14918,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"gauge@npm:^3.0.0":
|
||||||
|
version: 3.0.2
|
||||||
|
resolution: "gauge@npm:3.0.2"
|
||||||
|
dependencies:
|
||||||
|
aproba: ^1.0.3 || ^2.0.0
|
||||||
|
color-support: ^1.1.2
|
||||||
|
console-control-strings: ^1.0.0
|
||||||
|
has-unicode: ^2.0.1
|
||||||
|
object-assign: ^4.1.1
|
||||||
|
signal-exit: ^3.0.0
|
||||||
|
string-width: ^4.2.3
|
||||||
|
strip-ansi: ^6.0.1
|
||||||
|
wide-align: ^1.1.2
|
||||||
|
checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"gauge@npm:^4.0.0":
|
"gauge@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "gauge@npm:4.0.0"
|
resolution: "gauge@npm:4.0.0"
|
||||||
@ -20230,7 +20279,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"make-dir@npm:^3.0.0":
|
"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "make-dir@npm:3.1.0"
|
resolution: "make-dir@npm:3.1.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -21751,7 +21800,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"nan@npm:^2.12.1":
|
"nan@npm:^2.12.1, nan@npm:^2.15.0":
|
||||||
version: 2.15.0
|
version: 2.15.0
|
||||||
resolution: "nan@npm:2.15.0"
|
resolution: "nan@npm:2.15.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -22019,6 +22068,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"node-fetch@npm:^2.6.5":
|
||||||
|
version: 2.6.7
|
||||||
|
resolution: "node-fetch@npm:2.6.7"
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: ^5.0.0
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-gyp-build@npm:^4.2.1":
|
"node-gyp-build@npm:^4.2.1":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "node-gyp-build@npm:4.3.0"
|
resolution: "node-gyp-build@npm:4.3.0"
|
||||||
@ -22473,6 +22536,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"npmlog@npm:^5.0.1":
|
||||||
|
version: 5.0.1
|
||||||
|
resolution: "npmlog@npm:5.0.1"
|
||||||
|
dependencies:
|
||||||
|
are-we-there-yet: ^2.0.0
|
||||||
|
console-control-strings: ^1.1.0
|
||||||
|
gauge: ^3.0.0
|
||||||
|
set-blocking: ^2.0.0
|
||||||
|
checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"npmlog@npm:^6.0.0":
|
"npmlog@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "npmlog@npm:6.0.0"
|
resolution: "npmlog@npm:6.0.0"
|
||||||
@ -28646,7 +28721,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tar@npm:^6.0.2, tar@npm:^6.0.5, tar@npm:^6.1.2":
|
"tar@npm:^6.0.2, tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||||
version: 6.1.11
|
version: 6.1.11
|
||||||
resolution: "tar@npm:6.1.11"
|
resolution: "tar@npm:6.1.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user