UNPKG

@boomerang-io/carbon-addons-boomerang-react

Version:
155 lines (152 loc) 9.16 kB
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 };