From 9050035c7405236ea5a93384d58758f26b0d5633 Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Wed, 28 Nov 2018 00:44:15 +0100 Subject: [PATCH] - add option to enable/disable smart paste - add shortcut to paste smartly - use electron's clipboard --- browser/components/CodeEditor.js | 150 ++++++++++-------- browser/components/MarkdownEditor.js | 2 + browser/components/MarkdownSplitEditor.js | 2 + browser/main/Detail/SnippetNoteDetail.js | 2 + browser/main/lib/ConfigManager.js | 6 +- .../main/lib/dataApi/attachmentManagement.js | 39 +++++ .../main/modals/PreferencesModal/HotkeyTab.js | 14 +- browser/main/modals/PreferencesModal/UiTab.js | 15 +- 8 files changed, 163 insertions(+), 67 deletions(-) diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index d2fcb9c5..c7b6e385 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -13,7 +13,7 @@ import crypto from 'crypto' import consts from 'browser/lib/consts' import styles from '../components/CodeEditor.styl' import fs from 'fs' -const { ipcRenderer, remote } = require('electron') +const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') @@ -25,6 +25,10 @@ CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' const buildCMRulers = (rulers, enableRulers) => (enableRulers ? rulers.map(ruler => ({ column: ruler })) : []) +function translateHotkey (hotkey) { + return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') +} + export default class CodeEditor extends React.Component { constructor (props) { super(props) @@ -56,7 +60,11 @@ export default class CodeEditor extends React.Component { noteKey ) } - this.pasteHandler = (editor, e) => this.handlePaste(editor, e) + this.pasteHandler = (editor, e) => { + e.preventDefault() + + this.handlePaste(editor, false) + } this.loadStyleHandler = e => { this.editor.refresh() } @@ -120,42 +128,8 @@ export default class CodeEditor extends React.Component { } } - updateTableEditorState () { - const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) - if (active) { - if (this.extraKeysMode !== 'editor') { - this.extraKeysMode = 'editor' - this.editor.setOption('extraKeys', this.editorKeyMap) - } - } else { - if (this.extraKeysMode !== 'default') { - this.extraKeysMode = 'default' - this.editor.setOption('extraKeys', this.defaultKeyMap) - this.tableEditor.resetSmartCursor() - } - } - } - - componentDidMount () { - const { rulers, enableRulers } = this.props - const expandSnippet = this.expandSnippet.bind(this) - eventEmitter.on('line:jump', this.scrollToLineHandeler) - - const defaultSnippet = [ - { - id: crypto.randomBytes(16).toString('hex'), - name: 'Dummy text', - prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - } - ] - if (!fs.existsSync(consts.SNIPPET_FILE)) { - fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) - } + updateDefaultKeyMap () { + const { hotkey } = this.props this.defaultKeyMap = CodeMirror.normalizeKeyMap({ Tab: function (cm) { @@ -207,8 +181,51 @@ export default class CodeEditor extends React.Component { document.execCommand('copy') } return CodeMirror.Pass + }, + [translateHotkey(hotkey.pasteSmartly)]: cm => { + this.handlePaste(cm, true) } }) + } + + updateTableEditorState () { + const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) + if (active) { + if (this.extraKeysMode !== 'editor') { + this.extraKeysMode = 'editor' + this.editor.setOption('extraKeys', this.editorKeyMap) + } + } else { + if (this.extraKeysMode !== 'default') { + this.extraKeysMode = 'default' + this.editor.setOption('extraKeys', this.defaultKeyMap) + this.tableEditor.resetSmartCursor() + } + } + } + + componentDidMount () { + const { rulers, enableRulers } = this.props + const expandSnippet = this.expandSnippet.bind(this) + eventEmitter.on('line:jump', this.scrollToLineHandeler) + + const defaultSnippet = [ + { + id: crypto.randomBytes(16).toString('hex'), + name: 'Dummy text', + prefix: ['lorem', 'ipsum'], + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + } + ] + if (!fs.existsSync(consts.SNIPPET_FILE)) { + fs.writeFileSync( + consts.SNIPPET_FILE, + JSON.stringify(defaultSnippet, null, 4), + 'utf8' + ) + } + + this.updateDefaultKeyMap() this.value = this.props.value this.editor = CodeMirror(this.refs.root, { @@ -473,6 +490,14 @@ export default class CodeEditor extends React.Component { this.editor.setOption('extraKeys', this.defaultKeyMap) } + if (prevProps.hotkey !== this.props.hotkey) { + this.updateDefaultKeyMap() + + if (this.extraKeysMode === 'default') { + this.editor.setOption('extraKeys', this.defaultKeyMap) + } + } + if (this.state.clientWidth !== this.refs.root.clientWidth) { this.setState({ clientWidth: this.refs.root.clientWidth @@ -561,12 +586,8 @@ export default class CodeEditor extends React.Component { this.editor.replaceSelection(imageMd) } - handlePaste (editor, e) { - const { storageKey, noteKey, fetchUrlTitle } = this.props - - const clipboardData = e.clipboardData - const dataTransferItem = clipboardData.items[0] - const pastedTxt = clipboardData.getData('text') + handlePaste (editor, forceSmartPaste) { + const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props const isURL = str => { const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ @@ -623,27 +644,34 @@ export default class CodeEditor extends React.Component { return false } + const pastedTxt = clipboard.readText() + if (isInFencedCodeBlock(editor)) { - this.handlePasteText(e, editor, pastedTxt) - } else { - const pastedHtml = clipboardData.getData('text/html') - if (pastedHtml !== '') { - this.handlePasteHtml(e, editor, pastedHtml) - } else if (dataTransferItem.type.match('image')) { - attachmentManagement.handlePastImageEvent( + this.handlePasteText(editor, pastedTxt) + } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { + this.handlePasteUrl(editor, pastedTxt) + } else if (enableSmartPaste || forceSmartPaste) { + const image = clipboard.readImage() + if (!image.isEmpty()) { + attachmentManagement.handlePastNativeImage( this, storageKey, noteKey, - dataTransferItem + image ) - } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { - this.handlePasteUrl(e, editor, pastedTxt) + } else { + const pastedHtml = clipboard.readHTML() + if (pastedHtml.length > 0) { + this.handlePasteHtml(editor, pastedHtml) + } else { + this.handlePasteText(editor, pastedTxt) + } } + } else { + this.handlePasteText(editor, pastedTxt) } if (attachmentManagement.isAttachmentLink(pastedTxt)) { - e.preventDefault() - attachmentManagement .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) .then(modifiedText => { @@ -658,8 +686,7 @@ export default class CodeEditor extends React.Component { } } - handlePasteUrl (e, editor, pastedTxt) { - e.preventDefault() + handlePasteUrl (editor, pastedTxt) { const taggedUrl = `<${pastedTxt}>` editor.replaceSelection(taggedUrl) @@ -698,15 +725,12 @@ export default class CodeEditor extends React.Component { }) } - handlePasteHtml (e, editor, pastedHtml) { - e.preventDefault() + handlePasteHtml (editor, pastedHtml) { const markdown = this.turndownService.turndown(pastedHtml) editor.replaceSelection(markdown) } - handlePasteText (e, editor, pastedTxt) { - e.preventDefault() - + handlePasteText (editor, pastedTxt) { editor.replaceSelection(pastedTxt) } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 20ce9451..f1aa8d73 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -278,6 +278,8 @@ class MarkdownEditor extends React.Component { onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} spellCheck={config.editor.spellcheck} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} />
this.handleMouseDown(e)} >
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 4a38ffe5..c839fb94 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -717,6 +717,8 @@ class SnippetNoteDetail extends React.Component { enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleCodeChange(index)(e)} ref={'code-' + index} + enableSmartPaste={config.editor.enableSmartPaste} + hotkey={config.hotkey} /> }
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index d6b04d9b..c2ff9f7a 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -25,7 +25,8 @@ export const DEFAULT_CONFIG = { hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', - deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace' + deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', + pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V' }, ui: { language: 'en', @@ -52,7 +53,8 @@ export const DEFAULT_CONFIG = { enableTableEditor: false, enableFrontMatterTitle: true, frontMatterTitleField: 'title', - spellcheck: false + spellcheck: false, + enableSmartPaste: false }, preview: { fontSize: '14', diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index c193eaf2..373efddc 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -316,6 +316,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem reader.readAsDataURL(blob) } +/** + * @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code + * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code + * @param {String} storageKey Key of the current storage + * @param {String} noteKey Key of the current note + * @param {NativeImage} image The native image + */ +function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { + if (!codeEditor) { + throw new Error('codeEditor has to be given') + } + if (!storageKey) { + throw new Error('storageKey has to be given') + } + + if (!noteKey) { + throw new Error('noteKey has to be given') + } + if (!image) { + throw new Error('image has to be given') + } + + const targetStorage = findStorage.findStorage(storageKey) + const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + + createAttachmentDestinationFolder(targetStorage.path, noteKey) + + const imageName = `${uniqueSlug()}.png` + const imagePath = path.join(destinationDir, imageName) + + const binaryData = image.toPNG() + fs.writeFileSync(imagePath, binaryData, 'binary') + + const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName) + const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true) + codeEditor.insertAttachmentMd(imageMd) +} + /** * @description Returns all attachment paths of the given markdown * @param {String} markdownContent content in which the attachment paths should be found @@ -539,6 +577,7 @@ module.exports = { generateAttachmentMarkdown, handleAttachmentDrop, handlePastImageEvent, + handlePastNativeImage, getAttachmentsInMarkdownContent, getAbsolutePathsOfAttachmentsInContent, removeStorageAndNoteReferences, diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 7ad6f606..a0f6a739 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -79,7 +79,8 @@ class HotkeyTab extends React.Component { config.hotkey = { toggleMain: this.refs.toggleMain.value, toggleMode: this.refs.toggleMode.value, - deleteNote: this.refs.deleteNote.value + deleteNote: this.refs.deleteNote.value, + pasteSmartly: this.refs.pasteSmartly.value } this.setState({ config @@ -149,6 +150,17 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Paste Smartly')}
+
+ this.handleHotkeyChange(e)} + ref='pasteSmartly' + value={config.hotkey.pasteSmartly} + type='text' + /> +
+
+ +
+ +
+