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:
parent
e765a74eb0
commit
8d4a05157d
9 changed files with 73 additions and 11 deletions
|
@ -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",
|
||||||
|
|
|
@ -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--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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',
|
||||||
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue