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

feat(text-editor): add support for pasting inline images #3464

Merged
merged 2 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions etc/lime-elements.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,32 @@ interface Image_2 {
}
export { Image_2 as Image }

// @alpha (undocumented)
export interface ImageInfo {
fileInfoId: string;
src: string;
state: ImageState;
}

// @alpha (undocumented)
export interface ImageInserter {
// (undocumented)
fileInfo: FileInfo;
insertFailedThumbnail: () => void;
insertImage: (src?: string) => void;
insertThumbnail: () => void;
}

// @alpha (undocumented)
export enum ImageState {
// (undocumented)
FAILED = "failed",
// (undocumented)
LOADING = "loading",
// (undocumented)
SUCCESS = "success"
}

// @public (undocumented)
export interface InfoTileProgress {
displayPercentageColors?: boolean;
Expand Down Expand Up @@ -1651,6 +1677,10 @@ export namespace JSX {
"language"?: Languages;
"onChange"?: (event: LimelProsemirrorAdapterCustomEvent<string>) => void;
// @alpha
"onImagePasted"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInserter>) => void;
// @alpha
"onImageRemoved"?: (event: LimelProsemirrorAdapterCustomEvent<ImageInfo>) => void;
// @alpha
"triggerCharacters"?: TriggerCharacter[];
"value"?: string;
}
Expand Down Expand Up @@ -1772,6 +1802,10 @@ export namespace JSX {
"language"?: Languages;
"onChange"?: (event: LimelTextEditorCustomEvent<string>) => void;
// @alpha
"onImagePasted"?: (event: LimelTextEditorCustomEvent<ImageInserter>) => void;
// @alpha
"onImageRemoved"?: (event: LimelTextEditorCustomEvent<ImageInfo>) => void;
// @alpha
"onTriggerChange"?: (event: LimelTextEditorCustomEvent<TriggerEventDetail>) => void;
// @alpha
"onTriggerStart"?: (event: LimelTextEditorCustomEvent<TriggerEventDetail>) => void;
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just now actually tried this example for the first time, and holy smokes it's amazing! I mean, I knew exactly what I expected it to do, and it did exactly that, but it was still amazing to actually see! Sweet work! 🙌

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Component, h, State } from '@stencil/core';
import {
ImageInserter,
LimelTextEditorCustomEvent,
} from '@limetech/lime-elements';
/**
* Handling inline images (with base64 encoded data)
*
* To allow users to paste images directly into the text editor, you can
* listen to the `imagePasted` event, which is triggered when an image file
* is pasted into the editor.
*
* The `imagePasted` event contains an `ImageInserter` object, which you can
* use to insert a thumbnail of the pasted image into the editor.
* After the thumbnail is inserted, you can immediately insert the image
* as base64 encoded data using the `insertImage` method.
*
* :::note
* This example demonstrates the simplest approach using base64 encoding.
* However, for production use, it is recommended to upload images to
* external file storage and insert the src URL of the uploaded image
* instead, as shown in the file-storage example.
*/
@Component({
tag: 'limel-example-text-editor-with-inline-images-base64',
shadow: true,
})
export class TextEditorWithInlineImagesExample {
@State()
private value = 'Copy an image file and paste it here.';

public render() {
return (
<limel-text-editor
value={this.value}
onChange={this.handleChange}
onImagePasted={this.handleImagePasted}
contentType="html"
/>
);
}

private handleChange = (event: LimelTextEditorCustomEvent<string>) => {
this.value = event.detail;
};

private handleImagePasted = async (
event: LimelTextEditorCustomEvent<ImageInserter>,
) => {
const imageInserter = event.detail;

imageInserter.insertThumbnail();
imageInserter.insertImage();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Component, h, State, Host } from '@stencil/core';
import {
ImageInserter,
FileInfo,
ImageInfo,
LimelTextEditorCustomEvent,
LimelCheckboxCustomEvent,
} from '@limetech/lime-elements';
/**
* Handling inline images (with external file storage)
*
* To allow users to paste images directly into the text editor, you can
* listen to the `imagePasted` event, which is triggered when an image file
* is pasted into the editor.
*
* The `imagePasted` event contains an `ImageInserter` object, which you can
* use to insert a thumbnail of the pasted image into the editor.
* After the thumbnail is inserted, you can upload the image to an external
* file storage and insert the src url of the uploaded image using the
* `insertImage` method.
*
* If the image upload fails, you can insert a failed thumbnail using the
* `insertFailedThumbnail` method.
*
* :::note
* In this example, because we don't actually upload the image you paste
* anywhere, once the "upload" is done, we will replace the image you
* pasted with a url to an image of the Lime CRM logo.
*
* In reality, you would of course insert the url to the newly uploaded
* image instead.
*/
@Component({
tag: 'limel-example-text-editor-with-inline-images-file-storage',
shadow: true,
})
export class TextEditorWithInlineImagesExample {
@State()
private value = 'Copy an image file and paste it here.';

@State()
private uploadImageFails = false;

public render() {
return (
<Host>
<limel-text-editor
value={this.value}
onChange={this.handleChange}
onImagePasted={this.handleImagePasted}
onImageRemoved={this.handleImageRemoved}
/>
<limel-checkbox
label="Upload image fails - insert failed thumbnail"
onChange={this.handleFailedThumbnailChange}
/>
<limel-example-value label="Value" value={this.value} />
</Host>
);
}

private handleFailedThumbnailChange = (
event: LimelCheckboxCustomEvent<boolean>,
) => {
this.uploadImageFails = event.detail;
};

private handleChange = (event: LimelTextEditorCustomEvent<string>) => {
this.value = event.detail;
};

private handleImagePasted = async (
event: LimelTextEditorCustomEvent<ImageInserter>,
) => {
const imageInserter = event.detail;

imageInserter.insertThumbnail();

const imageSrc = await this.uploadImage(imageInserter.fileInfo);
if (imageSrc) {
imageInserter.insertImage(imageSrc);
} else {
imageInserter.insertFailedThumbnail();
}
};

private uploadImage = async (fileInfo: FileInfo): Promise<string> => {
try {
// Upload image to external file storage.
// fileInfo.fileContent contains the image data.

// Simulate upload delay.
const imageSrc: string = await new Promise((resolve, reject) => {
setTimeout(() => {
if (this.uploadImageFails) {
reject('Server error');
} else {
resolve('https://cdn.lime-crm.com/mail-addin-logo.png');
}
}, 2000);
});

// Return the src url of the uploaded image.
return imageSrc;
} catch (error) {
console.error(
`Failed to upload image ${fileInfo.filename}: ${error}`,
);
}
};

private handleImageRemoved = (
event: LimelTextEditorCustomEvent<ImageInfo>,
) => {
const imageInfo = event.detail;
console.log(`Image deleted: ${imageInfo.fileInfoId}`);

try {
throw new Error('Not implemented.');
} catch (error) {
console.error(
`Failed to delete image ${imageInfo.fileInfoId}`,
error,
);
}
};
}

This file was deleted.

Loading