UNPKG

@limetech/lime-elements

Version:
211 lines (210 loc) • 7.28 kB
import { Plugin, PluginKey } from 'prosemirror-state'; import { createFileInfo } from '../../../../../util/files'; import { Node, Slice, Fragment } from 'prosemirror-model'; export const pluginKey = new PluginKey('imageInserterPlugin'); export const createImageInserterPlugin = (imagePastedCallback) => { return new Plugin({ key: pluginKey, props: { handlePaste: (view, event, slice) => { return processPasteEvent(view, event, slice); }, handleDOMEvents: { imagePasted: (_, event) => { imagePastedCallback(event.detail); }, }, }, }); }; export const imageInserterFactory = (view, base64Data, fileInfo) => { return { fileInfo: fileInfo, insertThumbnail: createThumbnailInserter(view, base64Data, fileInfo), insertImage: createImageInserter(view, fileInfo), insertFailedThumbnail: createFailedThumbnailInserter(view, fileInfo), }; }; const createThumbnailInserter = (view, base64Data, fileInfo) => () => { const { state, dispatch } = view; const { schema } = state; const imageNodeAttrs = createImageNodeAttrs(base64Data, fileInfo, 'loading'); const placeholderNode = schema.nodes.image.create(imageNodeAttrs); const transaction = state.tr.replaceSelectionWith(placeholderNode); dispatch(transaction); }; const createImageInserter = (view, fileInfo) => (src) => { const { state, dispatch } = view; const { schema } = state; const tr = state.tr; state.doc.descendants((node, pos) => { if (node.attrs.fileInfoId === fileInfo.id) { const imageNodeAttrs = createImageNodeAttrs(src !== null && src !== void 0 ? src : node.attrs.src, fileInfo, 'success'); const imageNode = schema.nodes.image.create(imageNodeAttrs); tr.replaceWith(pos, pos + node.nodeSize, imageNode); return false; } }); dispatch(tr); }; const createFailedThumbnailInserter = (view, fileInfo) => () => { const { state, dispatch } = view; const { schema } = state; const tr = state.tr; state.doc.descendants((node, pos) => { if (node.attrs.fileInfoId === fileInfo.id) { const imageNodeAttrs = createImageNodeAttrs(node.attrs.src, fileInfo, 'failed'); const errorPlaceholderNode = schema.nodes.image.create(imageNodeAttrs); tr.replaceWith(pos, pos + node.nodeSize, errorPlaceholderNode); return false; } }); dispatch(tr); }; function createImageNodeAttrs(src, fileInfo, state) { return { src: src, alt: fileInfo.filename, fileInfoId: fileInfo.id, state: state, }; } /** * Check if a given ProseMirror node or fragment contains any image nodes. * @param node - The ProseMirror node or fragment to check. * @returns A boolean indicating whether the node contains any image nodes. */ const isImageNode = (node) => { if (node instanceof Node) { if (node.type.name === 'image') { return true; } let found = false; // eslint-disable-next-line unicorn/no-array-for-each node.content.forEach((child) => { if (isImageNode(child)) { found = true; } }); return found; } else if (node instanceof Fragment) { let found = false; // eslint-disable-next-line unicorn/no-array-for-each node.forEach((child) => { if (isImageNode(child)) { found = true; } }); return found; } return false; }; /** * Filter out image nodes from a ProseMirror fragment. * @param fragment - The ProseMirror fragment to filter. * @returns A new fragment with image nodes removed. */ const filterImageNodes = (fragment) => { const filteredChildren = []; // eslint-disable-next-line unicorn/no-array-for-each fragment.forEach((child) => { if (!isImageNode(child)) { if (child.content.size > 0) { const filteredContent = filterImageNodes(child.content); const newNode = child.copy(filteredContent); filteredChildren.push(newNode); } else { filteredChildren.push(child); } } }); return Fragment.fromArray(filteredChildren); }; /** * Process a paste event and trigger an imagePasted event if an image file is pasted. * If an HTML image element is pasted, this image is filtered out from the slice content. * * @param view - The ProseMirror editor view. * @param event - The paste event. * @param slice * @returns A boolean; True if an image file was pasted to prevent default paste behavior, otherwise false. */ const processPasteEvent = (view, event, slice) => { const clipboardData = event.clipboardData; if (!clipboardData) { return false; } const isImageFilePasted = handlePastedImages(view, clipboardData); const filteredSlice = new Slice(filterImageNodes(slice.content), slice.openStart, slice.openEnd); if (filteredSlice.content.childCount < slice.content.childCount) { const { state, dispatch } = view; const tr = state.tr.replaceSelection(filteredSlice); dispatch(tr); return true; } return isImageFilePasted; }; /** * Processes any image files found in the clipboard data and dispatches an imagePasted event. * * @param view - The ProseMirror editor view * @param clipboardData - The clipboard data transfer object containing potential image files * @returns True if at least one valid image file was found and processed, false otherwise */ function handlePastedImages(view, clipboardData) { let isImageFilePasted = false; const files = [...(clipboardData.files || [])]; for (const file of files) { if (isImageFile(file, clipboardData)) { isImageFilePasted = true; const reader = new FileReader(); reader.onloadend = () => { view.dom.dispatchEvent(new CustomEvent('imagePasted', { detail: imageInserterFactory(view, reader.result, createFileInfo(file)), })); }; reader.readAsDataURL(file); } } return isImageFilePasted; } /** * Determines if a file is an image that should be processed by the image handler. * * This function checks both the file's MIME type and the clipboard HTML content. * It filters out HTML content from Excel and HTML tables, as they are not relevant for image processing. * * @param file - The file object to check * @param clipboardData - The full clipboard data transfer object to examine for context * @returns True if the file is an image that should be processed, false otherwise */ function isImageFile(file, clipboardData) { var _a, _b; if (!isContentTypeImage(file)) { return false; } const html = (_b = (_a = clipboardData === null || clipboardData === void 0 ? void 0 : clipboardData.getData('text/html')) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : ''; return !isHtmlFromExcel(html) && !isHtmlTable(html); } function isContentTypeImage(file) { if (!(file === null || file === void 0 ? void 0 : file.type)) { return false; } return file.type.startsWith('image/'); } function isHtmlFromExcel(html) { if (!html) { return false; } return (html.includes('name=generator content="microsoft excel"') || html.includes('xmlns:x="urn:schemas-microsoft-com:office:excel"')); } function isHtmlTable(html) { if (!html) { return false; } return html.includes('<table'); } //# sourceMappingURL=inserter.js.map