Skip to content

Commit 13cd152

Browse files
committed
feat(rte): add image upload handler on paste
1 parent bba7dc6 commit 13cd152

File tree

5 files changed

+169
-95
lines changed

5 files changed

+169
-95
lines changed

packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,7 @@ function uploadImage(ref: MutableRefObject<Quill | null>, range: Range, options:
232232
{
233233
alt: options.alt,
234234
width: options.width,
235-
height: options.height,
236-
"data-src": "10696049115005183",
237-
"data-mx-timestamp": "1749298464712"
235+
height: options.height
238236
}
239237
);
240238
}, new Delta().retain(range.index).delete(range.length)) as Delta;

packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "./CustomToolbars/toolbarHandlers";
3131
import { useEmbedModal } from "./CustomToolbars/useEmbedModal";
3232
import Dialog from "./ModalDialog/Dialog";
33+
import MxUploader from "../utils/modules/uploader";
3334

3435
export interface EditorProps
3536
extends Pick<RichTextContainerProps, "imageSource" | "imageSourceContent" | "enableDefaultUpload"> {
@@ -166,6 +167,7 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
166167
if (arg[0].href) {
167168
customLinkHandler(arg[0]);
168169
} else if (arg[0].src) {
170+
// open dialog editor for updating imag or video
169171
if (arg[0].type === "video") {
170172
customVideoHandler(arg[0]);
171173
} else {
@@ -176,6 +178,9 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
176178
if (contextDispatch) {
177179
contextDispatch(arg[0]);
178180
}
181+
} else if (arg[0].type === "image") {
182+
// open dialog editor for updating image (triggered by module uploader)
183+
customImageUploadHandler(arg[0]);
179184
}
180185
}
181186
}
@@ -193,6 +198,13 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
193198
[ref, toolbarId]
194199
);
195200

201+
useEffect(() => {
202+
// if image source is set from entity, handle upload differently
203+
if (props.imageSource && props.imageSource.status === "available") {
204+
(ref.current?.getModule("uploader") as MxUploader)?.setEntityUpload?.(true);
205+
}
206+
}, [props.imageSource, props.imageSource?.status, ref]);
207+
196208
return (
197209
<Fragment>
198210
<div ref={containerRef} style={style} className={className}></div>

packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/ImageDialog.tsx

Lines changed: 119 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,35 @@ export default function ImageDialog(props: ImageDialogProps): ReactElement {
3939
imageSource?.status !== "available";
4040

4141
const inputReference = useRef<HTMLInputElement>(null);
42+
const isInputProcessed = useRef(false);
4243
useEffect(() => {
4344
setTimeout(() => inputReference?.current?.focus(), 50);
44-
}, []);
45+
if (
46+
!disableEmbed &&
47+
imageSource &&
48+
imageSource.status === "available" &&
49+
defaultValue?.files &&
50+
!isInputProcessed.current
51+
) {
52+
// if there is a file given, and imageSource is available
53+
// asume that we want to do image upload to entity
54+
// and switch to embed tab
55+
setActiveTab("embed");
56+
}
57+
}, [defaultValue?.files, disableEmbed, imageSource]);
58+
59+
useEffect(() => {
60+
if (activeTab === "embed" && defaultValue?.files && !isInputProcessed.current) {
61+
// upload image directly to entity using external file uploader widget (if available)
62+
const inputFiles = imageUploadElementRef.current?.querySelector("input[type='file']") as HTMLInputElement;
63+
if (inputFiles) {
64+
inputFiles.files = defaultValue.files as FileList;
65+
inputFiles.dispatchEvent(new Event("change", { bubbles: true }));
66+
}
67+
isInputProcessed.current = true;
68+
}
69+
// eslint-disable-next-line react-hooks/exhaustive-deps
70+
}, [activeTab]);
4571

