@boomerang-io/carbon-addons-boomerang-react
Version:
Carbon Addons for Boomerang apps
155 lines (152 loc) • 9.3 kB
JavaScript
import React, { useRef, useState, useEffect } from 'react';
import { pkg, Toolbar, ToolbarGroup, ToolbarButton } from '@carbon/ibm-products';
import 'quill/dist/quill.snow.css';
import { prefix } from '../../internal/settings.js';
import { TextBold, TextItalic, TextUnderline, ListBulleted, ListNumbered, Link } from '@carbon/react/icons';
import { TextInput, Button } from '@carbon/react';
import cx from 'classnames';
import sanitizeHtml from 'sanitize-html';
/*
IBM Confidential
694970X, 69497O0
© Copyright IBM Corp. 2022, 2025
*/
const RichTextAreaComponent = React.forwardRef(function RichTextAreaComponent({ label, labelText, maxWordCount, value, helperText, placeholder, onChange, setError, invalid, customError, readOnly, quillProps, }, ref) {
pkg.component.ToolbarGroup = pkg.component.Toolbar = pkg.component.ToolbarButton = true;
const labelValue = label || labelText;
const editorRef = useRef(null);
const [url, setUrl] = useState("");
const [wordCount, setWordCount] = useState(0);
const [savedSelection, setSavedSelection] = useState(null);
const [showUrlInput, setShowUrlInput] = useState(false);
const [noSelection, setNoSelection] = useState(false);
const quillRef = useRef(null);
useEffect(() => {
(async () => {
const { default: Quill } = await import('quill');
if (editorRef.current) {
const quill = new Quill(editorRef.current, {
theme: "snow",
modules: {
toolbar: false,
},
placeholder: !readOnly ? placeholder : "",
readOnly,
...quillProps,
});
quillRef.current = quill;
if (value) {
const cleanHtml = sanitizeHtml(value, {
allowedTags: ["p", "strong", "b", "i", "em", "u", "ol", "ul", "li", "a"],
allowedAttributes: {
a: ["href", "rel", "target"],
},
});
quillRef.current.clipboard.dangerouslyPasteHTML(cleanHtml);
const length = quillRef.current.getLength();
quillRef.current.setSelection(length, 0);
setWordCount(getWordCount());
if (setError) {
setError(maxWordCount && getWordCount() > maxWordCount);
}
}
quill.on("text-change", () => {
const currentCount = getWordCount();
setWordCount(currentCount);
const wordCountExceeded = maxWordCount && currentCount > maxWordCount;
if (onChange && !wordCountExceeded) {
onChange(quill.getSemanticHTML());
}
if (setError) {
setError(wordCountExceeded);
}
});
}
})()
.catch(error => {
console.error(error);
});
}, []);
const handleBold = () => {
quillRef.current?.format("bold", !quillRef.current.getFormat().bold);
};
const handleItalic = () => {
quillRef.current?.format("italic", !quillRef.current.getFormat().italic);
};
const handleUnderline = () => {
quillRef.current?.format("underline", !quillRef.current.getFormat().underline);
};
const handleOrderedList = () => {
quillRef.current?.format("list", "ordered");
};
const handleBulletList = () => {
quillRef.current?.format("list", "bullet");
};
const handleLinkBtn = () => {
const selection = quillRef.current?.getSelection();
if (selection && selection.length > 0) {
setSavedSelection(selection);
setShowUrlInput(true);
}
else {
setNoSelection(true);
}
};
const handleLinkInsert = () => {
if (url) {
const range = savedSelection || quillRef.current?.getSelection();
if (range?.length) {
quillRef.current?.formatText(range.index, range.length, "link", url);
}
}
setShowUrlInput(false);
setSavedSelection(null);
setUrl("");
};
const handleLinkCancel = () => {
setShowUrlInput(false);
setSavedSelection(null);
setUrl("");
};
const getWordCount = () => {
if (quillRef.current && maxWordCount) {
const text = quillRef.current.getText();
const words = text.trim().split(/\s+/);
return words.filter((word) => word.length > 0).length;
}
return 0;
};
const wordCountExceeded = maxWordCount && wordCount > maxWordCount;
return (React.createElement(React.Fragment, null,
React.createElement("div", { className: `${prefix}--rich-text-editor-labels` },
labelValue ? (React.createElement("div", { "data-testid": "rich-text-editor-label", className: `${prefix}--label` }, labelValue)) : null,
maxWordCount ? (React.createElement("div", { "data-testid": "rich-text-editor-word-count", className: cx(`${prefix}--label`, { [`${prefix}--rich-text-editor-error`]: wordCount > maxWordCount }) }, `${wordCount}/${maxWordCount}`)) : null),
(!readOnly) ?
React.createElement(Toolbar, { className: `${prefix}--rich-text-editor-toolbar` },
React.createElement(ToolbarGroup, null,
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-bold-btn", onClick: handleBold, label: "Bold", renderIcon: (props) => React.createElement(TextBold, { size: 16, ...props }) }),
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-italic-btn", onClick: handleItalic, label: "Italic", renderIcon: (props) => React.createElement(TextItalic, { size: 16, ...props }) }),
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-underline-btn", onClick: handleUnderline, label: "Underline", renderIcon: (props) => React.createElement(TextUnderline, { size: 16, ...props }) }),
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-bullet-list-btn", onClick: handleBulletList, label: "Bulleted List", renderIcon: (props) => React.createElement(ListBulleted, { size: 16, ...props }) }),
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-numbered-list-btn", onClick: handleOrderedList, label: "Numbered List", renderIcon: (props) => React.createElement(ListNumbered, { size: 16, ...props }) }),
React.createElement(ToolbarButton, { "data-testid": "rich-text-editor-hyperlink-btn", onClick: () => handleLinkBtn(), label: "Hyperlink", renderIcon: (props) => React.createElement(Link, { size: 16, ...props }) })))
: null,
showUrlInput && (React.createElement("div", { className: `${prefix}--rich-text-editor-url-input` },
React.createElement(TextInput, { onChange: (e) => {
setUrl(e.target.value);
}, id: "hyperlink-input", "data-testid": "rich-text-editor-hyperlink-input", placeholder: "Enter URL", size: "sm", type: "url", labelText: "" }),
React.createElement(Button, { "data-testid": "rich-text-editor-hyperlink-input-ok-btn", onClick: () => handleLinkInsert(), kind: "ghost", size: "sm" }, "OK"),
React.createElement(Button, { "data-testid": "rich-text-editor-hyperlink-input-cancel-btn", onClick: () => handleLinkCancel(), kind: "ghost", size: "sm" }, "Cancel"))),
React.createElement("div", { "data-testid": "rich-text-editor-text-area", className: cx({
[`${prefix}--rich-text-editor-error-border`]: wordCountExceeded,
}) },
React.createElement("div", { className: cx(`${prefix}--rich-text-editor`, {
[`${prefix}--rich-text-editor-disabled`]: readOnly,
}), onFocus: () => setNoSelection(false), ref: editorRef })),
React.createElement("div", { "data-testid": "rich-text-editor-footer", className: `${prefix}--rich-text-editor-footer` },
!noSelection && !wordCountExceeded && helperText ? (React.createElement("div", { "data-testid": "rich-text-editor-helper-text", className: `${prefix}--label` }, helperText)) : null,
wordCountExceeded && !customError ? (React.createElement("div", { "data-testid": "rich-text-editor-wordCount-error", className: cx(`${prefix}--label`, `${prefix}--rich-text-editor-error`) }, "Exceeded Word Count")) : null,
noSelection ? (React.createElement("div", { "data-testid": "rich-text-editor-link-input-error", className: cx(`${prefix}--label`, `${prefix}--rich-text-editor-error`) }, "Select text before adding link")) : null,
invalid ? (React.createElement("div", { className: cx(`${prefix}--label`, `${prefix}--rich-text-editor-error`) }, customError)) : null)));
});
export { RichTextAreaComponent as default };