I am using react-quill editor which supports image upload is accomplished by ‘formats/image’ from quill.
Quill editor doesn’t provide image resize option by default, so we are using the quill-blot-formatter package and register to Quill modules
Quill.register(‘modules/blotFormatter’, BlotFormatter);
It works with image resize but while doing so, it prohibits the scrolling effect of the Quill editor if the mouse focus is inside the resize window
Image resize prohibits editor scrolling
If the mouse focus/pointer outside the image resize then scrolling is working fine. I think somehow the event is not bubbling up to the ql-editor and it is stopped.
QuillEditor.js
import ReactQuill from 'react-quill';
const Quill = ReactQuill.Quill;
const Embed = Quill.import('blots/embed');
import BlotFormatter from 'quill-blot-formatter/dist/BlotFormatter';
import { CustomImage } from './CustomPlugins/CustomImage';
//to register uploaded image as blot
Quill.register(CustomImage, true);
// Register image resize
Quill.register('modules/blotFormatter', BlotFormatter);
<ReactQuill
ref={editorRef}
className={`w-full ${height} ${className} `}
formats={formats}
id={id}
modules={modules(toolbarId)}
theme="snow"
value={state}
onChange={handleChange}
/>
function modules(toolbarId) {
return {
blotFormatter: {
specs: [CustomImageSpec], //register the custom spec
align: {
icons: alignmentStyles,
},
},
};
}
CustomImageSpec.js
import DeleteIcon from '../../../assets/DeleteIcon.svg';
import { alignToolBar } from './helper';
import ImageSpec from 'quill-blot-formatter/dist/specs/ImageSpec';
// Custom image spec for blot formatter
// Adding delete icon to the blot toolbar on mount phase
export class CustomImageSpec extends ImageSpec {
img;
deleteIcon;
constructor(formatter) {
super(formatter);
this.img = null;
this.deleteIcon = this.createDeleteIcon();
}
createDeleteIcon() {
const spanElement = document.createElement('span');
const imgElement = document.createElement('img');
imgElement.src = DeleteIcon;
imgElement.alt = 'test image';
const clsList = ['blot-formatter__toolbar-button', 'blot-delete-icon'];
spanElement.classList.add(...clsList);
spanElement.appendChild(imgElement);
spanElement.style.cssText =
'display: flex; align-items:center; justify-content:center; width: 24px; height: 24px; cursor:pointer; user-select:none; padding: 2px';
spanElement.addEventListener('click', this.onDeleteIconClick);
return spanElement;
}
init() {
this.formatter.quill.root.addEventListener('click', this.onClick);
this.formatter.quill.root.addEventListener('scroll', (e) => {
this.formatter.repositionOverlay();
});
}
getTargetElement() {
return this.img;
}
onDeleteIconClick = () => {
// Handle delete icon click
if (this.img) {
this.img.remove();
this.formatter.hide();
}
};
onHide() {
this.img = null;
}
onClick = (event) => {
const el = event.target;
if (!el || el.tagName !== 'IMG') {
return;
}
this.img = el;
this.formatter.show(this);
alignToolBar(this.formatter);
this.formatter.overlay
?.getElementsByClassName('blot-formatter__toolbar')[0]
?.appendChild(this.deleteIcon);
};
}
CustomImage.js
import { Quill } from 'react-quill';
// eslint-disable-next-line react-refresh/only-export-components
const BaseImage = Quill.import('formats/image');
// const BaseImage = Quill.import('blots/embed')
// eslint-disable-next-line react-refresh/only-export-components
const ATTRIBUTES = ['alt', 'height', 'width', 'style'];
export class CustomImage extends BaseImage {
static formats(domNode) {
return ATTRIBUTES.reduce(function (formats, attribute) {
if (domNode.hasAttribute(attribute)) {
formats[attribute] = domNode.getAttribute(attribute);
}
return formats;
}, {});
}
format(name, value) {
if (ATTRIBUTES?.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
this.domNode.removeAttribute(name);
}
} else {
super.format(name, value);
}
}
}
//to register uploaded image as blot
Quill.register(CustomImage, true);