4672
const [formState, setFormState] = useState<imageConfigType>({
4773
files: null,
@@ -78,116 +104,117 @@ export default function ImageDialog(props: ImageDialogProps): ReactElement {
78104
};
79105

80106
useEffect(() => {
107+
// event listener for image selection triggered from custom widgets JS Action
81108
const imgRef = imageUploadElementRef.current;
82109

83-
// const element = ref.current;
84110
if (imgRef !== null) {
85111
imgRef.addEventListener("imageSelected", handleImageSelected);
86112
}
87-
// element.addEventListener("click", handleClick);
88-
89113
return () => {
90114
imgRef?.removeEventListener("imageSelected", handleImageSelected);
91115
};
116+
// eslint-disable-next-line react-hooks/exhaustive-deps
92117
}, [imageUploadElementRef.current]);
93118

94119
return (
95120
<DialogContent className="video-dialog">
96121
<DialogHeader onClose={onClose}>{activeTab === "general" ? "Insert/Edit" : "Embed"} Images</DialogHeader>
97122
<DialogBody>
98-
{!disableEmbed && (
99-
<div>
100-
<ul className="nav nav-tabs mx-tabcontainer-tabs" role="tablist">
101-
<li
102-
role="presentation"
103-
className={classNames({
104-
active: activeTab === "general"
105-
})}
106-
onClick={() => setActiveTab("general")}
107-
>
108-
<a href="#">General</a>
109-
</li>
110-
<li
111-
role="presentation"
112-
className={classNames({
113-
active: activeTab === "embed"
114-
})}
115-
onClick={(e: React.MouseEvent) => {
116-
setActiveTab("embed");
117-
e.stopPropagation();
118-
e.preventDefault();
119-
}}
120-
>
121-
<a href="#">Attachments</a>
122-
</li>
123-
</ul>
124-
</div>
125-
)}
126123
<div ref={imageUploadElementRef}>
127-
<If condition={activeTab === "general"}>
128-
<FormControl label="Source">
129-
{defaultValue?.src ? (
130-
<img
131-
src={defaultValue.src}
132-
alt={defaultValue.alt}
133-
className="mx-image-dialog-thumbnail-small"
134-
/>
135-
) : formState.entityGuid && selectedImageEntity ? (
136-
<div className="mx-image-dialog-thumbnail-container">
124+
{!disableEmbed && (
125+
<div>
126+
<ul className="nav nav-tabs mx-tabcontainer-tabs" role="tablist">
127+
<li
128+
role="presentation"
129+
className={classNames({
130+
active: activeTab === "general"
131+
})}
132+
onClick={() => setActiveTab("general")}
133+
>
134+
<a href="#">General</a>
135+
</li>
136+
<li
137+
role="presentation"
138+
className={classNames({
139+
active: activeTab === "embed"
140+
})}
141+
onClick={(e: React.MouseEvent) => {
142+
setActiveTab("embed");
143+
e.stopPropagation();
144+
e.preventDefault();
145+
}}
146+
>
147+
<a href="#">Attachments</a>
148+
</li>
149+
</ul>
150+
</div>
151+
)}
152+
<div>
153+
<If condition={activeTab === "general"}>
154+
<FormControl label="Source">
155+
{defaultValue?.src ? (
137156
<img
138-
src={selectedImageEntity.thumbnailUrl || selectedImageEntity.url}
139-
alt={selectedImageEntity.id}
157+
src={defaultValue.src}
158+
alt={defaultValue.alt}
140159
className="mx-image-dialog-thumbnail-small"
141160
/>
142-
<span className="icon-container">
143-
<span className="icons icon-Delete" onClick={onEmbedDeleted}></span>
144-
</span>
145-
</div>
146-
) : enableDefaultUpload ? (
161+
) : formState.entityGuid && selectedImageEntity ? (
162+
<div className="mx-image-dialog-thumbnail-container">
163+
<img
164+
src={selectedImageEntity.thumbnailUrl || selectedImageEntity.url}
165+
alt={selectedImageEntity.id}
166+
className="mx-image-dialog-thumbnail-small"
167+
/>
168+
<span className="icon-container">
169+
<span className="icons icon-Delete" onClick={onEmbedDeleted}></span>
170+
</span>
171+
</div>
172+
) : enableDefaultUpload ? (
173+
<input
174+
name="files"
175+
className="form-control mx-textarea-input mx-textarea-noresize code-input"
176+
type="file"
177+
accept={IMG_MIME_TYPES.join(", ")}
178+
onChange={onFileChange}
179+
></input>
180+
) : undefined}
181+
</FormControl>
182+
<FormControl label="Alternative description">
183+
<input
184+
className="form-control"
185+
type="text"
186+
name="alt"
187+
onChange={onInputChange}
188+
value={formState.alt}
189+
ref={inputReference}
190+
/>
191+
</FormControl>
192+
<FormControl label="Width">
147193
<input
148-
name="files"
149-
className="form-control mx-textarea-input mx-textarea-noresize code-input"
150-
type="file"
151-
accept={IMG_MIME_TYPES.join(", ")}
152-
onChange={onFileChange}
153-
></input>
154-
) : undefined}
155-
</FormControl>
156-
<FormControl label="Alternative description">
157-
<input
158-
className="form-control"
159-
type="text"
160-
name="alt"
161-
onChange={onInputChange}
162-
value={formState.alt}
163-
ref={inputReference}
164-
/>
165-
</FormControl>
166-
<FormControl label="Width">
167-
<input
168-
className="form-control"
169-
type="number"
170-
name="width"
171-
onChange={onInputChange}
172-
value={formState.width}
173-
/>
174-
px
175-
</FormControl>
176-
<FormControl label="Height">
177-
<input
178-
className="form-control"
179-
type="number"
180-
name="height"
181-
onChange={onInputChange}
182-
value={formState.height}
183-
/>
184-
px
185-
</FormControl>
186-
<DialogFooter onSubmit={() => onSubmit(formState)} onClose={onClose}></DialogFooter>
187-
</If>
188-
<If condition={activeTab === "embed"}>
189-
<div>{imageSourceContent}</div>
190-
</If>
194+
className="form-control"
195+
type="number"
196+
name="width"
197+
onChange={onInputChange}
198+
value={formState.width}
199+
/>
200+
px
201+
</FormControl>
202+
<FormControl label="Height">
203+
<input
204+
className="form-control"
205+
type="number"
206+
name="height"
207+
onChange={onInputChange}
208+
value={formState.height}
209+
/>
210+
px
211+
</FormControl>
212+
<DialogFooter onSubmit={() => onSubmit(formState)} onClose={onClose}></DialogFooter>
213+
</If>
214+
<If condition={activeTab === "embed"}>
215+
<div>{imageSourceContent}</div>
216+
</If>
217+
</div>
191218
</div>
192219
</DialogBody>
193220
</DialogContent>

packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IndentLeftStyle, IndentRightStyle } from "./formats/indent";
1414
import Formula from "./formats/formula";
1515
import QuillResize from "quill-resize-module";
1616
import QuillTableBetter from "./formats/quill-table-better/quill-table-better";
17+
import MxUploader from "./modules/uploader";
1718

1819
class Empty {
1920
doSomething(): string {
@@ -35,6 +36,7 @@ Quill.register(IndentLeftStyle, true);
3536
Quill.register(IndentRightStyle, true);
3637
Quill.register(Formula, true);
3738
Quill.register(Button, true);
39+
Quill.register({ "modules/uploader": MxUploader }, true);
3840
Quill.register("modules/resize", QuillResize, true);
3941
// add empty handler for view code, this format is handled by toolbar's custom config via ViewCodeDialog
4042
Quill.register({ "ui/view-code": Empty });
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Range } from "quill/core/selection";
2+
import Uploader from "quill/modules/uploader";
3+
import { ACTION_DISPATCHER } from "../helpers";
4+
5+
class MxUploader extends Uploader {
6+
protected useEntityUpload: boolean = false;
7+
8+
setEntityUpload(useEntityUpload: boolean): void {
9+
this.useEntityUpload = useEntityUpload;
10+
}
11+
12+
upload(range: Range, files: FileList | File[]): void {
13+
if (!this.quill.scroll.query("image")) {
14+
return;
15+
}
16+
if (this.useEntityUpload) {
17+
// If entity upload is enabled, the file will be handled by external widget's upload handler.
18+
const dataTransfer = new DataTransfer();
19+
Array.from(files).forEach(file => {
20+
if (file && this.options.mimetypes?.includes(file.type)) {
21+
dataTransfer.items.add(file);
22+
}
23+
});
24+
const imageInfo = {
25+
type: "image",
26+
files: dataTransfer.files
27+
};
28+
this.quill.emitter.emit(ACTION_DISPATCHER, imageInfo);
29+
} else {
30+
super.upload.call(this, range, files);
31+
}
32+
}
33+
}
34+
35+
export default MxUploader;

0 commit comments

Comments
 (0)