UNPKG

@atlaskit/editor-plugin-image-upload

Version:

Image upload plugin for @atlaskit/editor-core

161 lines (155 loc) 5.69 kB
import { isPastedFile } from '@atlaskit/editor-common/paste'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { isClipboardEvent } from '../ui/hooks/clipboard'; import { isDragEvent, isDroppedFile } from '../ui/hooks/drag-drop'; import { canInsertMedia, isMediaSelected } from '../ui/hooks/utils'; import { insertExternalImage, startImageUpload } from './commands'; import { stateKey } from './plugin-key'; /** * Microsoft Office includes a screenshot image when copying text content. * * This function determines whether or not we can ignore the image if it * came from MS Office. We do this by checking for * * - plain text * - HTML text which includes the MS Office namespace * - the number of files and the file name/type * * It is easy to manually verify this using by using Office on Mac * (or Excel if on Windows) and pasting into * https://evercoder.github.io/clipboard-inspector/ * * Note: image content in Word is stored in the `text/html` portion * of the clipboard, not under `files` attachment like the screenshot. * * @returns boolean True if the paste event contains a screenshot from MS Office */ const hasScreenshotImageFromMSOffice = ev => { const { clipboardData } = ev; if (!clipboardData || clipboardData.files.length !== 1) { return false; } const textPlain = !!clipboardData.getData('text/plain'); const textHtml = clipboardData.getData('text/html'); const isOfficeXMLNamespace = textHtml.includes('urn:schemas-microsoft-com:office:office'); const file = clipboardData.files[0]; const isImagePNG = file.type === 'image/png' && file.name === 'image.png'; return isImagePNG && textPlain && isOfficeXMLNamespace; }; const createReferenceEventFromEvent = event => { var _event$dataTransfer, _event$clipboardData; if (!isDragEvent(event) && !isClipboardEvent(event)) { return null; } // Get files list and early exit if files is undefined const files = isDragEvent(event) ? (_event$dataTransfer = event.dataTransfer) === null || _event$dataTransfer === void 0 ? void 0 : _event$dataTransfer.files : (_event$clipboardData = event.clipboardData) === null || _event$clipboardData === void 0 ? void 0 : _event$clipboardData.files; if (!files) { return null; } // Convert filelist into an array const filesArray = Array.from(files); // Creating a new DataTransfer object should remove any mutation that could be possible from the original event const dataTransfer = filesArray.reduce((acc, value) => { acc.items.add(value); return acc; }, new DataTransfer()); return { type: isDragEvent(event) ? 'drop' : 'paste', ...(isDragEvent(event) && { dataTransfer }), ...(isClipboardEvent(event) && { clipboardData: dataTransfer }) }; }; const createDOMHandler = (pred, _eventName, uploadHandlerReference) => (view, event) => { if (!pred(event)) { return false; } const shouldUpload = !hasScreenshotImageFromMSOffice(event); const referenceEvent = createReferenceEventFromEvent(event); if (shouldUpload && referenceEvent) { event.preventDefault(); event.stopPropagation(); // Insert external image into document if (uploadHandlerReference.current) { uploadHandlerReference.current(referenceEvent, options => { insertExternalImage(options)(view.state, view.dispatch); }); } // Start image upload startImageUpload(referenceEvent)(view.state, view.dispatch); } return shouldUpload; }; const getNewActiveUpload = (tr, pluginState) => { const meta = tr.getMeta(stateKey); if (meta && meta.name === 'START_UPLOAD') { return { event: meta.event }; } return pluginState.activeUpload; }; export const createPlugin = uploadHandlerReference => ({ dispatch, providerFactory }) => { return new SafePlugin({ state: { init(_config, state) { return { active: false, enabled: canInsertMedia(state), hidden: !state.schema.nodes.media || !state.schema.nodes.mediaSingle }; }, apply(tr, pluginState, _oldState, newState) { const newActive = isMediaSelected(newState); const newEnabled = canInsertMedia(newState); const newActiveUpload = getNewActiveUpload(tr, pluginState); if (newActive !== pluginState.active || newEnabled !== pluginState.enabled || newActiveUpload !== pluginState.activeUpload) { const newPluginState = { ...pluginState, active: newActive, enabled: newEnabled, activeUpload: newActiveUpload }; dispatch(stateKey, newPluginState); return newPluginState; } return pluginState; } }, key: stateKey, view: () => { const handleProvider = async (name, provider) => { if (name !== 'imageUploadProvider' || !provider) { return; } try { const imageUploadProvider = await provider; uploadHandlerReference.current = imageUploadProvider; } catch (e) { uploadHandlerReference.current = null; } }; providerFactory.subscribe('imageUploadProvider', handleProvider); return { destroy() { uploadHandlerReference.current = null; providerFactory.unsubscribe('imageUploadProvider', handleProvider); } }; }, props: { handleDOMEvents: { drop: createDOMHandler(isDroppedFile, 'atlassian.editor.image.drop', uploadHandlerReference), paste: createDOMHandler(event => isPastedFile(event), 'atlassian.editor.image.paste', uploadHandlerReference) } } }); };