From 8d4a05157dc95ee14aceb526dd8e8007a75cff84 Mon Sep 17 00:00:00 2001 From: Thomas Zarebczan Date: Thu, 31 Mar 2022 15:55:00 -0400 Subject: [PATCH] Paste/drop images directly to markdown editor Ticket: 1135 - Changed `FileDrop` to only cover the upper 20% of the page, otherwise it will clash with markdown image drop. --- package.json | 1 + static/app-strings.json | 1 + .../common/form-components/form-field.jsx | 28 +++++++++++++++++-- ui/component/fileDrop/view.jsx | 8 ++++-- ui/constants/cdn_urls.js | 18 ++++++++++++ ui/effects/use-drag-drop.js | 13 +++++---- ui/scss/component/_file-drop.scss | 2 +- ui/scss/component/_markdown-editor.scss | 8 ++++++ yarn.lock | 5 ++++ 9 files changed, 73 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 7b95477af..053cbdca8 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "express": "^4.17.1", "humanize-duration": "^3.27.0", "if-env": "^1.0.4", + "inline-attachment": "^2.0.3", "match-sorter": "^6.3.0", "parse-duration": "^1.0.0", "player.js": "^0.1.0", diff --git a/static/app-strings.json b/static/app-strings.json index 3f86fc7d7..b51ceb7db 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2229,5 +2229,6 @@ "Still Valid Until": "Still Valid Until", "Active channel": "Active channel", "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.": "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.", + "Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.", "--end--": "--end--" } diff --git a/ui/component/common/form-components/form-field.jsx b/ui/component/common/form-components/form-field.jsx index be6319ec6..912fc5f13 100644 --- a/ui/component/common/form-components/form-field.jsx +++ b/ui/component/common/form-components/form-field.jsx @@ -1,6 +1,9 @@ // @flow import 'easymde/dist/easymde.min.css'; +import 'inline-attachment/src/inline-attachment'; +import 'inline-attachment/src/codemirror-4.inline-attachment'; +import { IMG_CDN_PUBLISH_URL, JSON_RESPONSE_KEYS, UPLOAD_CONFIG } from 'constants/cdn_urls'; import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field'; import { openEditorMenu, stopContextMenu } from 'util/context-menu'; import { lazyImport } from 'util/lazyImport'; @@ -179,8 +182,8 @@ export class FormField extends React.PureComponent { // SimpleMDE max char check editor.codemirror.on('beforeChange', (instance, changes) => { if (textAreaMaxLength && changes.update) { - var str = changes.text.join('\n'); - var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from)); + let str = changes.text.join('\n'); + let delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from)); if (delta <= 0) return; @@ -227,6 +230,16 @@ export class FormField extends React.PureComponent { } } catch (e) {} // Do nothing (revert to original behavior) }); + + // Add ability to upload pasted/dragged image (https://github.com/sparksuite/simplemde-markdown-editor/issues/328#issuecomment-227075500) + window.inlineAttachment.editors.codemirror4.attach(editor.codemirror, { + uploadUrl: IMG_CDN_PUBLISH_URL, + uploadFieldName: UPLOAD_CONFIG.BLOB_KEY, + extraParams: { [UPLOAD_CONFIG.ACTION_KEY]: UPLOAD_CONFIG.ACTION_VAL }, + filenameTag: '{filename}', + urlText: '![image]({filename})', + jsonFieldName: JSON_RESPONSE_KEYS.UPLOADED_URL, + }); }; return ( @@ -250,6 +263,17 @@ export class FormField extends React.PureComponent { options={{ spellChecker: true, hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'], + status: [ + { + className: 'editor-statusbar__upload-hint', + defaultValue: (el) => { + el.innerHTML = __('Attach images by pasting or drag-and-drop.'); + }, + }, + 'lines', + 'words', + 'cursor', + ], previewRender(plainText) { const preview = ; return ReactDOMServer.renderToString(preview); diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index cb42b6a59..c3dbdf151 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -115,8 +115,12 @@ function FileDrop(props: Props) { // Handle file drop... React.useEffect(() => { - if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { - getTree(dropData) + const DROP_AREA_HEIGHT_PCT = 0.2; // @see: css[.file-drop -> height] + const windowHeight = window.innerHeight || document.documentElement?.clientHeight || 768; + const dropAreaBottom = windowHeight * DROP_AREA_HEIGHT_PCT; + + if (dropData && dropData.y <= dropAreaBottom && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { + getTree(dropData.dataTransfer) .then((entries) => { if (entries && entries.length) { setFiles(entries); diff --git a/ui/constants/cdn_urls.js b/ui/constants/cdn_urls.js index 771a900c2..f6725ef19 100644 --- a/ui/constants/cdn_urls.js +++ b/ui/constants/cdn_urls.js @@ -1,2 +1,20 @@ export const IMG_CDN_PUBLISH_URL = 'https://thumbs.odycdn.com/upload.php'; export const IMG_CDN_STATUS_URL = 'https://thumbs.odycdn.com/status.php'; + +export const JSON_RESPONSE_KEYS = Object.freeze({ + STATUS: 'type', + UPLOADED_URL: 'message', + TIMESTAMP: 'timestamp', + // --- Sample response --- + // { + // "type": "success", + // "message": "https://thumbs.odycdn.com/90c08452a2e2ebb347c8c95f749a84c5.png", + // "timestamp": 1648731440 + // } +}); + +export const UPLOAD_CONFIG = Object.freeze({ + BLOB_KEY: 'file-input', + ACTION_KEY: 'upload', + ACTION_VAL: 'Upload', +}); diff --git a/ui/effects/use-drag-drop.js b/ui/effects/use-drag-drop.js index 110593e3d..ec7640e9f 100644 --- a/ui/effects/use-drag-drop.js +++ b/ui/effects/use-drag-drop.js @@ -23,7 +23,7 @@ const DRAG_STATE = { }; // Returns simple detection for global drag-drop -export default function useFetched() { +export default function useDragDrop() { const [drag, setDrag] = React.useState(false); const [dropData, setDropData] = React.useState(null); @@ -32,7 +32,7 @@ export default function useFetched() { let draggingElement = false; // Handle file drop - const handleDropEvent = event => { + const handleDropEvent = (event) => { // Ignore non file types ( html elements / text ) if (!draggingElement) { event.stopPropagation(); @@ -41,7 +41,7 @@ export default function useFetched() { const files = event.dataTransfer.files; // Check for files if (files.length > 0) { - setDropData(event.dataTransfer); + setDropData(event); } } // Reset state ( hide drop zone ) @@ -50,12 +50,12 @@ export default function useFetched() { }; // Drag event for non files type ( html elements / text ) - const handleDragElementEvent = event => { + const handleDragElementEvent = (event) => { draggingElement = DRAG_STATE[event.type]; }; // Drag events - const handleDragEvent = event => { + const handleDragEvent = (event) => { event.stopPropagation(); event.preventDefault(); // Prevent multiple drop areas @@ -71,7 +71,7 @@ export default function useFetched() { }; // Register / Unregister listeners - const handleEventListeners = event => { + const handleEventListeners = (event) => { const action = `${event}EventListener`; // Handle drop event document[action]('drop', handleDropEvent); @@ -82,6 +82,7 @@ export default function useFetched() { document[action](DRAG_TYPES.END, handleDragElementEvent); document[action](DRAG_TYPES.START, handleDragElementEvent); }; + // On component mounted: // Register event listeners handleEventListeners(LISTENER.ADD); diff --git a/ui/scss/component/_file-drop.scss b/ui/scss/component/_file-drop.scss index fc0bc6d99..b897ef184 100644 --- a/ui/scss/component/_file-drop.scss +++ b/ui/scss/component/_file-drop.scss @@ -4,7 +4,7 @@ position: fixed; background: var(--color-background-overlay); opacity: 0; - height: 100%; + height: 20%; width: 100%; overflow: hidden; pointer-events: none; diff --git a/ui/scss/component/_markdown-editor.scss b/ui/scss/component/_markdown-editor.scss index 44ec8404d..f4e342f34 100644 --- a/ui/scss/component/_markdown-editor.scss +++ b/ui/scss/component/_markdown-editor.scss @@ -52,6 +52,14 @@ } } +.editor-statusbar { + .editor-statusbar__upload-hint { + display: block; + float: left; + margin-left: 0; + } +} + .form-field--SimpleMDE { margin-top: var(--spacing-l); diff --git a/yarn.lock b/yarn.lock index 94ba688f1..b7485a058 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9234,6 +9234,11 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== +inline-attachment@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inline-attachment/-/inline-attachment-2.0.3.tgz#5ee32374583fabd3b7206df2e20f251ba20c4306" + integrity sha1-XuMjdFg/q9O3IG3y4g8lG6IMQwY= + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"