Skip to content

Commit 3ef8dda

Browse files
committed
feat(rte): keep image aspect ratio
1 parent 13cd152 commit 3ef8dda

File tree

7 files changed

+169
-48
lines changed

7 files changed

+169
-48
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,16 @@ export function useEmbedModal(ref: MutableRefObject<Quill | null>): ModalReturnT
152152
dialogType: "image",
153153
config: {
154154
onSubmit: (value: imageConfigType) => {
155+
const defaultImageConfig = {
156+
alt: value.alt,
157+
width: value.width,
158+
height: value.keepAspectRatio ? undefined : value.height
159+
};
160+
155161
if (value.src) {
156162
const index = selection?.index ?? 0;
157163
const length = 1;
158-
const imageConfig = {
159-
alt: value.alt,
160-
width: value.width,
161-
height: value.height
162-
};
164+
const imageConfig = defaultImageConfig;
163165
// update existing image attribute
164166
const imageUpdateDelta = new Delta().retain(index).retain(length, imageConfig);
165167
ref.current?.updateContents(imageUpdateDelta, Emitter.sources.USER);
@@ -170,9 +172,7 @@ export function useEmbedModal(ref: MutableRefObject<Quill | null>): ModalReturnT
170172
uploadImage(ref, selection, value);
171173
} else if (value.entityGuid) {
172174
const imageConfig = {
173-
alt: value.alt,
174-
width: value.width,
175-
height: value.height,
175+
...defaultImageConfig,
176176
"data-src": value.entityGuid
177177
};
178178
const delta = new Delta()

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export default function ImageDialog(props: ImageDialogProps): ReactElement {
7474
alt: defaultValue?.alt ?? "",
7575
width: defaultValue?.width ?? 100,
7676
height: defaultValue?.height ?? 100,
77-
src: defaultValue?.src ?? undefined
77+
src: defaultValue?.src ?? undefined,
78+
keepAspectRatio: true
7879
});
7980

8081
const onFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
@@ -87,6 +88,11 @@ export default function ImageDialog(props: ImageDialogProps): ReactElement {
8788
setFormState({ ...formState, [e.target.name]: e.target.value });
8889
};
8990

91+
const onInputCheckboxChange = (e: ChangeEvent<HTMLInputElement>): void => {
92+
e.stopPropagation();
93+
setFormState({ ...formState, keepAspectRatio: !formState.keepAspectRatio });
94+
};
95+
9096
const onEmbedSelected = (image: imageListType): void => {
9197
setFormState({ ...formState, entityGuid: image.id, src: undefined, files: null });
9298
setSelectedImageEntity(image);
@@ -206,9 +212,18 @@ export default function ImageDialog(props: ImageDialogProps): ReactElement {
206212
name="height"
207213
onChange={onInputChange}
208214
value={formState.height}
215+
disabled={formState.keepAspectRatio}
209216
/>
210217
px
211218
</FormControl>
219+
<FormControl label="Keep aspect ratio">
220+
<input
221+
type="checkbox"
222+
name="keepAspectRatio"
223+
checked={formState.keepAspectRatio}
224+
onChange={onInputCheckboxChange}
225+
/>
226+
</FormControl>
212227
<DialogFooter onSubmit={() => onSubmit(formState)} onClose={onClose}></DialogFooter>
213228
</If>
214229
<If condition={activeTab === "embed"}>

packages/pluggableWidgets/rich-text-web/src/utils/formats.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export type imageConfigType = {
2626
height?: number;
2727
src?: string;
2828
entityGuid?: string;
29+
keepAspectRatio?: boolean;
2930
};

packages/pluggableWidgets/rich-text-web/src/utils/formats/image.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ class CustomImage extends Image {
1818
super.format(name, value);
1919
}
2020

21-
if (name === "data-src") {
21+
if (name === "data-src" && !this.domNode.dataset.entity) {
2222
this.domNode.setAttribute("src", fetchDocumentUrl(value, Date.now()));
23+
// Mark the image as an entity to prevent further src changes
24+
this.domNode.setAttribute("data-entity", "true");
2325
}
2426
}
2527

packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,11 @@
11
import Quill from "quill";
22
import QuillResize from "quill-resize-module";
33
import { ACTION_DISPATCHER } from "../helpers";
4-
5-
type ToolbarTool = {
6-
text: string;
7-
className: string;
8-
verify: (activeEle: HTMLElement) => boolean;
9-
handler: (
10-
this: typeof QuillResize.Modules.Base,
11-
_evt: MouseEvent,
12-
_button: HTMLElement,
13-
activeEle: HTMLIFrameElement
14-
) => void;
15-
};
16-
17-
// eslint-disable-next-line no-unsafe-optional-chaining
18-
class MxResizeToolbar extends QuillResize.Modules?.Toolbar {
19-
_addToolbarButtons(): void {
20-
const buttons: HTMLButtonElement[] = [];
21-
this.options.tools.forEach((tool: ToolbarTool) => {
22-
if (tool.verify && tool.verify.call(this, this.activeEle) === false) {
23-
return;
24-
}
25-
26-
const button = document.createElement("button");
27-
button.className = tool.className;
28-
buttons.push(button);
29-
button.setAttribute("aria-label", tool.text);
30-
button.setAttribute("type", "button");
31-
32-
button.addEventListener("click", evt => {
33-
tool.handler.call(this, evt, button, this.activeEle);
34-
// image may change position; redraw drag handles
35-
this.requestUpdate();
36-
});
37-
this.toolbar.appendChild(button);
38-
});
39-
}
40-
}
4+
import MxResizeToolbar from "../modules/resizeToolbar";
5+
import MxResize from "../modules/resize";
416

427
export const RESIZE_MODULE_CONFIG = {
43-
modules: ["DisplaySize", MxResizeToolbar, "Resize", "Keyboard"],
8+
modules: ["DisplaySize", MxResizeToolbar, MxResize, "Keyboard"],
449
tools: [
4510
{
4611
text: "Edit Image",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import QuillResize from "quill-resize-module";
2+
3+
type sizeType = {
4+
width?: number;
5+
height?: number;
6+
};
7+
type resultSizeType =
8+
| sizeType
9+
| {
10+
width?: string;
11+
height?: string;
12+
};
13+
14+
type limitType = {
15+
ratio?: number;
16+
minWidth?: number;
17+
maxWidth?: number;
18+
minHeight?: number;
19+
maxHeight?: number;
20+
unit?: string;
21+
};
22+
23+
type eventType = { clientX: number; clientY: number };
24+
// eslint-disable-next-line no-unsafe-optional-chaining
25+
export default class MxResize extends QuillResize.Modules?.Resize {
26+
// modified from https://github.com/mudoo/quill-resize-module/blob/master/src/modules/Resize.js
27+
calcSize(evt: eventType, limit: limitType = {}): resultSizeType {
28+
// update size
29+
const deltaX = evt.clientX - this.dragStartX;
30+
const deltaY = evt.clientY - this.dragStartY;
31+
32+
const size: sizeType = {};
33+
let direction = 1;
34+
35+
(this.blotOptions.attribute || ["width"]).forEach((key: "width" | "height") => {
36+
size[key] = this.preDragSize[key];
37+
});
38+
39+
const allowHeight =
40+
this.activeEle.getAttribute("height") !== undefined && this.activeEle.getAttribute("height") !== null;
41+
if (!allowHeight) {
42+
delete size.height;
43+
}
44+
45+
// left-side
46+
if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[3]) {
47+
direction = -1;
48+
}
49+
50+
if (size.width) {
51+
size.width = Math.round(this.preDragSize.width + deltaX * direction);
52+
}
53+
if (size.height) {
54+
size.height = Math.round(this.preDragSize.height + deltaY * direction);
55+
}
56+
57+
let { width, height } = size;
58+
59+
// keep ratio
60+
if (limit.ratio) {
61+
let limitHeight;
62+
if (limit.minWidth) width = Math.max(limit.minWidth, width!);
63+
if (limit.maxWidth) width = Math.min(limit.maxWidth, width!);
64+
65+
height = width! * limit.ratio;
66+
67+
if (limit.minHeight && height < limit.minHeight) {
68+
limitHeight = true;
69+
height = limit.minHeight;
70+
}
71+
if (limit.maxHeight && height > limit.maxHeight) {
72+
limitHeight = true;
73+
height = limit.maxHeight;
74+
}
75+
76+
if (limitHeight) {
77+
width = height / limit.ratio;
78+
}
79+
} else {
80+
if (size.width) {
81+
if (limit.minWidth) width = Math.max(limit.minWidth, width!);
82+
if (limit.maxWidth) width = Math.min(limit.maxWidth, width!);
83+
}
84+
if (size.height) {
85+
if (limit.minHeight) height = Math.max(limit.minHeight, height!);
86+
if (limit.maxHeight) height = Math.min(limit.maxHeight, height!);
87+
}
88+
}
89+
const res: resultSizeType = {};
90+
91+
if (limit.unit) {
92+
if (width) res.width = width + "px";
93+
if (height) res.height = height + "px";
94+
} else {
95+
if (width) res.width = width;
96+
if (height) res.height = height;
97+
}
98+
return res;
99+
}
100+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import QuillResize from "quill-resize-module";
2+
3+
type ToolbarTool = {
4+
text: string;
5+
className: string;
6+
verify: (activeEle: HTMLElement) => boolean;
7+
handler: (
8+
this: typeof QuillResize.Modules.Base,
9+
_evt: MouseEvent,
10+
_button: HTMLElement,
11+
activeEle: HTMLIFrameElement
12+
) => void;
13+
};
14+
15+
// eslint-disable-next-line no-unsafe-optional-chaining
16+
export default class MxResizeToolbar extends QuillResize.Modules?.Toolbar {
17+
_addToolbarButtons(): void {
18+
const buttons: HTMLButtonElement[] = [];
19+
this.options.tools.forEach((tool: ToolbarTool) => {
20+
if (tool.verify && tool.verify.call(this, this.activeEle) === false) {
21+
return;
22+
}
23+
24+
const button = document.createElement("button");
25+
button.className = tool.className;
26+
buttons.push(button);
27+
button.setAttribute("aria-label", tool.text);
28+
button.setAttribute("type", "button");
29+
30+
button.addEventListener("click", evt => {
31+
tool.handler.call(this, evt, button, this.activeEle);
32+
// image may change position; redraw drag handles
33+
this.requestUpdate();
34+
});
35+
this.toolbar.appendChild(button);
36+
});
37+
}
38+
}

0 commit comments

Comments
 (0)