diff --git a/.ackrc b/.ackrc new file mode 100644 index 000000000..834fca5d3 --- /dev/null +++ b/.ackrc @@ -0,0 +1,2 @@ +--ignore-dir=packages/superdoc/dist +--ignore-dir=packages/super-editor/dist/ diff --git a/packages/super-editor/src/components/toolbar/super-toolbar.js b/packages/super-editor/src/components/toolbar/super-toolbar.js index 71b34b74c..02b17ba06 100644 --- a/packages/super-editor/src/components/toolbar/super-toolbar.js +++ b/packages/super-editor/src/components/toolbar/super-toolbar.js @@ -5,7 +5,12 @@ import { makeDefaultItems } from './defaultItems'; import { getActiveFormatting } from '@core/helpers/getActiveFormatting.js'; import { vClickOutside } from '@harbour-enterprises/common'; import Toolbar from './Toolbar.vue'; -import { startImageUpload, getFileOpener } from '../../extensions/image/imageHelpers/index.js'; +import { + checkAndProcessImage, + replaceSelectionWithImagePlaceholder, + uploadAndInsertImage, + getFileOpener, +} from '../../extensions/image/imageHelpers/index.js'; import { findParentNode } from '@helpers/index.js'; import { toolbarIcons } from './toolbarIcons.js'; import { toolbarTexts } from './toolbarTexts.js'; @@ -366,10 +371,29 @@ export class SuperToolbar extends EventEmitter { return; } - startImageUpload({ + const { size, file } = await checkAndProcessImage({ + file: result.file, + getMaxContentSize: () => this.activeEditor.getMaxContentSize(), + }); + + if (!file) { + return; + } + + const id = {}; + + replaceSelectionWithImagePlaceholder({ + view: this.activeEditor.view, + editorOptions: this.activeEditor.options, + id, + }); + + await uploadAndInsertImage({ editor: this.activeEditor, view: this.activeEditor.view, - file: result.file, + file, + size, + id, }); }, diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index c3f6e3d84..f7acb7a64 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -1,5 +1,5 @@ import { Attribute, Node } from '@core/index.js'; -import { ImagePlaceholderPlugin } from './imageHelpers/imagePlaceholderPlugin.js'; +import { ImageRegistrationPlugin } from './imageHelpers/imageRegistrationPlugin.js'; import { ImagePositionPlugin } from './imageHelpers/imagePositionPlugin.js'; export const Image = Node.create({ @@ -147,6 +147,6 @@ export const Image = Node.create({ }, addPmPlugins() { - return [ImagePlaceholderPlugin(), ImagePositionPlugin({ editor: this.editor })]; + return [ImageRegistrationPlugin({ editor: this.editor }), ImagePositionPlugin({ editor: this.editor })]; }, }); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/handleBase64.js b/packages/super-editor/src/extensions/image/imageHelpers/handleBase64.js new file mode 100644 index 000000000..172595e2e --- /dev/null +++ b/packages/super-editor/src/extensions/image/imageHelpers/handleBase64.js @@ -0,0 +1,34 @@ +const simpleHash = (str) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(); +}; + +export const base64ToFile = (base64String) => { + const arr = base64String.split(','); + const mimeMatch = arr[0].match(/:(.*?);/); + const mimeType = mimeMatch ? mimeMatch[1] : ''; + const data = arr[1]; + + // Decode the base64 string + const binaryString = atob(data); + + // Generate filename using a hash of the binary data + const hash = simpleHash(binaryString); + const extension = mimeType.split('/')[1] || 'bin'; // Simple way to get extension + const filename = `image-${hash}.${extension}`; + + // Create a typed array from the binary string + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Create a Blob and then a File + const blob = new Blob([bytes], { type: mimeType }); + return new File([blob], filename, { type: mimeType }); +}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/handleUrl.js b/packages/super-editor/src/extensions/image/imageHelpers/handleUrl.js new file mode 100644 index 000000000..6373876d8 --- /dev/null +++ b/packages/super-editor/src/extensions/image/imageHelpers/handleUrl.js @@ -0,0 +1,106 @@ +/** + * Handles URL to File conversion with comprehensive CORS error handling + */ + +/** + * Converts a URL to a File object with proper CORS error handling + * @param {string} url - The image URL to fetch + * @param {string} [filename] - Optional filename for the resulting file + * @param {string} [mimeType] - Optional MIME type for the resulting file + * @returns {Promise} File object or null if CORS prevents access + */ +export const urlToFile = async (url, filename, mimeType) => { + try { + // Try to fetch the image with credentials mode set to 'omit' to avoid CORS preflight + const response = await fetch(url, { + mode: 'cors', + credentials: 'omit', + headers: { + // Add common headers that might help with CORS + Accept: 'image/*,*/*;q=0.8', + }, + }); + + if (!response.ok) { + console.warn(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`); + return null; + } + + const blob = await response.blob(); + + // Extract filename from URL if not provided + const finalFilename = filename || extractFilenameFromUrl(url); + + // Determine MIME type from response if not provided + const finalMimeType = mimeType || response.headers.get('content-type') || blob.type || 'image/jpeg'; + + return new File([blob], finalFilename, { type: finalMimeType }); + } catch (error) { + if (isCorsError(error)) { + console.warn(`CORS policy prevents accessing image from ${url}:`, error.message); + return null; + } + + console.error(`Error fetching image from ${url}:`, error); + return null; + } +}; + +/** + * Checks if an error is likely a CORS-related error + * @param {Error} error - The error to check + * @returns {boolean} True if the error appears to be CORS-related + */ +const isCorsError = (error) => { + const errorMessage = error.message.toLowerCase(); + const errorName = error.name.toLowerCase(); + + return ( + errorName.includes('cors') || + errorMessage.includes('cors') || + errorMessage.includes('cross-origin') || + errorMessage.includes('access-control') || + errorMessage.includes('network error') || // Often indicates CORS in browsers + errorMessage.includes('failed to fetch') // Common CORS error message + ); +}; + +/** + * Extracts a filename from a URL + * @param {string} url - The URL to extract filename from + * @returns {string} The extracted filename + */ +const extractFilenameFromUrl = (url) => { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop(); + + // If no extension, add a default one + if (filename && !filename.includes('.')) { + return `${filename}.jpg`; + } + + return filename || 'image.jpg'; + } catch { + return 'image.jpg'; + } +}; + +/** + * Validates if a URL can be accessed without CORS issues + * @param {string} url - The URL to validate + * @returns {Promise} True if the URL is accessible without CORS issues + */ +export const validateUrlAccessibility = async (url) => { + try { + const response = await fetch(url, { + method: 'HEAD', + mode: 'cors', + credentials: 'omit', + }); + return response.ok; + } catch (_error) { + return false; + } +}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imagePlaceholderPlugin.js b/packages/super-editor/src/extensions/image/imageHelpers/imagePlaceholderPlugin.js deleted file mode 100644 index 11e1fdd66..000000000 --- a/packages/super-editor/src/extensions/image/imageHelpers/imagePlaceholderPlugin.js +++ /dev/null @@ -1,57 +0,0 @@ -import { Plugin, PluginKey } from 'prosemirror-state'; -import { Decoration, DecorationSet } from 'prosemirror-view'; - -export const ImagePlaceholderPluginKey = new PluginKey('ImagePlaceholder'); - -export const ImagePlaceholderPlugin = () => { - return new Plugin({ - key: ImagePlaceholderPluginKey, - - state: { - init() { - return DecorationSet.empty; - }, - - apply(tr, set) { - // For reference. - // let diffStart = tr.doc.content.findDiffStart(oldState.doc.content); - // let diffEnd = oldState.doc.content.findDiffEnd(tr.doc.content); - // let map = diffEnd && diffStart - // ? new StepMap([diffStart, diffEnd.a - diffStart, diffEnd.b - diffStart]) - // : new StepMap([0, 0, 0]); - // let pmMapping = new Mapping([map]); - // let set = value.map(pmMapping, tr.doc); - /// - - // Adjust decoration positions to changes made by the transaction - set = set.map(tr.mapping, tr.doc); - - // See if the transaction adds or removes any placeholders - let action = tr.getMeta(ImagePlaceholderPluginKey); - - if (action?.type === 'add') { - let widget = document.createElement('placeholder'); - let deco = Decoration.widget(action.pos, widget, { - id: action.id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.type === 'remove') { - set = set.remove(set.find(null, null, (spec) => spec.id == action.id)); - } - return set; - }, - }, - - props: { - decorations(state) { - return this.getState(state); - }, - }, - }); -}; - -export const findPlaceholder = (state, id) => { - let decos = ImagePlaceholderPluginKey.getState(state); - let found = decos?.find(null, null, (spec) => spec.id === id); - return found?.length ? found[0].from : null; -}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js new file mode 100644 index 000000000..82223ddad --- /dev/null +++ b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js @@ -0,0 +1,210 @@ +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; +import { ReplaceStep, ReplaceAroundStep } from 'prosemirror-transform'; +import { base64ToFile } from './handleBase64'; +import { urlToFile, validateUrlAccessibility } from './handleUrl'; +import { checkAndProcessImage, uploadAndInsertImage } from './startImageUpload'; + +const key = new PluginKey('ImageRegistration'); + +export const ImageRegistrationPlugin = ({ editor }) => { + const { view } = editor; + return new Plugin({ + key, + state: { + init() { + return { set: DecorationSet.empty }; + }, + + apply(tr, { set }) { + // For reference. + // let diffStart = tr.doc.content.findDiffStart(oldState.doc.content); + // let diffEnd = oldState.doc.content.findDiffEnd(tr.doc.content); + // let map = diffEnd && diffStart + // ? new StepMap([diffStart, diffEnd.a - diffStart, diffEnd.b - diffStart]) + // : new StepMap([0, 0, 0]); + // let pmMapping = new Mapping([map]); + // let set = value.map(pmMapping, tr.doc); + /// + const meta = tr.getMeta(key); + // If meta is set, it overrides the default behavior. + if (meta) { + set = meta.set; + return { set }; + } + // Adjust decoration positions to changes made by the transaction + set = set.map(tr.mapping, tr.doc); + + return { set }; + }, + }, + appendTransaction: (trs, _oldState, state) => { + let foundImages = []; + trs.forEach((tr) => { + if (tr.docChanged) { + // Check if there are any images in the incoming transaction. If so, we need to register them. + tr.steps.forEach((step, index) => { + const stepMap = step.getMap(); + foundImages = foundImages.map(({ node, pos, id }) => { + const mappedPos = stepMap.map(pos, -1); + return { node, pos: mappedPos, id }; + }); + if (step instanceof ReplaceStep || step instanceof ReplaceAroundStep) { + // Check for new images. + (tr.docs[index + 1] || tr.doc).nodesBetween( + stepMap.map(step.from, -1), + stepMap.map(step.to, 1), + (node, pos) => { + if (node.type.name === 'image' && !node.attrs.src.startsWith('word/media')) { + // Node contains an image that is not yet registered. + const id = {}; + foundImages.push({ node, pos, id }); + } else { + return true; + } + }, + ); + } + }); + } + }); + + if (!foundImages || foundImages.length === 0) { + return null; + } + + // Register the images. (async process). + registerImages(foundImages, editor, view); + + // Remove all the images that were found. These will eventually be replaced by the updated images. + const tr = state.tr; + + // We need to delete the image nodes and replace them with decorations. This will change their positions. + + // Get the current decoration set + let { set } = key.getState(state); + + // Add decorations for the images first at their current positions + foundImages + .toSorted((a, b) => a.pos - b.pos) + .forEach(({ pos, id }) => { + let deco = Decoration.widget(pos, () => document.createElement('placeholder'), { + side: -1, + id, + }); + set = set.add(tr.doc, [deco]); + }); + + // Then delete the image nodes (highest position first to avoid position shifting issues) + foundImages + .toSorted((a, b) => b.pos - a.pos) + .forEach(({ node, pos }) => { + tr.delete(pos, pos + node.nodeSize); + }); + // Map the decoration set through the transaction to adjust positions + set = set.map(tr.mapping, tr.doc); + + // Set the updated decoration set in the transaction metadata + tr.setMeta(key, { set }); + + return tr; + }, + props: { + decorations(state) { + let { set } = key.getState(state); + return set; + }, + }, + }); +}; + +export const findPlaceholder = (state, id) => { + let { set } = key.getState(state); + let found = set?.find(null, null, (spec) => spec.id === id); + return found?.length ? found[0].from : null; +}; + +export const removeImagePlaceholder = (state, tr, id) => { + let { set } = key.getState(state); + set = set.map(tr.mapping, tr.doc); + set = set.remove(set.find(null, null, (spec) => spec.id == id)); + return tr.setMeta(key, { set, type: 'remove' }); +}; + +export const addImagePlaceholder = (state, tr, id, pos) => { + let { set } = key.getState(state); + set = set.map(tr.mapping, tr.doc); + let deco = Decoration.widget(pos, () => document.createElement('placeholder'), { + id, + }); + set = set.add(tr.doc, [deco]); + return tr.setMeta(key, { set, type: 'add' }); +}; + +export const getImageRegistrationMetaType = (tr) => { + const meta = tr.getMeta(key); + if (meta && meta.type) { + return meta.type; + } + return null; +}; + +const registerImages = async (foundImages, editor, view) => { + foundImages.forEach(async (image) => { + const src = image.node.attrs.src; + const id = image.id; + let file = null; + + if (src.startsWith('http')) { + // First check if the URL is accessible without CORS issues + const isAccessible = await validateUrlAccessibility(src); + + if (isAccessible) { + // Download image first, create fileobject, then proceed with registration. + file = await urlToFile(src); + } else { + console.warn(`Image URL ${src} is not accessible due to CORS or other restrictions. Using original URL.`); + // Fallback: Remove the placeholder. + const tr = view.state.tr; + removeImagePlaceholder(view.state, tr, id); + view.dispatch(tr); + return; + } + } else if (src.startsWith('data:')) { + file = base64ToFile(src); + } else { + console.error(`Unsupported image source: ${src}`); + } + + if (!file) { + // If file conversion failed, remove the placeholder to avoid stuck UI + const tr = view.state.tr; + removeImagePlaceholder(view.state, tr, id); + view.dispatch(tr); + return; + } + + try { + const process = await checkAndProcessImage({ + getMaxContentSize: () => editor.getMaxContentSize(), + file, + }); + + if (!process.file) { + // Processing failed, remove placeholder + const tr = view.state.tr; + removeImagePlaceholder(view.state, tr, id); + view.dispatch(tr); + return; + } + + await uploadAndInsertImage({ editor, view, file: process.file, size: process.size, id }); + } catch (error) { + console.error(`Error processing image from ${src}:`, error); + // Ensure placeholder is removed even on error + const tr = view.state.tr; + removeImagePlaceholder(view.state, tr, id); + view.dispatch(tr); + } + }); +}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/index.js b/packages/super-editor/src/extensions/image/imageHelpers/index.js index 61bf20062..3d2da5344 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/index.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/index.js @@ -1,6 +1,6 @@ export * from './getFileOpener.js'; export * from './startImageUpload.js'; export * from './handleImageUpload.js'; -export * from './imagePlaceholderPlugin.js'; +export * from './imageRegistrationPlugin.js'; export * from './processUploadedImage.js'; export * from './imagePositionPlugin.js'; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js b/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js index 1b482f8e7..dc4037056 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js @@ -3,13 +3,17 @@ * @param {string | File} fileData Base 64 string or File object. * @returns {Promise} Resolves with a base 64 string or File object. */ -export const processUploadedImage = (fileData, editor) => { +export const processUploadedImage = (fileData, getMaxContentSize) => { return new Promise((resolve, reject) => { const img = new window.Image(); img.onload = () => { const canvas = document.createElement('canvas'); - const { width: logicalWidth, height: logicalHeight } = getAllowedImageDimensions(img.width, img.height, editor); + const { width: logicalWidth, height: logicalHeight } = getAllowedImageDimensions( + img.width, + img.height, + getMaxContentSize, + ); // Set canvas to original image size first canvas.width = img.width; @@ -64,8 +68,8 @@ export const processUploadedImage = (fileData, editor) => { }); }; -export const getAllowedImageDimensions = (width, height, editor) => { - const { width: maxWidth, height: maxHeight } = editor.getMaxContentSize(); +export const getAllowedImageDimensions = (width, height, getMaxContentSize) => { + const { width: maxWidth, height: maxHeight } = getMaxContentSize(); if (!maxWidth || !maxHeight) return { width, height }; let adjustedWidth = width; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.js b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.js index 74f38dd30..51b22ed94 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.js @@ -1,115 +1,107 @@ -import { ImagePlaceholderPluginKey, findPlaceholder } from './imagePlaceholderPlugin.js'; +import { findPlaceholder, removeImagePlaceholder, addImagePlaceholder } from './imageRegistrationPlugin.js'; import { handleImageUpload as handleImageUploadDefault } from './handleImageUpload.js'; import { processUploadedImage } from './processUploadedImage.js'; import { insertNewRelationship } from '@core/super-converter/docx-helpers/document-rels.js'; -export const startImageUpload = async ({ editor, view, file }) => { - const imageUploadHandler = - typeof editor.options.handleImageUpload === 'function' - ? editor.options.handleImageUpload - : handleImageUploadDefault; - +const fileTooLarge = (file) => { let fileSizeMb = Number((file.size / (1024 * 1024)).toFixed(4)); if (fileSizeMb > 5) { window.alert('Image size must be less than 5MB'); - return; + return true; + } + return false; +}; + +export const checkAndProcessImage = async ({ getMaxContentSize, file }) => { + if (fileTooLarge(file)) { + return { file: null, size: { width: 0, height: 0 } }; } - let width; - let height; try { // Will process the image file in place - const processedImageResult = await processUploadedImage(file, editor); - width = processedImageResult.width; - height = processedImageResult.height; - file = processedImageResult.file; + const processedImageResult = await processUploadedImage(file, getMaxContentSize); + const process = processedImageResult; + return { file: process.file, size: { width: process.width, height: process.height } }; } catch (err) { console.warn('Error processing image:', err); - return; + return { file: null, size: { width: 0, height: 0 } }; } - - await uploadImage({ - editor, - view, - file, - size: { width, height }, - uploadHandler: imageUploadHandler, - }); }; -export async function uploadImage({ editor, view, file, size, uploadHandler }) { - // A fresh object to act as the ID for this upload - let id = {}; - +export function replaceSelectionWithImagePlaceholder({ editorOptions, view, id }) { // Replace the selection with a placeholder - let { tr, schema } = view.state; + let { tr } = view.state; let { selection } = tr; - if (editor.options.isHeaderOrFooter) { - selection = editor.options.lastSelection; + if (editorOptions.isHeaderOrFooter) { + selection = editorOptions.lastSelection; } - if (!selection.empty && !editor.options.isHeaderOrFooter) { + if (!selection.empty && !editorOptions.isHeaderOrFooter) { tr.deleteSelection(); } - let imageMeta = { - type: 'add', - pos: selection.from, - id, - }; + tr = addImagePlaceholder(view.state, tr, id, selection.from); - tr.setMeta(ImagePlaceholderPluginKey, imageMeta); view.dispatch(tr); +} - try { - let url = await uploadHandler(file); - - let fileName = file.name.replace(' ', '_'); - let placeholderPos = findPlaceholder(view.state, id); - - // If the content around the placeholder has been deleted, - // drop the image - if (placeholderPos == null) { - return; - } - - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - let removeMeta = { type: 'remove', id }; - - let mediaPath = `word/media/${fileName}`; - - let rId = null; - if (editor.options.mode === 'docx') { - const [, path] = mediaPath.split('word/'); // Path without 'word/' part. - const id = addImageRelationship({ editor, path }); - if (id) rId = id; - } - - let imageNode = schema.nodes.image.create({ - src: mediaPath, - size, - rId, - }); - - editor.storage.image.media = Object.assign(editor.storage.image.media, { [mediaPath]: url }); - - // If we are in collaboration, we need to share the image with other clients - if (editor.options.ydoc) { - editor.commands.addImageToCollaboration({ mediaPath, fileData: url }); - } - - view.dispatch( - view.state.tr - .replaceWith(placeholderPos, placeholderPos, imageNode) // or .insert(placeholderPos, imageNode) - .setMeta(ImagePlaceholderPluginKey, removeMeta), - ); - } catch { - let removeMeta = { type: 'remove', id }; - // On failure, just clean up the placeholder - view.dispatch(tr.setMeta(ImagePlaceholderPluginKey, removeMeta)); +export async function uploadAndInsertImage({ editor, view, file, size, id }) { + const imageUploadHandler = + typeof editor.options.handleImageUpload === 'function' + ? editor.options.handleImageUpload + : handleImageUploadDefault; + + // try { + let url = await imageUploadHandler(file); + + let fileName = file.name.replace(' ', '_'); + let placeholderPos = findPlaceholder(view.state, id); + + // If the content around the placeholder has been deleted, + // drop the image + if (placeholderPos == null) { + return; + } + + let mediaPath = `word/media/${fileName}`; + + let rId = null; + if (editor.options.mode === 'docx') { + const [, path] = mediaPath.split('word/'); // Path without 'word/' part. + const id = addImageRelationship({ editor, path }); + if (id) rId = id; + } + + let imageNode = view.state.schema.nodes.image.create({ + src: mediaPath, + size, + id, + rId, + }); + + editor.storage.image.media = Object.assign(editor.storage.image.media, { [mediaPath]: url }); + + // If we are in collaboration, we need to share the image with other clients + if (editor.options.ydoc) { + editor.commands.addImageToCollaboration({ mediaPath, fileData: url }); } + + let tr = view.state.tr; + + tr.replaceWith(placeholderPos, placeholderPos, imageNode); + + tr = removeImagePlaceholder(view.state, tr, id); + // Otherwise, insert it at the placeholder's position, and remove + // the placeholder + + view.dispatch(tr); + // } catch { + + // const tr = removeImagePlaceholder(view.state.tr, id); + // // On failure, just clean up the placeholder + // view.dispatch(tr); + // } } function addImageRelationship({ editor, path }) { diff --git a/packages/super-editor/src/extensions/pagination/pagination.js b/packages/super-editor/src/extensions/pagination/pagination.js index bf803ecd3..94e4fce48 100644 --- a/packages/super-editor/src/extensions/pagination/pagination.js +++ b/packages/super-editor/src/extensions/pagination/pagination.js @@ -9,7 +9,7 @@ import { broadcastEditorEvents, } from './pagination-helpers.js'; import { CollaborationPluginKey } from '@extensions/collaboration/collaboration.js'; -import { ImagePlaceholderPluginKey } from '@extensions/image/imageHelpers/imagePlaceholderPlugin.js'; +import { getImageRegistrationMetaType } from '@extensions/image/imageHelpers/imageRegistrationPlugin.js'; import { LinkedStylesPluginKey } from '@extensions/linked-styles/index.js'; import { findParentNodeClosestToPos } from '@core/helpers/findParentNodeClosestToPos.js'; import { generateDocxRandomId } from '../../core/helpers/index.js'; @@ -122,9 +122,9 @@ export const Pagination = Extension.create({ } // We need special handling for images / the image placeholder plugin - const imagePluginTransaction = tr.getMeta(ImagePlaceholderPluginKey); - if (imagePluginTransaction) { - if (imagePluginTransaction.type === 'remove') { + const imageRegistrationMetaType = getImageRegistrationMetaType(tr); + if (imageRegistrationMetaType) { + if (imageRegistrationMetaType === 'remove') { onImageLoad(editor); } return { ...oldState }; diff --git a/packages/super-editor/src/tests/editor/relationships.test.js b/packages/super-editor/src/tests/editor/relationships.test.js index 2fa588219..b9d71c62a 100644 --- a/packages/super-editor/src/tests/editor/relationships.test.js +++ b/packages/super-editor/src/tests/editor/relationships.test.js @@ -2,8 +2,10 @@ import { loadTestDataForEditorTests, initTestEditor } from '@tests/helpers/helpe import { TextSelection } from 'prosemirror-state'; import { expect } from 'vitest'; import { getDocumentRelationshipElements } from '@core/super-converter/docx-helpers/document-rels.js'; -import { uploadImage } from '@extensions/image/imageHelpers/startImageUpload.js'; -import { handleImageUpload as handleImageUploadDefault } from '@extensions/image/imageHelpers/handleImageUpload.js'; +import { + uploadAndInsertImage, + replaceSelectionWithImagePlaceholder, +} from '@extensions/image/imageHelpers/startImageUpload.js'; import { imageBase64 } from './data/imageBase64.js'; describe('Relationships tests', () => { @@ -39,12 +41,20 @@ describe('Relationships tests', () => { const blob = await fetch(imageBase64).then((res) => res.blob()); const file = new File([blob], 'image.png', { type: 'image/png' }); - await uploadImage({ + const id = {}; + + replaceSelectionWithImagePlaceholder({ + view: editor.view, + editorOptions: editor.options, + id, + }); + + await uploadAndInsertImage({ editor, view: editor.view, file, size: { width: 100, height: 100 }, - uploadHandler: handleImageUploadDefault, + id, }); const imageNode = editor.state.doc.firstChild.firstChild;