react-quill-new
Version:
The Quill rich-text editor as a React component.
347 lines (345 loc) • 11.3 kB
JavaScript
import { isEqual } from 'lodash-es';
import React, { createRef } from 'react';
import Quill from 'quill';
export { default as Quill } from 'quill';
import { jsx } from 'react/jsx-runtime';
// src/index.tsx
var ReactQuill = class extends React.Component {
constructor(props) {
super(props);
this.editingAreaRef = createRef();
this.containerRef = createRef();
/*
Changing one of these props should cause a full re-render and a
re-instantiation of the Quill editor.
*/
this.dirtyProps = ["modules", "formats", "bounds", "theme", "children"];
/*
Changing one of these props should cause a regular update. These are mostly
props that act on the container, rather than the quillized editing area.
*/
this.cleanProps = [
"id",
"className",
"style",
"placeholder",
"tabIndex",
"onChange",
"onChangeSelection",
"onFocus",
"onBlur",
"onKeyPress",
"onKeyDown",
"onKeyUp",
"useSemanticHTML"
];
this.state = {
generation: 0
};
/*
Tracks the internal selection of the Quill editor
*/
this.selection = null;
this.onEditorChange = (eventName, rangeOrDelta, oldRangeOrDelta, source) => {
if (eventName === "text-change") {
this.onEditorChangeText?.(
this.props.useSemanticHTML !== false ? this.editor.getSemanticHTML() : this.editor.root.innerHTML,
rangeOrDelta,
source,
this.unprivilegedEditor
);
} else if (eventName === "selection-change") {
this.onEditorChangeSelection?.(rangeOrDelta, source, this.unprivilegedEditor);
}
};
const value = this.isControlled() ? props.value : props.defaultValue;
this.value = value ?? "";
}
validateProps(props) {
if (React.Children.count(props.children) > 1)
throw new Error("The Quill editing area can only be composed of a single React element.");
if (React.Children.count(props.children)) {
const child = React.Children.only(props.children);
if (child?.type === "textarea")
throw new Error("Quill does not support editing on a <textarea>. Use a <div> instead.");
}
if (this.lastDeltaChangeSet && props.value === this.lastDeltaChangeSet)
throw new Error(
"You are passing the `delta` object from the `onChange` event back as `value`. You most probably want `editor.getContents()` instead. See: https://github.com/zenoamaro/react-quill#using-deltas"
);
}
shouldComponentUpdate(nextProps, nextState) {
this.validateProps(nextProps);
if (!this.editor || this.state.generation !== nextState.generation) {
return true;
}
if ("value" in nextProps) {
const prevContents = this.getEditorContents();
const nextContents = nextProps.value ?? "";
if (!this.isEqualValue(nextContents, prevContents)) {
this.setEditorContents(this.editor, nextContents);
}
}
if (nextProps.readOnly !== this.props.readOnly) {
this.setEditorReadOnly(this.editor, nextProps.readOnly);
}
return [...this.cleanProps, ...this.dirtyProps].some((prop) => {
return !isEqual(nextProps[prop], this.props[prop]);
});
}
shouldComponentRegenerate(nextProps) {
return this.dirtyProps.some((prop) => {
return !isEqual(nextProps[prop], this.props[prop]);
});
}
componentDidMount() {
this.instantiateEditor();
this.setEditorContents(this.editor, this.getEditorContents());
}
componentWillUnmount() {
this.destroyEditor();
}
componentDidUpdate(prevProps, prevState) {
if (this.editor && this.shouldComponentRegenerate(prevProps)) {
const delta = this.editor.getContents();
const selection = this.editor.getSelection();
this.regenerationSnapshot = { delta, selection };
this.setState({ generation: this.state.generation + 1 });
this.destroyEditor();
}
if (this.editor && prevProps.placeholder !== this.props.placeholder) {
this.editor.root.dataset.placeholder = this.props.placeholder || "";
}
if (this.state.generation !== prevState.generation) {
const { delta, selection } = this.regenerationSnapshot;
delete this.regenerationSnapshot;
this.instantiateEditor();
const editor = this.editor;
editor.setContents(delta);
postpone(() => this.setEditorSelection(editor, selection));
}
}
instantiateEditor() {
if (this.editor) {
this.hookEditor(this.editor);
} else {
this.editor = this.createEditor(this.getEditingArea(), this.getEditorConfig());
}
}
destroyEditor() {
if (!this.editor) return;
this.unhookEditor(this.editor);
const toolbar = this.props.modules?.toolbar;
const usingExternalToolbar = typeof toolbar === "object" && toolbar && "container" in toolbar && typeof toolbar.container === "string" || typeof toolbar === "string";
if (!usingExternalToolbar) {
const leftOverToolbar = this.containerRef.current?.querySelector(".ql-toolbar");
if (leftOverToolbar) {
leftOverToolbar.remove();
}
}
delete this.editor;
}
/*
We consider the component to be controlled if `value` is being sent in props.
*/
isControlled() {
return "value" in this.props;
}
getEditorConfig() {
return {
bounds: this.props.bounds,
formats: this.props.formats,
modules: this.props.modules,
placeholder: this.props.placeholder,
readOnly: this.props.readOnly,
tabIndex: this.props.tabIndex,
theme: this.props.theme
};
}
getEditor() {
if (!this.editor) throw new Error("Accessing non-instantiated editor");
return this.editor;
}
/**
Creates an editor on the given element. The editor will be passed the
configuration, have its events bound,
*/
createEditor(element, config) {
const editor = new Quill(element, config);
if (config.tabIndex != null) {
this.setEditorTabIndex(editor, config.tabIndex);
}
this.hookEditor(editor);
return editor;
}
hookEditor(editor) {
this.unprivilegedEditor = this.makeUnprivilegedEditor(editor);
editor.on("editor-change", this.onEditorChange);
}
unhookEditor(editor) {
editor.off("editor-change", this.onEditorChange);
}
getEditorContents() {
return this.value;
}
getEditorSelection() {
return this.selection;
}
/*
True if the value is a Delta instance or a Delta look-alike.
*/
isDelta(value) {
return value && value.ops;
}
/*
Special comparison function that knows how to compare Deltas.
*/
isEqualValue(value, nextValue) {
if (this.isDelta(value) && this.isDelta(nextValue)) {
return isEqual(value.ops, nextValue.ops);
} else {
return isEqual(value, nextValue);
}
}
/*
Replace the contents of the editor, but keep the previous selection hanging
around so that the cursor won't move.
*/
setEditorContents(editor, value) {
this.value = value;
const sel = this.getEditorSelection();
if (typeof value === "string") {
editor.setContents(editor.clipboard.convert({ html: value }));
} else {
editor.setContents(value);
}
postpone(() => this.setEditorSelection(editor, sel));
}
setEditorSelection(editor, range) {
this.selection = range;
if (range) {
const length = editor.getLength();
range.index = Math.max(0, Math.min(range.index, length - 1));
range.length = Math.max(0, Math.min(range.length, length - 1 - range.index));
editor.setSelection(range);
}
}
setEditorTabIndex(editor, tabIndex) {
if (editor?.scroll?.domNode) {
editor.scroll.domNode.tabIndex = tabIndex;
}
}
setEditorReadOnly(editor, value) {
if (value) {
editor.disable();
} else {
editor.enable();
}
}
/*
Returns a weaker, unprivileged proxy object that only exposes read-only
accessors found on the editor instance, without any state-modifying methods.
*/
makeUnprivilegedEditor(editor) {
const e = editor;
return {
getHTML: () => e.root.innerHTML,
getSemanticHTML: e.getSemanticHTML.bind(e),
getLength: e.getLength.bind(e),
getText: e.getText.bind(e),
getContents: e.getContents.bind(e),
getSelection: e.getSelection.bind(e),
getBounds: e.getBounds.bind(e)
};
}
getEditingArea() {
const element = this.editingAreaRef.current;
if (!element) {
throw new Error("Cannot find element for editing area");
}
if (element.nodeType === 3) {
throw new Error("Editing area cannot be a text node");
}
return element;
}
/*
Renders an editor area, unless it has been provided one to clone.
*/
renderEditingArea() {
const { children, preserveWhitespace } = this.props;
const { generation } = this.state;
if (React.Children.count(children)) {
return React.cloneElement(React.Children.only(children), {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
key: generation,
ref: this.editingAreaRef
});
}
return preserveWhitespace ? /* @__PURE__ */ jsx("pre", { ref: this.editingAreaRef }, generation) : /* @__PURE__ */ jsx("div", { ref: this.editingAreaRef }, generation);
}
render() {
return /* @__PURE__ */ jsx(
"div",
{
ref: this.containerRef,
id: this.props.id,
style: this.props.style,
className: `quill ${this.props.className ?? ""}`,
onKeyPress: this.props.onKeyPress,
onKeyDown: this.props.onKeyDown,
onKeyUp: this.props.onKeyUp,
children: this.renderEditingArea()
},
this.state.generation
);
}
onEditorChangeText(value, delta, source, editor) {
if (!this.editor) return;
const nextContents = this.isDelta(this.value) ? editor.getContents() : this.props.useSemanticHTML !== false ? editor.getSemanticHTML() : editor.getHTML();
if (nextContents !== this.getEditorContents()) {
this.lastDeltaChangeSet = delta;
this.value = nextContents;
this.props.onChange?.(value, delta, source, editor);
}
}
onEditorChangeSelection(nextSelection, source, editor) {
if (!this.editor) return;
const currentSelection = this.getEditorSelection();
const hasGainedFocus = !currentSelection && nextSelection;
const hasLostFocus = currentSelection && !nextSelection;
if (isEqual(nextSelection, currentSelection)) return;
this.selection = nextSelection;
this.props.onChangeSelection?.(nextSelection, source, editor);
if (hasGainedFocus) {
this.props.onFocus?.(nextSelection, source, editor);
} else if (hasLostFocus) {
this.props.onBlur?.(currentSelection, source, editor);
}
}
focus() {
if (!this.editor) return;
this.editor.focus();
}
blur() {
if (!this.editor) return;
this.selection = null;
this.editor.blur();
}
};
ReactQuill.displayName = "React Quill";
/*
Export Quill to be able to call `register`
*/
ReactQuill.Quill = Quill;
ReactQuill.defaultProps = {
theme: "snow",
modules: {},
readOnly: false
};
function postpone(fn) {
Promise.resolve().then(fn);
}
var index_default = ReactQuill;
export { index_default as default };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map