@limetech/lime-elements
Version:
211 lines (210 loc) • 7.28 kB
JavaScript
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