Open utterances-bot opened 3 years ago
I'm aware this may not be the ideal implementation, so I've started a thread on the tiptap repo and will edit this or write a new post if there is a better way
I am super curious to your direct s3 upload. I was planning to build one myself but then got to this post. Anything to share?
I am super curious to your direct s3 upload. I was planning to build one myself but then got to this post. Anything to share?
Hi @georgeboot it's using pre-signed URLs. You will need something on the server side to connect privately to the S3 (or compatible) API and get a single use URL with which you can PUT upload the file directly to S3.
The specific code depends on your back and front end language and framework choices. But here is a simplified example for Laravel and Vue.
This assumes you've handled the routing, added correct S3 credentials etc.
class UploadUrlController
{
public function __invoke()
{
$s3 = Storage::disk('s3');
// Get the underlying S3 client
$client = $s3->getDriver()->getAdapter()->getClient();
$expiry = "+20 minutes";
// Have some sort of naming strategy
$extension = Request::input('extension');
$fileName = 'img-' . time() . '-' . Auth::user()->id . '.' . $extension;
// Generate the pre signed URL
$command = $client->getCommand('PutObject', [
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $fileName,
'ACL' => 'public-read'
]);
$request = $client->createPresignedRequest($command, $expiry);
$uploadURL = (string) $request->getUri();
return [
'url' => $uploadURL,
'fileName' => $fileName,
'filePath' => explode('?', $uploadURL)[0]
];
}
}
And on the front end
async fileChanged() {
const files = document.getElementById('fileUpload').files;
if (files) {
const config = {
headers: // any authorisation needed
}
this.uploadProgress = 0
const file = files[0];
const fileName = file.name;
const extension = fileName.split('.').pop()
const response = await axios.post('/api/upload-url', { extension: extension }, config).catch(
error => {
// handle the error
}
);
const options = {
// If you want the uploaded file to be public
headers: {
'x-amz-acl': 'public-read'
},
onUploadProgress: (progressEvent) => {
// Handle some visual feedback
}
};
await axios.put(response.data.url, file, options).catch(
error => {
// handle the error
}
);
const src = response.data.filePath
this.doSomethingWithTheImage({ src })
document.getElementById('fileUpload').value = null
}
}
The ACL used when you generate the URL with getCommand
must also be passed in the header of the PUT.
If you want private files, that's most likely the default in your setup and you can omit both.
Awesome, thanks! I am using Livewire so they take care of the upload to s3 already.
What I was planning:
input[type="file"]
$livewire.upload(file)
...this.editor.chain().focus().setImage({ src: urlReturnedByLivewire }).run()
Yes that sounds absolutely fine :)
It sounds like you're perfectly on top of things already. Did I miss a specific part of your question?
Thanks a lot! My approach worked in my head but practise is usually different haha. Thanks for sharing your example, it reassures my solution might just work and will help a lot for the general structure.
I will share my code once ready. Someone else might just want to use it.
I will share my code once ready. Someone else might just want to use it.
Nice one mate 👍
I made a package out of it: https://github.com/georgeboot/laravel-tiptap
How I solved the image upload part:
Thanks for coming back George, that's a great package. You should submit it here https://laravel-news.com/links
Why did you implement your own S3 upload in the end? To keep the requirements to only Alpine.js and not also Livewire?
Why did you implement your own S3 upload in the end? To keep the requirements to only Alpine.js and not also Livewire?
I didn't like livewire doing an ajax call before each image. The solution I used, allows for one pre-signed URL that can be reused multiple times. But yeah, it's a minor thing. It also didn't play too well with the client-side resizing.
This helped a lot. I recently added it to my react project and it works good. Do you have any way we can show the local image till the image is uploaded on s3. And then after uploading we show the actual image from s3 signed url link?
This helped a lot. I recently added it to my react project and it works good. Do you have any way we can show the local image till the image is uploaded on s3. And then after uploading we show the actual image from s3 signed url link?
You'll need to dig into the underlying ProseMirror plugin system add a Decoration. Here is an example from ProseMirror docs.
You can also search for ready made ProseMirror plugins which can, with some tweaking, be used with TipTap.
@joevallender Thanks for the example. I will have a look at it.
@joevallender You did a nice job of extending the Image extension. Thank you. Although, defaultOptions is deprecated. There's a warning on my browser console to use addOptions instead.
@joevallender You did a nice job of extending the Image extension. Thank you. Although, defaultOptions is deprecated. There's a warning on my browser console to use addOptions instead.
Thanks @Igirid :)
I published this as a quick experiment prior to any real documentation being published for TipTap v2... so I think a quite few parts could be either wrong, out of date, or at the very least be done better!
@joevallender This is really nice! I am wondering how you were getting urls for the image on each new load if they are in private s3 buckets as I couldn't see any url fetching in the extension code?
Hi @damienpjones
See this comment https://github.com/joevallender/aboutwebdev/issues/2#issuecomment-895092263 for an example of how to save into a public s3 bucket (using PHP/Laravel for the server side, but it's similar in all languages) and return the image URL.
The example only uses the the response.data.filePath
to display the image. But you can also save that URL anywhere you like. If it was, for example, a user profile you might have a user object and you can add user.profileImage
and save that back to the server as you would user.name
or any other text value
@joevallender This is great! Thanks so much for sharing!
I've noticed that when loading content direction into TipTap the attribute gets stripped, I'm new to this but I'm guessing I need to create some sorta addNodeView() maybe?
@joevallender This is great! Thanks so much for sharing!
I've noticed that when loading content direction into TipTap the attribute gets stripped, I'm new to this but I'm guessing I need to create some sorta addNodeView() maybe?
I haven't worked with this exactly, but I guess you'd either want to add the dir
attribute to your image node or, as this plugin appears to do, globally
@joevallender Thanks! Incase anyone else comes across this, I did some digging and it looking like global attributes is the best way to go, and recommended in the docs. 🤙
@joevallender I'm actually using private buckets that need signed urls so used a node view
return ({ node }) => {
const imgEl = document.createElement('img');
imgEl.classList.add('rich-text-image');
if (node.attrs.datasrc.includes('s3')) {
getS3SignedUrl(node.attrs.datasrc).then((url) => {
imgEl.src = url;
});
} else {
imgEl.src = node.attrs.datasrc;
}
return {
dom: imgEl,
};
};
},
along with using a datasrc attribute which I use to set the HTML src attribute on each load
renderHTML({ node, HTMLAttributes }) {
if (node.attrs.datasrc.includes('s3')) {
getS3SignedUrl(node.attrs.datasrc).then((url) => {
HTMLAttributes.src = url;
});
} else {
HTMLAttributes.src = node.attrs.datasrc;
}
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
@damienpjones I ended up going down this route too, much simpler! ✨
Thanks for this. Nicely done.
One issue I'm having is initially setting editor content with the images size attributes. They aren't showing in the DOM which appears to default to small always. I cloned your code and added class="custom-image custom-image-large" to the content when initialized (in line 207 of Editor.vue). Any idea what I am doing wrong?
Got it...just added a parseHTML to look for classes and set the size attribute accordingly.
Adding functionality to the image extension in tiptap 2 and Vue 3 | aboutweb.dev
Webdev related writings
https://aboutweb.dev/blog/tiptap2-vue3-extending-image-functionality/