Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uploading images with TinyMCE does not replace the src attribute inside the image tag #501

Open
aiphton opened this issue Feb 7, 2024 · 5 comments
Labels
GitHub Issues that have been added to our internal issue tracker. status: escalated

Comments

@aiphton
Copy link

aiphton commented Feb 7, 2024

📝 Provide detailed reproduction steps (if any)

I'm trying to upload images from my React frontend to my Laravel backend using TinyMCE the image plugin that is provided from TinyMCE. My backend and frontend are glued together with inertiajs.
The upload process works fine and saves the image, but when I try to save the form with the image inside of the TinyMCE Editor I get an error. This error comes from my backend where I first validate the request and then allow it to be processed.
This request fails because the image gets omitted as a base64 string inside of the src attribute inside of the <img> tag and the resulting string exceeds my max limit of 65535 characters.
Now strangely after that first unsuccessful attempt the src attribute wihtin the <img> tag has been replaced successfully with the location string from my backend and I now can save the form without a problem.

I will also provide my a snippet of my current TinyMCE setup. My current implementation is that the image is first added locally as a blob and then uploaded to the server when the user saves the form. After that, TinyMCE should automatically replace the src attribute with the appropriate location string.

My current implementation is as follows

 const submit = (e) => {
    e.preventDefault()

    editorRef.current.uploadImages().then((uploadResults) => {
      if (!memory) {
        post(route('memory'), {
          onSuccess: handleSuccess,
          onError: (errors) => {
            console.error(Object.values(errors))
            handleError()
          }
        })
      } else {
        post(route('memory.update', { slug: memory.slug }), {
          onSuccess: handleSuccess,
          onError: (errors) => {
            console.error(errors)
            handleError()
          }
        })
      }
    })
  }

const handleSuccess = () => {
    toast({
      title: <Check className="text-blue-500"/>,
      description: !memory
        ? t('apiResponse.success.add')
        : t('apiResponse.success.text'),
    })
  }

  const handleError = () => {
    toast({
      title: t('apiResponse.error.title')
    })
  }

Is there something wrong with how I'm handling the Promise?

I am grateful for anyone's efforts to point me in the right direction. I have spent hours trying to fix this problem. I just can't seem to get behind it.
If you need additional context, please ask. I'm more than happy to provide it.

I have already tried setting the state of body manually and replacing the base64 string with the url from the promise result

async function uploadImages() {
  const results = await editorRef.current.uploadImages();
  const body= data.body; // Create a copy of the state

  for (const result of results) {
    if (result.status === true) {
      const imageUrl = result.uploadUri;
      updatedBody = updatedBody.replace(result.blobInfo.base64(), imageUrl);
    }
  }

  setData('body', updatedBody);
  return results.map(result => result.status === true);
}

and then using it like this:

const submit = async (e) => {
  e.preventDefault();

  try {
    const uploadResults = await uploadImages();

    if (uploadResults.every(value => value)) {
      if (typeof memory === 'undefined') {
        post(route('memory'), { onSuccess: handleSuccess });
      } else {
        patch(route('memory.update', { slug: memory.slug }), { onSuccess: handleSuccess });
      }
    } else {
      toast({ title: t('apiResponse.error.title') });
    }
  } catch (error) {
    console.error('Error during image uploads:', error);
  }
};

Here is my TinyMCE implementation:

<Editor
  init={{
    file_picker_types: 'image',
    relative_urls: false,
    remove_script_host: false,
    document_base_url: '../',
    images_upload_handler: handleImageUpload,
    images_upload_url: route('file.store'),
    automatic_uploads: false,
  }}
/>

const handleImageUpload = (blobInfo, progress) => new Promise((resolve, reject) => {
    const formData = new FormData()
    formData.append('file', blobInfo.blob())

    axios.post(route('file.store'), formData, {
      onUploadProgress: ({loaded, total}) => {
        progress(Math.round((loaded * 100) / total))
      },
      withXSRFToken: true,
    }).then((response) => {
      const { location } = response.data
      resolve(location)
    }).catch((error) => {
      console.error('Error:', error)
      reject(error.response.data.location)
    })
  })

✔️ Expected result

The src attribute gets replaced before the form is submitted.

❌ Actual result

The form gets omitted with the blob URI still in the src attribute.

📃 Other details

  • Browser: Opera GX
  • OS: Windows 10

If you'd like to see this fixed sooner, add a 👍 reaction to this post.

@aiphton
Copy link
Author

aiphton commented Feb 14, 2024

Upon debugging, I discovered that the onEditorChange function isn't triggered after invoking uploadImages(). Despite implementing a method to update the body state, the body remains unchanged.

Here's how I integrate TinyMCE as a component in my form:

<TinyEditorComponent
  onInit={(evt, editor) => (editorRef.current = editor)}
  init={{
    content_style: 'body { font-size:14px };',
    height: '100vh',
  }}
  onEditorChange={onEditorChange}
  initialValue={text}
/>

Here, the text value in the initialValue prop is simply a state derived from data retrieved from the database.

And here's the method I've implemented:

const onEditorChange = (newContent) => {
  setData('body', newContent);
  console.log("Body changed:", newContent);
};

This method simply updates the state of the body, which is later sent via the request. Note that I'm using the setData function from the InertiaJS userForm hook, but I've also experimented with a conventional useState hook from React with the same outcome: the state data isn't updated.

Additionally, I have to manually invoke the onEditorChange method because TinyMCE doesn't trigger it automatically after uploadImages() completes. So, the submission process looks something like this:

const submit = (e) => {
  e.preventDefault();

  editorRef.current.uploadImages().then((uploadResults) => {
    const updatedContent = editorRef.current.getContent();
    onEditorChange(updatedContent);
  }).then(() => {
    // Additional actions after image upload
  });
};

@aiphton
Copy link
Author

aiphton commented Feb 15, 2024

I resolved the issue through sending the updatedContent immediatly with the help of the interiaJS router and not depending on the state of data.body state.

@aiphton aiphton closed this as completed Feb 15, 2024
@TheSpyder
Copy link
Member

Apologies, this should've been moved to our React integration. Not updating the state after image upload might be a bug, so I'm reopening it and moving it over.

@TheSpyder TheSpyder reopened this Feb 16, 2024
@TheSpyder TheSpyder transferred this issue from tinymce/tinymce Feb 16, 2024
@aiphton
Copy link
Author

aiphton commented Feb 16, 2024

If you need any more details or informations feel free to ask.

@danoaky-tiny
Copy link
Collaborator

danoaky-tiny commented Mar 7, 2024

Internal ref: INT-3289

@danoaky-tiny danoaky-tiny added the GitHub Issues that have been added to our internal issue tracker. label Mar 10, 2024
@tinymce tinymce deleted a comment from tiny-stale-bot Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
GitHub Issues that have been added to our internal issue tracker. status: escalated
Projects
None yet
Development

No branches or pull requests

3 participants