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.
This commit is contained in:
Thomas Zarebczan 2022-03-31 15:55:00 -04:00 committed by Thomas Zarebczan
parent e765a74eb0
commit 8d4a05157d
9 changed files with 73 additions and 11 deletions

View file

@ -63,6 +63,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"humanize-duration": "^3.27.0", "humanize-duration": "^3.27.0",
"if-env": "^1.0.4", "if-env": "^1.0.4",
"inline-attachment": "^2.0.3",
"match-sorter": "^6.3.0", "match-sorter": "^6.3.0",
"parse-duration": "^1.0.0", "parse-duration": "^1.0.0",
"player.js": "^0.1.0", "player.js": "^0.1.0",

View file

@ -2229,5 +2229,6 @@
"Still Valid Until": "Still Valid Until", "Still Valid Until": "Still Valid Until",
"Active channel": "Active channel", "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.", "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--" "--end--": "--end--"
} }

View file

@ -1,6 +1,9 @@
// @flow // @flow
import 'easymde/dist/easymde.min.css'; 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 { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu'; import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import { lazyImport } from 'util/lazyImport'; import { lazyImport } from 'util/lazyImport';
@ -179,8 +182,8 @@ export class FormField extends React.PureComponent<Props, State> {
// SimpleMDE max char check // SimpleMDE max char check
editor.codemirror.on('beforeChange', (instance, changes) => { editor.codemirror.on('beforeChange', (instance, changes) => {
if (textAreaMaxLength && changes.update) { if (textAreaMaxLength && changes.update) {
var str = changes.text.join('\n'); let str = changes.text.join('\n');
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from)); let delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
if (delta <= 0) return; if (delta <= 0) return;
@ -227,6 +230,16 @@ export class FormField extends React.PureComponent<Props, State> {
} }
} catch (e) {} // Do nothing (revert to original behavior) } 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 ( return (
@ -250,6 +263,17 @@ export class FormField extends React.PureComponent<Props, State> {
options={{ options={{
spellChecker: true, spellChecker: true,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'], 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) { previewRender(plainText) {
const preview = <MarkdownPreview content={plainText} noDataStore />; const preview = <MarkdownPreview content={plainText} noDataStore />;
return ReactDOMServer.renderToString(preview); return ReactDOMServer.renderToString(preview);

View file

@ -115,8 +115,12 @@ function FileDrop(props: Props) {
// Handle file drop... // Handle file drop...
React.useEffect(() => { React.useEffect(() => {
if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { const DROP_AREA_HEIGHT_PCT = 0.2; // @see: css[.file-drop -> height]
getTree(dropData) 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) => { .then((entries) => {
if (entries && entries.length) { if (entries && entries.length) {
setFiles(entries); setFiles(entries);

View file

@ -1,2 +1,20 @@
export const IMG_CDN_PUBLISH_URL = 'https://thumbs.odycdn.com/upload.php'; 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 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',
});

View file

@ -23,7 +23,7 @@ const DRAG_STATE = {
}; };
// Returns simple detection for global drag-drop // Returns simple detection for global drag-drop
export default function useFetched() { export default function useDragDrop() {
const [drag, setDrag] = React.useState(false); const [drag, setDrag] = React.useState(false);
const [dropData, setDropData] = React.useState(null); const [dropData, setDropData] = React.useState(null);
@ -32,7 +32,7 @@ export default function useFetched() {
let draggingElement = false; let draggingElement = false;
// Handle file drop // Handle file drop
const handleDropEvent = event => { const handleDropEvent = (event) => {
// Ignore non file types ( html elements / text ) // Ignore non file types ( html elements / text )
if (!draggingElement) { if (!draggingElement) {
event.stopPropagation(); event.stopPropagation();
@ -41,7 +41,7 @@ export default function useFetched() {
const files = event.dataTransfer.files; const files = event.dataTransfer.files;
// Check for files // Check for files
if (files.length > 0) { if (files.length > 0) {
setDropData(event.dataTransfer); setDropData(event);
} }
} }
// Reset state ( hide drop zone ) // Reset state ( hide drop zone )
@ -50,12 +50,12 @@ export default function useFetched() {
}; };
// Drag event for non files type ( html elements / text ) // Drag event for non files type ( html elements / text )
const handleDragElementEvent = event => { const handleDragElementEvent = (event) => {
draggingElement = DRAG_STATE[event.type]; draggingElement = DRAG_STATE[event.type];
}; };
// Drag events // Drag events
const handleDragEvent = event => { const handleDragEvent = (event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
// Prevent multiple drop areas // Prevent multiple drop areas
@ -71,7 +71,7 @@ export default function useFetched() {
}; };
// Register / Unregister listeners // Register / Unregister listeners
const handleEventListeners = event => { const handleEventListeners = (event) => {
const action = `${event}EventListener`; const action = `${event}EventListener`;
// Handle drop event // Handle drop event
document[action]('drop', handleDropEvent); document[action]('drop', handleDropEvent);
@ -82,6 +82,7 @@ export default function useFetched() {
document[action](DRAG_TYPES.END, handleDragElementEvent); document[action](DRAG_TYPES.END, handleDragElementEvent);
document[action](DRAG_TYPES.START, handleDragElementEvent); document[action](DRAG_TYPES.START, handleDragElementEvent);
}; };
// On component mounted: // On component mounted:
// Register event listeners // Register event listeners
handleEventListeners(LISTENER.ADD); handleEventListeners(LISTENER.ADD);

View file

@ -4,7 +4,7 @@
position: fixed; position: fixed;
background: var(--color-background-overlay); background: var(--color-background-overlay);
opacity: 0; opacity: 0;
height: 100%; height: 20%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;

View file

@ -52,6 +52,14 @@
} }
} }
.editor-statusbar {
.editor-statusbar__upload-hint {
display: block;
float: left;
margin-left: 0;
}
}
.form-field--SimpleMDE { .form-field--SimpleMDE {
margin-top: var(--spacing-l); margin-top: var(--spacing-l);

View file

@ -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" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== 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: inline-style-parser@0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"