Add paste image (#10212)

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
Co-authored-by: ludwiczakpawel <ludwiczakpawel@gmail.com>
Co-authored-by: Nick Taylor <nick@dev.to>
Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
Andrew Bone 2021-02-04 15:58:56 +00:00 committed by GitHub
parent 39a63c17f2
commit 9ef1534d83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 1 deletions

View file

@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
import Textarea from 'preact-textarea-autosize';
import { useEffect, useRef } from 'preact/hooks';
import { Toolbar } from './Toolbar';
import { handleImagePasted } from './pasteImageHelpers';
import {
handleImageDrop,
handleImageFailure,
onDragOver,
onDragExit,
} from './dragAndDropHelpers';
import { usePasteImage } from '@utilities/pasteImage';
import { useDragAndDrop } from '@utilities/dragAndDrop';
function handleImageSuccess(textAreaRef) {
@ -17,7 +19,9 @@ function handleImageSuccess(textAreaRef) {
// textarea ref.
const editableBodyElement = textAreaRef.current.base;
const { links, image } = response;
const altText = image[0].name.replace(/\.[^.]+$/, '');
const altText = image[0]
? image[0].name.replace(/\.[^.]+$/, '')
: 'alt text';
const markdownImageLink = `![${altText}](${links[0]})\n`;
const { selectionStart, selectionEnd, value } = editableBodyElement;
const before = value.substring(0, selectionStart);
@ -50,9 +54,17 @@ export const EditorBody = ({
onDragExit,
});
const setPasteElement = usePasteImage({
onPaste: handleImagePasted(
handleImageSuccess(textAreaRef),
handleImageFailure,
),
});
useEffect(() => {
if (textAreaRef.current) {
setElement(textAreaRef.current.base);
setPasteElement(textAreaRef.current.base);
}
});

View file

@ -0,0 +1,29 @@
import { matchesDataTransferType } from '../pasteImageHelpers';
describe('pasteImageHelpers', () => {
describe('matchesDataTransferType', () => {
it('returns false if no types are provided', () => {
expect(matchesDataTransferType([])).toBe(false);
});
it('returns true if at least one type matches Files', () => {
const result = matchesDataTransferType(['example', 'Files', 'other']);
expect(result).toBe(true);
});
it('returns false if no types match type Files', () => {
const result = matchesDataTransferType(['example', 'other']);
expect(result).toBe(false);
});
it('returns true if at least one type matches a custom type provided', () => {
const result = matchesDataTransferType(['example', 'other'], 'other');
expect(result).toBe(true);
});
it('returns false if no types match a custom type provided', () => {
const result = matchesDataTransferType(['example', 'Files'], 'other');
expect(result).toBe(false);
});
});
});

View file

@ -0,0 +1,55 @@
import { addSnackbarItem } from '../../Snackbar';
import { processImageUpload } from '../actions';
/**
* Determines if at least one type of drag and drop datum type matches the data transfer type to match.
*
* @param {string[]} types An array of data transfer types.
* @param {string} dataTransferType The data transfer type to match.
*/
export function matchesDataTransferType(
types = [],
dataTransferType = 'Files',
) {
return types.some((type) => type === dataTransferType);
}
/**
* Handler for when image is pasted.
*
* @param {function} handleImageSuccess Callback for when image upload succeeds
* @param {function} handleImageFailure Callback for when image upload fails
*/
export function handleImagePasted(handleImageSuccess, handleImageFailure) {
return function (event) {
if (!event.clipboardData || !event.clipboardData.items) return;
if (!matchesDataTransferType(event.clipboardData.types)) return;
event.preventDefault();
const { files } = event.clipboardData;
if (files.length > 1) {
addSnackbarItem({
message: 'Only one image can be pasted at a time.',
addCloseButton: true,
});
return;
}
processImageUpload(files, handleImageSuccess, handleImageFailure);
};
}
/**
* Handler for when image upload fails.
*
* @param {Error} error an error
* @param {string} error.message an error message
*/
export function handleImageFailure({ message }) {
addSnackbarItem({
message,
addCloseButton: true,
});
}

View file

@ -0,0 +1,25 @@
import { renderHook, act } from '@testing-library/preact-hooks';
import { fireEvent } from '@testing-library/preact';
import { usePasteImage } from '../pasteImage';
describe('usePasteImage', () => {
it('listens for paste events on the set element', () => {
const handlePasteImage = jest.fn();
document.body.innerHTML = `
<div id="paste-area"></div>
`;
const element = document.getElementById('paste-area');
const {
result: { current: setElement },
} = renderHook(() => usePasteImage({ onPaste: handlePasteImage }));
act(() => {
setElement(element);
});
fireEvent.paste(element, { clipboardData: {} });
expect(handlePasteImage).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,39 @@
import { useEffect, useState } from 'preact/hooks';
/**
* A custom Preact hook used to attach a catch all for pasting images to a DOM element.
* @example
* function SomeComponent(props) {
* const { setElement } = usePasteImage({
* onPaste: somePasteHandler
* });
*
* const someDomRef = useRef(null);
*
* useEffect(() => {
* if (someDomRef.current) {
* setElement(someDomRef.current);
* }
* });
*
* return <textarea ref={someDomRef}>I'm a text area</textarea>;
* };
*
* @param {object} props
* @param {Function} props.onPaste The handler that runs when the paste event is fired.
*/
export function usePasteImage({ onPaste }) {
const [element, setElement] = useState(null);
useEffect(() => {
if (!element) return;
element.addEventListener('paste', onPaste);
return () => {
element.removeEventListener('paste', onPaste);
};
}, [element, onPaste]);
return setElement;
}